Prototype chaining is the default way to implement inheritance.
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; }; }here is the inheritance magic code:
TwoDShape.prototype = new Shape(); TwoDShape.prototype.constructor = TwoDShape; Triangle.prototype = new TwoDShape(); Triangle.prototype.constructor = Triangle; var tri = new Triangle(5, 10); tri.getArea(); // 25
Instead of augmenting the object in the prototype
property of TwoDShape
with individual properties,
completely overwrite it with another object, created by invoking the Shape()
constructor with new
.
The same for Triangle
: its prototype is replaced with an object created by new TwoDShape()
.
The important thing to note is that JavaScript works with objects, not classes.
An instance is created using the new Shape()
constructor and after that its properties can be inherited;
After inheriting, whatever modification to Shape()
function, will have no effect on TwoDShape
.
This is because inheritance works on one instance: the instance to inherit from.
Although the tri
object doesn't have its own toString()
method, it inherited one and can call it.
tri.toString() // "Triangle"the inherited method
toString()
binds thethis
object totri
.
Here is what the JavaScript engine does when you call my.toString()
:
- it loops through all of the properties of
tri
and doesn't find a method calledtoString()
; - it looks at the object that
my.__proto__
points to; this object is the instancenew TwoDShape()
created during the inheritance process. - now the JavaScript engine loops through the instance of
TwoDShape
and doesn't find atoString()
method. It then checks the__proto__
of that object. This time__proto__
points to the instance created bynew Shape()
; - the instance of
new Shape()
is examined andtoString()
is finally found; - this method is invoked in the context of
tri
, meaning thatthis
points totri
.
tri.constructor; // Triangle(side, height)tri instanceof Shape // true tri instanceof TwoDShape // true tri instanceof Triangle // true tri instanceof Array // falseShape.prototype.isPrototypeOf(tri) // true TwoDShape.prototype.isPrototypeOf(tri) // true Triangle.prototype.isPrototypeOf(tri) // true String.prototype.isPrototypeOf(tri) // false
Of course, it's possible to create objects using the other two constructors.
var td = new TwoDShape(); td.constructor; // TwoDShape() td.toString() var s = new Shape(); s.constructor; // Shape()
When creating objects using a constructor function, own properties are added using this
.
This could be inefficient in cases where properties don't change across instances.
function Shape() {
this.name = 'shape';
}
In the example above, every time new object is created using new Shape()
, a new name
property will be created and stored somewhere in 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';
In this way every time an object is created using new Shape()
, this object will not have its own property name
, but will use 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 on the example above by adding all methods and suitable properties to the prototype.
In the case of Shape()
and TwoDShape()
everything is meant to be shared.
function Shape() {}
// augment prototype
Shape.prototype.name = 'shape';
Shape.prototype.toString = function() { return this.name; };
function TwoDShape() {}
// take care of inheritance first before augmenting the prototype,
// otherwise anything you add to TwoDShape.prototype will be wiped out when you inherit.
TwoDShape.prototype = new Shape();
TwoDShape.prototype.constructor = TwoDShape;
// augment prototype
TwoDShape.prototype.name = '2D shape';
The Triangle constructor is a little different, because every object it creates is a new triangle, which may have different dimensions.
So it's good to keep side
and height
as its own properties and share the rest.
The method getArea()
, for example, is the same regardless of the actual dimensions of each triangle.
function Triangle(side, height) {
this.side = side;
this.height = height;
}
// take care of inheritance first before augmenting the prototype,
Triangle.prototype = new TwoDShape();
Triangle.prototype.constructor = Triangle;
// augment prototype
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function() { return this.side * this.height / 2; };
var tri = new Triangle(5, 10);
tri.getArea(); // 25
tri.toString(); // "Triangle"
tri.hasOwnProperty('side'); // true
tri.hasOwnProperty('name'); // false
TwoDShape.prototype.isPrototypeOf(tri); // true
tri instanceof Shape // true
for reasons of efficiency, it's good to add the reusable properties and methods to the prototype.
If previous tip is received, then it's probably a good idea to inherit only the prototype, because all the reusable code is there.
This means that inheriting the object contained in Shape.prototype
is better than inheriting the object created with new Shape()
.
To gain a little more efficiency:
not creating a new object for the sake of inheritance alone,
not having less lookups during runtime when it comes to searching for toString()
for example.
function Shape() {}
// augment prototype
Shape.prototype.name = 'shape';
Shape.prototype.toString = function() {return this.name;};
function TwoDShape() {}
// take care of inheritance
TwoDShape.prototype = Shape.prototype;
TwoDShape.prototype.constructor = TwoDShape;
// augment prototype
TwoDShape.prototype.name = '2D shape';
function Triangle(side, height) {
this.side = side;
this.height = height;
}
// take care of inheritance
Triangle.prototype = TwoDShape.prototype;
Triangle.prototype.constructor = Triangle;
// augment prototype
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function() { return this.side * this.height / 2; }
var tri = new Triangle(5, 10);
tri.getArea(); // 25
tri.toString(); // "Triangle"
So the lookup is only a two-step process as opposed to four (in the previous example) or three (in the first example).
Simply copying the prototype is more efficient but it has a side effect:
because all of the children and parents point to the same object, when a child modifies the prototype, the parents get the changes, and so do the siblings.
Triangle.prototype.name = 'Triangle'; //it effectively changes Shape.prototype.name
var s = new Shape();
s.name // "Triangle"
A solution to the problem outlined above, is to use an intermediary to break the chain.
The intermediary is in the form of a temporary constructor function.
Creating an empty function F()
and setting its prototype to the prototype of the parent constructor, allows to call new F()
and create objects that have no properties of their own, but inherit everything from the parent's prototype.
function Shape() {}
// augment prototype
Shape.prototype.name = 'shape';
Shape.prototype.toString = function() {return this.name;};
function TwoDShape() {}
// take care of inheritance
var F = function(){};
F.prototype = Shape.prototype;
TwoDShape.prototype = new F();
TwoDShape.prototype.constructor = TwoDShape;
// augment prototype
TwoDShape.prototype.name = '2D shape';
function Triangle(side, height) {
this.side = side;
this.height = height;
}
// take care of inheritance
var F = function(){};
F.prototype = TwoDShape.prototype;
Triangle.prototype = new F();
Triangle.prototype.constructor = Triangle;
// augment prototype
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function() { return this.side * this.height / 2; };
var my = new Triangle(5, 10);
my.getArea(); // 25
my.toString(); // "Triangle"
Using this approach, we keep the prototype chain in place
and the parents' properties are not overwritten by the children.
In fact:
var s = new Shape();
s.name; // "shape"
At the same time, this approach supports the idea that:
- only properties and methods added to the prototype should be inherited, and
- own properties should not be inherited.
The rationale behind this is are that own properties are likely to be too specific to be reusable.
Let's move the code that takes care of all of the inheritance details into a reusable inherits()
function:
function inherits(Child, Parent) {
var F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
Using this function (or your own custom version of it) will help you keep your code clean with regard to the repetitive inheritance-related tasks.
This way you can inherit by simply using:
inherits(Child, Parent);
So in our shapes examples, inheritance works as follow:
function Shape() {} // augment prototype Shape.prototype.name = 'shape'; Shape.prototype.toString = function() {return this.name;}; function TwoDShape() {} // take care of inheritance inherits(TwoDShape, Shape); // augment prototype TwoDShape.prototype.name = '2D shape'; function Triangle(side, height) { this.side = side; this.height = height; } // take care of inheritance inherits(Triangle, TwoDShape); // augment prototype Triangle.prototype.name = 'Triangle'; Triangle.prototype.getArea = function() { return this.side * this.height / 2; }; var my = new Triangle(5, 10); my.getArea(); // 25 my.toString(); // "Triangle"