Prototypal 'inheritance'
Kishor Neupane
Posted on January 15, 2021
Inheritance in general, implies 'copying' certain behaviors from a class to create new objects. For every single object(instance) that is created from a certain class, the attributes and behaviors are 'copied'. Class-based languages like C++, Java, etc implement this mechanism of object creation where each object is a separate 'copy' of a class. In languages like Javascript, there is no concept of class! Still, Javascript is object oriented. Object creation and manipulation is pretty simple in Javascript.
However, implementing concepts like inheritance is not so straightforward. Javascript is a Prototype-based language, meaning it does not have classes, it only deals with objects. So, the way inheritance works in Javascript is by the use of links or references to the other objects to use or share behaviors.
Prototypes
Every object has a hidden property [[Prototype]] that references some object. Double braces in [[Prototype]] means its internal/hidden. Internal properties are not part of ECMAScript(a standard for scripting languages like Javascript), which means we cannot use it as an identifier in our code. But, in this case, we can access the hidden property [[Prototype]] with the help of _proto_ property(dunder proto) which is available to every object in Javascript. _proto_ is just a getter/setter for our hidden property [[Prototype]].
var person = {
name: 'Keanu',
age: 35
}
console.log(person);
console.log(person.hasOwnProperty('name'));
If you see the console, you should see a person object with a proto property along with the name and age properties. The code person.hasOwnProperty('name') works and actually returns true even though we have not defined 'hasOwnProperty' method on our person object. This is due to the _proto_ property which points to 'Object.prototype'. 'prototype' in 'Object.prototype' is just a property of 'Object' constructor function that happens to point to some other object. Remember a function is an object too!
Every function in Javascript has a 'prototype' property(when 'new' is used to create objects). Every object in Javascript has a _proto_ property.
Basically in Javascript, almost all objects are 'instances' of 'Object' which means all objects inherit from Object.prototype. If a certain property is not found in an object, Javascript tries to look that property in the object referred by _proto. Since 'Object.prototype' is pointed by __proto_ and 'Object.prototype' has a method called'hasOwnProperty', person.hasOwnProperty('name') works fine.By default, every object in Javascript has a _proto_ which points to 'Object.prototype' which we can see from our example.
This is the core concept of prototypes. If you want to inherit stuff, you need _proto_ to point to the object whose properties you want. The object
pointed by _proto_ is called a prototype object or simply a prototype. That's it.
Inheritance in action
var vehicle = {
color: 'red',
run: function(){
console.log('Vehicle running.');
}
};
var truck = {
carryStuff: function(){
console.log('Truck is carrying stuff.');
}
};
truck.__proto__ = vehicle; // set [[Prototype]]
truck.run(); // truck runs too!
console.log(truck.__proto__ === vehicle); // obviously!
console.log(truck.__proto__.__proto__ === Object.prototype); // true
So, truck.run works because we have pointed _proto_ to vehicle object. Since, run() is not available in truck, javascript looks up in the 'prototype chain' untill it reaches the end, which is our good old 'Object'.
We used object literals in above example but now lets use constructor functions to simulate inheritance for reusability.
// a 'constructor' function
function Vehicle(color, price){
this.color = color;
this.price = price;
this.run = function(){
console.log('Vehicle is running!');
}
}
Since we want 'run' method to be availabe for child 'class', we put it in a prototype property so that it can be inherited.
function Vehicle(color, price){
this.color = color;
this.price = price;
}
Vehicle.prototype.run = function(){
console.log('Vehicle is running!');
}
// child class
function Car(color, price, fuelType){
this.color = color;
this.price = price;
this.fuelType = fuelType;
}
Car.prototype = Object.create(Vehicle.prototype);
Object.create() creates a new object with [[Prototype]] set to its first parameter, in this case 'Vehicle.prototype'.
var ferrari = new Car('red', 45465413, 'petrol');
ferrari.run(); //works
console.log(ferrari.__proto__ === Car.prototype); // true
console.log(ferrari.color);
console.log(ferrari.price);
We can improve our car class. We are setting color and price in Car which is redundant since Vehicle already does that and we are inheriting from it. So, we can call Vehicle which sets color and price properties but changing 'this' of Vehicle object to 'this' of Car object.
function Car(color, price, fuelType){
Vehicle.call(this, color, price); // calling Vehicle in Car's context
this.fuelType = fuelType;
}
Car.prototype = Object.create(Vehicle.prototype);
var ferrari = new Car('red', 45465413, 'petrol');
ferrari.run(); //works
console.log(ferrari.__proto__ === Car.prototype); // true
console.log(ferrari.color);
console.log(ferrari.price);
Also, every prototype object has 'constructor' property which points to the constructor function back. This property points to the function it was created from. In our case for 'ferrari' object, its constructor should be 'Car'.
console.log(ferrari.__proto__.constructor); // Oops... Vehicle!
So, we should set the constructor property as well.
Car.prototype.constructor = Car;
console.log(ferrari.__proto__.constructor); // Car
So, overall our pattern for inheritance looks something like this in ES5.
// parent class
function Vehicle(color, price){
this.color = color;
this.price = price;
}
Vehicle.prototype.run = function(){
console.log('Vehicle is running!');
}
Vehicle.prototype.honk = function(){
console.log('Peep peep!');
}
// child class
function Car(color, price, fuelType){
Vehicle.call(this, color, price); // its like calling super()
this.fuelType = fuelType;
}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
In ES6 inheritance is straightforward due to some syntactic sugar.
class Vehicle{
constructor(color, price){
this.color = color;
this.price = price;
}
run(){
console.log('Vehicle is running!');
}
honk(){
console.log('Peep peep!');
}
}
class Car extends Vehicle{
constructor(color, price, fuelType){
super(color, price);
this.fuelType = fuelType;
}
someMethod(){
console.log('Just some method of Car');
}
}
var ferrari = new Car('red', 45465413, 'petrol');
ferrari.run(); // Vehicle is running!
console.log(ferrari.__proto__ === Car.prototype); // true
console.log(ferrari.color); // red
console.log(ferrari.price); // 45465413
In this way, with the help of prototypes, we can 'inherit' features for reusability in Javascript. Prototypal inheritance is useful in situations when we want add new properties after the creation of objects unlike classical inheritance. Also, developers can quickly customize objects without thinking much about the structure of the class itself. However, type checking is difficult in prototype based languages and object creation is a bit slower too. The concept of prototypes were built for simplicity and Javascript being a dynamic language, it is a perfect match.
Posted on January 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024