Prototypal inheritance in Javascript

13-01-2018 • Javascript

Inheritance in Javascript can be quite confusing. Javascript uses prototypal inheritance and doesn't have a concept of classes like a lot of other programming languages do. Although es6 has a Class operator, this is just syntactical sugar. In this post we'll explore how inheritance in Javascript really works.

Constructor functions

In Javascript we can create "constructor functions", which are normal function that will be used as the constructor for our new instances. We can then instantiate new objects by calling the function with the new operator.

function Car(make, color) {
  this.make = make
  this.color = color
  this.move = function(){
    console.log('the ' + this.color + ' car moved')
  }
}

var myCar = new Car('volvo', 'white')
var yourCar = new Car('audi', 'black')

myCar.move()               //the white car moved
console.log(myCar.color)   //white
console.log(yourCar.color) //black

If we define properties and methods inside of the constructor function, like in the example above, these will be copied (duplicated) on the newly created objects.

The prototype

Every object in Javascript has a prototype, exposed by the __proto__ property and in es6 the Object.getPrototypeOf() and Object.setPrototypeOf() methods.

You should not use the __proto__ property, its exact behavior has only been standardized in es6. Also changing an object's prototype directly is a very slow operation which should be avoided. If you really need to access an objects prototype in production, use the es6 methods where possible.

When we try to access a property or method on an object, Javascript first tries to find that property or method on the object itself. When the property or method is not found, Javascript will look for it on the object's prototype, then on the prototype's prototype and so on until the property or method is found or the end of the chain is reached. This is often referred to as the "prototype chain".

For example a function's prototype (__proto__) references Function.prototype which in turn has a prototype (__proto__) which references Object.prototype, because of this any function has access to the properties and methods of Object.

function fn() { }
console.log(fn.__proto__ === Function.prototype)               //true
console.log(Function.prototype.__proto__ === Object.prototype) //true

Instead of defining properties and methods inside the constructor function, we can also define them on the constructor function's prototype property. Any instance created with the new operator will automatically have it's prototype reference the constructor function's prototype property.

The prototype property of a function is automatically created and is simply an empty object. For this reason a default prototype property will always have a prototype that references Object.prototype.

Example:

function Car(make, color) {
  this.make = make
  this.color = color
}
Car.prototype.move = function () {
  console.log('the ' + this.color + ' car moved')
}

var myCar = new Car('volvo', 'black')
console.log(myCar.color) //black
myCar.move()             //the black car moved

console.log(myCar.__proto__ === Car.prototype) //true

In the above code the move method is defined on Car's prototype property. When we create myCar by calling new Car('volvo', 'black'), the myCar instance will be created with a prototype that references Car's prototype property.

myCar's prototype chain looks like this:

myCar.__proto__ = Car.prototype

Car.prototype = {
  move: function () {
    console.log('the ' + this.color + ' car moved')
  },
  __proto__: Object.prototype
}

When we call myCar.move() Javascript tries to find the move method on the myCar object and doesn't find it, Javascript will then look at myCar's prototype, which references Car.prototype, and finds the method there.

myCar not only has access to all properties and methods of Car.prototype, but because Javascript will also look at the prototype's prototype, which in this case references Object.prototype, myCar also has access to the properties and methods of Object.prototype. Using this chaining mechanism we can inherited from multiple prototypes.

Here is a more complex example:

function Car(color) {
  this.color = color
}
Car.prototype.honk = function () {
  console.log('hoooooonk!!!')
}

function FourByFour(color) {
  this.color = color
}
FourByFour.prototype = Object.create(Car.prototype)
FourByFour.prototype.goOffroad = function () {
  console.log('the ' + this.color + ' four-by-four goes offroad ')
}

var myCar = new FourByFour('black')
myCar.honk()      //hoooooonk!!!
myCar.goOffroad() //the black four-by-four goes offroad

console.log(myCar.__proto__ === FourByFour.prototype)         //true
console.log(FourByFour.prototype.__proto__ === Car.prototype) //true
console.log(Car.prototype.__proto__ === Object.prototype)     //true

Here we use the Object.create() method to create a new empty object with a prototype that references Car.prototype. We use this new object to override FourByFour's default prototype property.

The Object.create(prototype[, properties]) method creates a new object with the specified prototype and properties.

myCar's prototype chain now looks like this:

myCar.__proto__ = FourByFour.prototype

FourByFour.prototype = {
  goOffroad: function () {
    console.log('the ' + this.color + ' four-by-four goes offroad ')
  }
  __proto__: Car.prototype
}

Car.prototype = {
  honk: function () {
    console.log('hoooooonk!!!')
  },
  __proto__: Object.prototype
}

myCar now has access to all properties and methods of FourByFour.prototype, Car.prototype and Object.prototype.

Properties and functions defined on the prototype have the added advantage of not being duplicated when instantiating new objects, which lowers memory usage.
Also they can act as default values which can be overwritten on the object, when the property is deleted it will revert to its default value defined on the prototype.

The constructor property

The last thing we need to do, is to fix the constructor property. Although in many cases you won't need the constructor property, it is good practice to do this.

prototype.constructor should hold a reference to the constructor function that created the instance. The keyword here is "should".

Example:

function Car() {}

function FourByFour() {}
FourByFour.prototype = Object.create(Car.prototype)

console.log(Car.prototype.constructor)        //function Car() {}
console.log(FourByFour.prototype.constructor) //function Car() {}

Here the Car.prototype.constructor property correctly references the Car constructor function, but the FourByFour.prototype.constructor property also references Car constructor function which, of course, is wrong. To fix this we simply set the FourByFour.prototype.constructor property to reference the FourByFour constructor function, like so:

function Car() {}

function FourByFour() {}
FourByFour.prototype = Object.create(Car.prototype)
FourByFour.prototype.constructor = FourByFour

console.log(Car.prototype.constructor) //function Car() {}

console.log(
  FourByFour.prototype.constructor)    //function FourByFour() {}

FourByFour.prototype.constructor now correctly references the FourByFour constructor function.

Calling the super constructor and methods

Many classed based programming languages allow you to call constructor and methods of the super (parent) class. In javascript we can do this by using the call() method.

The Function.prototype.call(scope[, arg1, arg2, ...]) method calls a function with a given scope (this value) and provided arguments.

With the call() method we can borrow methods from another object and use them inside of another object using that objects scope. Example:

function Car(make, color) {
  this.make = make
  this.color = color
  this.motor = 'electric'
}
Car.prototype.honk = function () {
  console.log(
    'The '+this.motor+' '+this.color+' '+this.make+' honked')
}

function FourByFour(make, color) {
  Car.call(this, make, color)
}
FourByFour.prototype = Object.create(Car.prototype)

var myCar = new FourByFour('Tesla', 'black')
myCar.honk() //The electric black Tesla honked

In the example above FourByFour borrows the Car's constructor function and passes itself (this) as the scope. Because this now references FourByFour the make, color and motor are now created on the instances created by the FourByFour constructor function.

The same thing can be done with the methods defined on the prototype:

function Car() {
}
Car.prototype.set = function (make, color) {
  this.make = make
  this.color = color
}

function FourByFour(make, color) {
}
FourByFour.prototype = Object.create(Car.prototype)
FourByFour.prototype.set = function (make, color) {
  Car.prototype.set.call(this, make, color)
}

var myCar = new FourByFour()
myCar.set('Volvo', 'red')

One final example

In this final example we will put everything together:

function Car(make, color) {
  this.make = make
  this.color = color
}
Car.prototype.honk = function () {
  console.log('hoooooonk!!!')
}

function FourByFour(make, color) {
  Car.call(this, make, color)
}
FourByFour.prototype = Object.create(Car.prototype)
FourByFour.prototype.constructor = FourByFour
FourByFour.prototype.goOffroad = function () {
  console.log(
    'the '+this.color+' '+this.make+' four-by-four goes offroad')
}