JavaScript Class inheritance

In this chapter, we will explore the class inheritance.

It is a way for one class to extend another one, and allows creating new functionality on top of the existing.

The “extend” Keyword

Imagine that you have a class Car:

class Car {
  constructor(name) {
    this.name = name;
    this.speed = 0;
  }
  drive(speed) {
    this.speed += speed;
    console.log(`${this.name} drives with speed ${this.speed}.`);
  }
  stop() {
    this.speed = 0;
    console.log(`${this.name} stands still.`);
  }
}
let car = new Car("My Car");

Now, let’s say that you intend to create a new class MyCar.

Since car are cars, the MyCar class can be based on Car having access to car methods so that they are able to do what generic cars can do.

The syntax for extending another class is as follows:

class Child extends Parent

The next step is creating class class MyCar that inherits from Car:

class Car {
  constructor(name) {
    this.name = name;
    this.speed = 0;
  }
  drive(speed) {
    this.speed += speed;
    console.log(`${this.name} drives with speed ${this.speed}.`);
  }
  stop() {
    this.speed = 0;
    console.log(`${this.name} stands still.`);
  }
}
let car = new Car("My Car");

class MyCar extends Car {
  parked() {
    console.log(`${this.name} is parked!`);
  }
}
let mycar = new MyCar("MyCar is Black");
mycar.drive(60); // MyCar drives with speed 60.
mycar.parked(); // MyCar is parked!

The object of Car class has access to both MyCar methods, like car.parked(), and Car methods, like mycar.drive().

The extends keyword uses prototype mechanics. It is setting MyCar.prototype. In case a method can’t be found in .prototype, JavaScript takes it from Car.prototype.

For example, for finding mycar.drive, the engine checks out:

  • The mycar object ( doesn’t have drive)
  • Its prototype (MyCar.prototype). It has parked but doesn’t have drive)
  • Its prototype, which is Car.prototype due to extends. Finally, it has the run method.

Overriding a Method

The next step is to override a method. All the methods not specified in class MyCar are taken directly “as is” from class Car.

But you need to specify your own method in MyCar such as stop() to use it instead.

Let’s have a look at the example:

class MyCar extends Car {
  stop() {
    // ...now this will be used for mycar.stop()
    // instead of stop() from class Car
  }
}

The aim is not replacing a parent method but building on top of it to tweak or extend its functionality. You do something in your method calling the parent method either before/after it or in the process.

There is a “super” keyword for that provided by classes:

  1. super.method(...) for calling a parent method
  2. super(...) for calling a parent constructor (only inside your constructor).

Overriding Constructor

It’s a little tricky to override a constructor.

In the previous examples, MyCar didn’t have its constructor.

In case a class extends another one and has no constructor, then an empty constructor is created, like this:

class MyCar extends Car {
  // generated for extending classes without own constructors
  constructor(...args) {
    super(...args);
  }
}

Basically, it calls the parent constructor, passing it all the arguments. It happens in case you don’t write your own constructor.

Now, let’s practice adding a custom constructor to MyCar. It specifies the engineForce besides name:

class Car {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class MyCar extends Car {
  constructor(name, engineForce) {
    this.speed = 0;
    this.name = name;
    this.engineForce = engineForce;
  }
  // ...
}

// Doesn't work!
let mycar = new MyCar("MyCar is Black", 60); // Error: this is not defined.

As you can see, an error occurred not allowing to create rabbits.

To understand the reason, you need to get into details.

In JavaScript, a constructor function of an inheriting class (known as “derived constructor”) and other functions are separated. A derived constructor has a unique internal property [[ConstructorKind]]:"derived". It’s a unique internal label.

The label’s behavior is affected by new.

  • Whenever an ordinary function is executed with new, it generates an empty object assigning it to this.
  • When a derived constructor runs, it doesn’t happen. It expects parent constructor to do that.

Hence, the derived constructor should call super to execute its parent constructor. In different circumstances, the object for this will not be generated. So, an error will occur.

It is necessary to call super() before using this for the MyCar constructor to work.

Here is an example:

class Car {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class MyCar extends Car {
  constructor(name, engineForce) {
    super(name);
    this.engineForce = engineForce;
  }
  // ...
}

// now fine
let mycar = new MyCar("MyCar is black", 60);
console.log(mycar.name); // MyCar is black
console.log(mycar.engineForce); // 60

[[HomeObject]]

In JavaScript, there is another unique internal property for functions. It’s [[HomeObject]].

Whenever a function is specified as an object method or a class, its [[HomeObject]] property is transformed into that object.

Then super uses it for resolving the parent prototype along with its methods.

Let’s see how it works:

let car = {
  name: "Car",
    drive() { // car.drive.[[HomeObject]] == car
    console.log(`${this.name} drives.`);
  }
};
let mycar = {
  __proto__: car,
  name: "MyCar",
  drive() { // mycar.drive.[[HomeObject]] == mycar
    super.drive();
  }
};
let powerfulEngine = {
  __proto__: mycar,
  name: "Machine with Powerful Engine ",
  drive() { // powerfulEngine.drive.[[HomeObject]] ==  powerfulEngine
    super.drive();
  }
};
// works correctly
powerfulEngine.drive(); //  drives.

It works thanks to the mechanics of [[HomeObject]]. The powerfulEngin e.drive method knows its [[HomeObject]] and takes the parent method from its prototype without using this.

Methods, Not Function Properties

[[HomeObject]] is defined for methods in classes, as well as in plain objects. But note that for objects you must specify methods exactly as method() and not like "method: function()". That is an essential difference for JavaScript.

A non-method syntax mentioned below is used for comparison. [[HomeObject]] is not applied, and the inheritance doesn’t operate:

let car = {
  drive: function () { // intentionally writing like this instead of drive() {...
    // ...
  }
};

let mycar = {
  __proto__: car,
  drive: function () {
  super.drive();
  }
};

mycar.drive(); // Error calling super (because there's no [[HomeObject]])



Do you find this helpful?

Related articles