The Javascript Prototype in action: Inheritance

In my previous posts, we have learned about the Javascript Object Model and how to create classes using prototypes. In this post, we shall build upon our previous knowledge and apply inheritance to our Javascript classes.

Before we begin, we must know a few things:

  1. ECMAScript 2015 has introduced the ‘extends’ keyword in Javascript to represent inheritance between two classes.
  2. However, for our purposes, we will be going about inheritance the traditional way of using functions and prototypes. We adopt this approach to get a deeper understanding of how things work.

The Javascript Prototypal Inheritance Model

The object model of Javascript is based on prototypes. All Javascript objects inherit their methods and properties from a prototype object. When we create a class using constructor functions, we specify the methods we want our class instances to have by using the prototype.

Similarly, Javascript uses prototypes in its inheritance model. This model is called the prototypal inheritance model.

In this model, there is a chain of prototype objects, one after another. Whenever a method on an object is called, the prototype of that object is first searched for the definition of that method. If the method definition is not found, then the prototype object higher in the prototype chain is searched. This process continues all the way up the chain.
If the method definition is found, it is executed. If not, then an error is raised.

Let us understand the above concepts with an example of our own.

Our parent class

Let us create our parent Rectangle class. Our Rectangle object will have two attributes: a length and a breadth. There will also be methods to calculate the area, perimeter, and display the sides of the rectangle on the screen.

Type in the following code:

function Rectangle(rlength,rbreadth)
{
  this._length=rlength;
  this._breadth=rbreadth;
};
Rectangle.prototype.area=function()
{
  return this._length*this._breadth;
};
Rectangle.prototype.perimeter=function()
{
  return 2*(this._length+this._breadth);
};
Rectangle.prototype.display=function()
{
  console.log(`Length:${this._length},Breadth:${this._breadth}`);
};

Our constructor function accepts the length and the breadth as parameters. Our parent class prototype defines three methods ‘area’, ‘perimeter’ and ‘display’. These methods will be available to all objects of this class.

Let us test our class.

const r=new Rectangle(4,5);
r.display();
console.log(`Perimeter: ${r.perimeter()}`);
console.log(`Area: ${r.area()}`);

Our output is:

Length:4,Breadth:5
Perimeter: 18
Area: 20

Our child class

A square is a rectangle whose length and breadth are the same. So let us create a Square class that inherits from the Rectangle class.

function Square(side)
{
  Rectangle.call(this,side,side);
}

Our Square class requires only one parameter, the side, for its construction. Now let us look at the ‘call’ function.

The ‘call’ method: the equivalent of ‘super’

Javascript defines the ‘call’ function as ‘Function.prototype.call’, i.e., as a method available to all function objects.
Thus, if ‘func’ is a function, then we use the ‘call’ method like ‘func.call(<arguments>)’.
Now let us understand this function.

The ‘call’ method takes several arguments:

  1. The first is a value that must be used as ‘this’ inside the context of ‘func’ when it will be executed.
  2. All the other arguments are those that are passed to ‘func’ when it is called naturally.

In short, the ‘call’ function provides a method for us to call a function while specifying which object should be used as ‘this’ in the context of the function.

In this case, we are invoking the Rectangle constructor function. The constructor function is invoked on our Square instance as that is what we are passing in as the first argument. The remaining two arguments are what is expected by the Rectangle constructor function. As both sides of a square are the same, we pass in the same value twice.

In short, we are executing our Rectangle constructor with our Square instance replacing every occurrence of ‘this’ inside it. The result is that all attributes set by the Rectangle constructor for Rectangle objects are also set for our Square objects.
This is just like calling ‘super’ in Java or Python or calling our parent constructor in C++.

Creating the prototype chain

Our next step is to create the prototype chain between the Square and Rectangle constructor functions.

When we first create a constructor function, its default prototype is created, which inherits from Object.prototype. The same is true here. If you create a new Square object and call one of the Rectangle methods on it, you will get an error.

const s=new Square(4);
s.display();

The error message is:

Uncaught TypeError: s.display is not a function

This happens because our Square.prototype is inheriting from Object.prototype and not from Rectangle.prototype. Our squares are completely unaware of the methods of Rectangle. To make this possible, we have to redefine our Square.prototype so that it inherits from Rectangle.prototype instead of Object.prototype.

Redefine Square.prototype as so:

Square.prototype=Object.create(Rectangle.prototype);

Several things are happening here. Let us look at them one by one.

  1. Object.create returns an object that inherits from the argument object passed to it. Here, it returns an object that inherits from Rectangle.prototype.
  2. That object is then assigned to Square.prototype. Effectively, we are reassigning Square.prototype to an object that inherits from Rectangle.prototype. So now Square.prototype inherits from Rectangle.prototype.

Our prototype chain is almost complete.

Linking the ‘constructor’ attribute

We have created a new Square.prototype, just as we want it. But we are not done yet. Log our Square.prototype to the browser.

console.log(Square.prototype);

Our output is:

Our Square.prototype inherits from Rectangle.prototype. However, its ‘constructor’ property, which must point to the Square constructor function, is not set. So let us set that.

Object.defineProperty(
  Square.prototype,
  'constructor',
  {
    value:Square,
    ennumerable:true,
    writable:false,
  }
);

Here, we define the ‘constructor’ property on our Square.prototype using the Object.defineProperty function.

Object.defineProperty accepts three parameters:

  1. The target object on which we are setting the property. Here it is Square.prototype.
  2. The name used to reference the property. Here the name is ‘constructor’. So we will be able to refer to it as Square.prototype.constructor.
  3. An object describing the nature of the property being set. Here, we set the ‘constructor’ property to refer to the Square constructor function. Our property is enumerable and once set, it cannot be changed, i.e., cannot be written to. It is read-only.
    Note that an attempt to assign the constructor will raise an error in strict mode, and will fail silently otherwise.

Log your Square.prototype to the console now. The ‘constructor’ property is properly set to refer to the Square constructor function.

Testing our inheritance chain

We have successfully created our prototype chain. Now it is time to test it. Execute the following code:

const s=new Square(4);
s.display();
console.log(s.perimeter());
console.log(s.area());

The browser output is:

Length:4,Breadth:4
16
16

We create our square with side 4. We call our methods on it. These methods are all defined on Rectangle.prototype.

When we call any method on our square object, the browser first checks for the definition of that method in Square.prototype. If no definition is found, Rectangle.prototype is checked next. This process continues all the way up the prototype chain. If the method definition is found, it is executed. If not, an error is raised.

Method overriding

In our Square class, the method ‘display’ does not seem quite right. As a square, we only want the side. We do not want to display a separate length and breadth. Also as the length and breadth are equal, we can optimize our perimeter method over what we are inheriting from Rectangle.

Type in the following code:

Square.prototype.perimeter=function()
{
  return 4*this._length;
};
Square.prototype.display=function()
{
  console.log(`Side:${this._length}`);
};

Calling our methods now, we get the desired result.

s.display();
console.log(s.perimeter());
Our output is
Side:4
16

Here, we define methods on Square.prototype which have the same name as those in Rectangle.prototype. While traversing the prototype chain, the definitions in Square.prototype are encountered first. Thus, the methods defined here are the ones that get executed. We verify this by observing the output of the ‘display’ method.

Note that the instance variables have the same name as those defined in ‘Rectangle’.

Also note that for Rectangle objects, the prototype chain is traversed from Rectangle.prototype upwards. Thus the methods defined on Square.prototype are not visible.

Wrapping up

The following are some diagrammatical representations of the steps we have performed so far:

  • Creating our two classes.
  • Linking the prototype chain.
  • Setting the constructor attribute.

 

Leave a Reply

Your email address will not be published. Required fields are marked *