Chapter 7. Inheritance

If you go back to Chapter 1, Object-Oriented JavaScript, and review the Object-oriented programming section, you'll see that you already know how to apply most of them to JavaScript. You know what objects, methods, and properties are. You know that there are no classes in ES5, although you can achieve them using constructor functions. ES6 introduces the notion of classes; we will take a detailed look at how ES6 classes work in the next chapter. Encapsulation? Yes, the objects encapsulate both the data and the means (methods) to do something with the data. Aggregation? Sure, an object can contain other objects. In fact, this is almost always the case since methods are functions and functions are also objects.

Now, let's focus on the inheritance part. This is one of the most interesting features, as it allows you to reuse existing code, thus promoting laziness, which is likely to be what brought human species to computer programming in the first place.

JavaScript is a dynamic language, and there is usually more than one way to achieve any given task. Inheritance is not an exception. In this chapter, you'll see some common patterns for implementing inheritance. Having a good understanding of these patterns will help you pick the right one, or the right mix, depending on your task, project, or style.

Prototype chaining

Let's start with the default way of implementing inheritance - inheritance chaining through the prototype.

As you already know, every function has a prototype property, which points to an object. When a function is invoked using the new operator, an object is created and returned. This new object has a secret link to the prototype object. The secret link (called __proto__ in some environments) allows methods and properties of the prototype object to be used as if they belonged to the newly created object.

The prototype object is just a regular object and, therefore, it also has the secret link to its prototype. And so, a chain called a prototype chain is created:

Prototype chaining

In this illustration, an object A contains a number of properties. One of the properties is the hidden __proto__ property, which points to another object, B. B's __proto__ property points to C. This chain ends with the Object.prototype object, the grandparent, and every object inherits from it.

This is all good to know, but how does it help you? The practical side is that when object A lacks a property but B has it, A can still access this property as its own. The same applies if B also doesn't have the required property, but C does. This is how inheritance takes place - an object can access any property found somewhere down the inheritance chain.

Throughout this chapter, you'll see different examples that use the following hierarchy - a generic Shape parent is inherited by a 2D shape, which in turn is inherited by any number of specific two-dimensional shapes such as a triangle, rectangle, and so on.

Prototype chaining example

Prototype chaining is the default way to implement inheritance. In order to implement the hierarchy, let's define three constructor functions:

    function Shape(){ 
    this.name = 'Shape'; 
    this.toString = function () { 
        return this.name; 
      }; 
    } 
 
    function TwoDShape(){ 
      this.name = '2D shape'; 
    } 
 
    function Triangle(side, height){ 
      this.name = 'Triangle'; 
      this.side = side; 
      this.height = height; 
      this.getArea = function () { 
        return this.side * this.height / 2; 
      }; 
    } 

The code that performs the inheritance magic is as follows:

    TwoDShape.prototype = new Shape(); 
    Triangle.prototype = new TwoDShape(); 

What's happening here? You take the object contained in the prototype property of TwoDShape, and instead of augmenting it with individual properties, you completely overwrite it with another object, created by invoking the Shape() constructor with new. The same process can be followed for Triangle-its prototype is replaced by an object created by new TwoDShape(). It's important to remember that JavaScript works with objects, not classes. You need to create an instance using the new Shape() constructor, and after that, you can inherit its properties; you don't inherit from Shape() directly. Additionally, after inheriting, you can modify the Shape() constructor, overwrite it, or even delete it, and this will have no effect on TwoDShape, because all you needed is one instance to inherit from.

As you know from the previous chapter, overwriting the prototype (as opposed to just adding properties to it), has side effects on the constructor property. Therefore, it's a good idea to reset the constructor property after inheriting. Consider the following example:

    TwoDShape.prototype.constructor = TwoDShape; 
    Triangle.prototype.constructor = Triangle; 

Now, let's test what has happened so far. Creating a Triangle object and calling its own getArea() method works as expected:

    >var my = new Triangle(5, 10); 
    >my.getArea(); 
    25 

Although the my object doesn't have its own toString() method, it inherited one and you can call it. Note how the inherited method toString() binds the this object to my:

    >my.toString(); 
    "Triangle" 

It's fascinating to consider what the JavaScript engine does when you call my.toString():

  • It loops through all of the properties of my and doesn't find a method called toString().
  • It looks at the object that my.__proto__ points to this object is the instance new TwoDShape() created during the inheritance process.
  • Now, the JavaScript engine loops through the instance of TwoDShape and doesn't find a toString() method. It then checks __proto__ of that object. This time, __proto__ points to the instance created by new Shape().
  • The instance of new Shape() is examined, and toString() is finally found.
  • This method is invoked in the context of my, meaning that this points to my.

If you ask my, Who's your constructor?, it reports it correctly because of the reset of the constructor property after the inheritance:

    >my.constructor === Triangle; 
    true 

Using the instanceof operator, you can validate that my is an instance of all three constructors:

    > my instanceof Shape; 
    true 
    > my instanceofTwoDShape; 
    true 
    > my instanceof Triangle; 
    true 
    > my instanceof Array; 
    false 

The same happens when you call isPrototypeOf()on the constructors by passing my:

    >Shape.prototype.isPrototypeOf(my); 
    true 
    >TwoDShape.prototype.isPrototypeOf(my); 
    true 
    >Triangle.prototype.isPrototypeOf(my); 
    true 
    >String.prototype.isPrototypeOf(my); 
    false 

You can also create objects using the other two constructors. Objects created with new TwoDShape() also get the toString()method inherited from Shape():

    >var td = new TwoDShape(); 
    >td.constructor === TwoDShape; 
    true 
    >td.toString(); 
    "2D shape" 
    >var s = new Shape(); 
    >s.constructor === Shape; 
    true 

Moving shared properties to the prototype

When you create objects using a constructor function, own properties are added using this. This could be inefficient in cases where properties don't change across instances. In the previous example, Shape() was defined as follows:

    function Shape(){ 
    this.name = 'Shape'; 
    } 

This means that every time you create a new object using new Shape(), a new name property is created and stored somewhere in the memory. The other option is to have the name property added to the prototype and shared among all the instances:

    function Shape() {} 
    Shape.prototype.name = 'Shape'; 

Now, every time you create an object using new Shape(), this object doesn't get its own property name, but uses the one added to the prototype. This is more efficient, but you should only use it for properties that don't change from one instance to another. Methods are ideal for this type of sharing.

Let's improve the preceding example by adding all methods and suitable properties to prototype. In the case of Shape() and TwoDShape(), everything is meant to be shared:

    // constructor 
    function Shape() {} 
 
    // augment prototype 
    Shape.prototype.name = 'Shape'; 
    Shape.prototype.toString = function () { 
      return this.name; 
    }; 
 
    // another constructor 
    function TwoDShape() {} 
 
    // take care of inheritance 
    TwoDShape.prototype = new Shape(); 
    TwoDShape.prototype.constructor = TwoDShape; 
 
    // augment prototype 
    TwoDShape.prototype.name = '2D shape'; 

As you can see, you have to take care of inheritance first before augmenting the prototype. Otherwise, anything you add to TwoDShape.prototype gets wiped out when you inherit.

The Triangle constructor is a little different, because every object it creates is a new triangle, which is likely to have different dimensions. So, it's good to keep side and height as own properties and share the rest. The getArea() method, for example, is the same, regardless of the actual dimensions of each triangle. Again, you do the inheritance bit first and then augment the prototype:

    function Triangle(side, height) { 
    this.side = side; 
    this.height = height; 
    } 
    // take care of inheritance 
    Triangle.prototype = new TwoDShape(); 
    Triangle.prototype.constructor = Triangle; 
 
    // augment prototype 
    Triangle.prototype.name = 'Triangle'; 
    Triangle.prototype.getArea = function () { 
    return this.side * this.height / 2; 
    }; 

All the preceding test code works exactly the same. Here is an example:

    >var my = new Triangle(5, 10); 
    >my.getArea(); 
    25 
    >my.toString(); 
    "Triangle" 

There is only a slight behind-the-scenes difference when calling my.toString(). The difference is that there is one more lookup to be done before the method is found in Shape.prototype, as opposed to in the new Shape() instance, like it was in the previous example.

You can also play with hasOwnProperty() to see the difference between the own property versus a property coming down the prototype chain:

    >my.hasOwnProperty('side'), 
    true 
    >my.hasOwnProperty('name'), 
    false 

The calls to isPrototypeOf() and the instanceof operator from the previous example work in exactly the same way:

    >TwoDShape.prototype.isPrototypeOf(my); 
    true 
    > my instanceof Shape; 
    true 
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.174.239