JavaScript OOP Course 4: Prototypical Inheritance
References
- Code with Mosh: The Ultimate JavaScript Mastery Series – Part 2
- W3School: JavaScript Tutorial
Creating Your Own Prototypical Inheritance
function Shape(color) {
this.color = color;
}
Shape.prototype.duplicate = function() {
console.log('duplicate');
};
const shape = new Shape('red');
Object.getPrototypeOf(shape);
{duplicate: ƒ, constructor: ƒ}
duplicate: ƒ ()
constructor: ƒ Shape(color)
[[Prototype]]: Object
Now we want to inherit the duplicate()
method by another type of objects, like Circle and Square.
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.draw = function() {
console.log('draw');
};
const circle1 = new Circle(1);
Note the Constructor here is ƒ Circle(radius)
.
Object.getPrototypeOf(circle1);
{draw: ƒ, constructor: ƒ}
draw: ƒ ()
constructor: ƒ Circle(radius)
[[Prototype]]: Object
Change the Prototype of the Constructor and create a new Object at this Base.
Circle.prototype = Object.create(Shape.prototype);
const circle2 = new Circle(2);
Note now the Constructor is ƒ Shape(color)
.
Object.getPrototypeOf(circle2);
Shape {}
[[Prototype]]: Object
duplicate: ƒ ()
constructor: ƒ Shape(color)
[[Prototype]]: Object
circle2.duplicate();
duplicate
Resetting the Constructor
As best practice when we reset the Prototype make sure you reset the constructor as well. Let's continue by the last example, where we saw when the Prototype was changed to Shape
also the Constructor was changed to Shape
, here is how to set it back to Circle
.
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
const circle3 = new Circle(3);
Note in the example below the Constructor is changed back to Circle
.
Object.getPrototypeOf(circle3);
Shape {constructor: ƒ}
constructor: ƒ Circle(radius)
[[Prototype]]: Object
duplicate: ƒ ()
constructor: ƒ Shape(color)
[[Prototype]]: Object
Calling the Super Constructor
function Shape(color) {
this.color = color;
}
Shape.prototype.duplicate = function() {
console.log('duplicate');
};
function Circle(radius, color) {
Shape.call(this, color);
this.radius = radius;
}
Circle.prototype.draw = function() {
console.log('draw');
};
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
const shape = new Shape('red');
const circle = new Circle(10, 'blue');
circle;
Circle {color: 'blue', radius: 10}
color: "blue"
radius: 10
[[Prototype]]: Shape
constructor: ƒ Circle(radius, color)
[[Prototype]]: Object
duplicate: ƒ ()
constructor: ƒ Shape(color)
[[Prototype]]: Object
Intermediate Function Inheritance
function Shape(color) {
this.color = color;
}
Shape.prototype.duplicate = function() {
console.log('duplicate');
};
function Square(size, color) {
Shape.call(this, color);
this.size = size;
}
Square.prototype = Object.create(Shape.prototype);
Square.prototype.constructor = Square;
const square = new Square(10, 'green');
square;
Square {color: 'green', size: 10}
color: "green"
size: 10
[[Prototype]]: Shape
constructor: ƒ Square(size, color)
[[Prototype]]: Object
duplicate: ƒ ()
constructor: ƒ Shape(color)
[[Prototype]]: Object
Now let's rewrite the above code and extract the two lines that changes the Prototype and the Constructor in a Function that we can reuse multiple times.
function extend(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
This extend()
function is what we call Intermediate function inheritance. Here is how to use it.
function Shape(color) {
this.color = color;
}
Shape.prototype.duplicate = function() { console.log('duplicate'); };
function Circle(radius) {
this.radius = radius;
}
extend(Circle, Shape);
function Square(size) {
this.size = size;
}
extend(Square, Shape);
const circle = new Circle(10);
const square = new Square(10);
circle;
Circle {radius: 10}
radius: 10
[[Prototype]]: Shape
constructor: ƒ Circle(radius)
[[Prototype]]: Object
duplicate: ƒ ()
constructor: ƒ Shape(color)
[[Prototype]]: Object
square;
Square {size: 10}
size: 10
[[Prototype]]: Shape
constructor: ƒ Square(size)
[[Prototype]]: Object
duplicate: ƒ ()
constructor: ƒ Shape(color)
[[Prototype]]: Object
Method Overriding
function extend(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
function Shape() { }
Shape.prototype.duplicate = function() {
console.log('duplicate');
};
function Circle() { }
extend(Circle, Shape);
const circle = new Circle();
circle;
Circle {}
[[Prototype]]: Shape
constructor: ƒ Circle()
[[Prototype]]: Object
duplicate: ƒ ()
constructor: ƒ Shape()
[[Prototype]]: Object
circle.duplicate();
duplicate
Now let's imagine the duplicate() method should behave differently at Circle objects. In order to override this method (or reimplementing a method on a child object) we need to put our declaration after the resetting the prototype by the extend()
function.
Circle.prototype.duplicate = function() {
console.log('duplicate circle');
};
circle.duplicate();
duplicate circle
Polymorphism
Polymorphism – many form. In the following example the polymorphism manifests in the duplicate()
method which is specific for each type of shape…
function extend(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
function Shape() { }
Shape.prototype.duplicate = function() {
console.log('duplicate');
};
function Circle() { }
extend(Circle, Shape);
Circle.prototype.duplicate = function() {
console.log('duplicate circle');
};
function Square() { }
extend(Square, Shape);
Square.prototype.duplicate = function() {
console.log('duplicate square');
};
Let's define an array of shape objects.
const shapes = [
new Circle(),
new Square()
];
Now we can iterate over this array by using for.. of..
loop in the following way.
for (let shape of shapes)
shape.duplicate();
duplicate circle
duplicate square
When to Use Inheritance
- Inheritance is great tool to solving the problem of code reuse. You have to be really careful about using it because it can make your source code complex and fragile, so don't use inheritance just for the sake of using it, especially in small projects. Keep it simple and stupid.
- Start with simple objects and then if you see number of these objects share similar features then perhaps you can encapsulate these features inside of an generic object and use inheritance.
- But remember inheritance is not the only solution that enabled code reuse. There is another technique called Composition (that can be used for the same purpose). Favor Composition over Inheritance.
- Avoid creating inheritance hierarchies, because they are very fragile. If you want to use inheritance keep it to one level – do not go than more one level of inheritance.