Class inheritance

Class inheritance

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");

Here is the graphical visualization of the Car object and Car class:

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 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 use 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 are not “free”

In general, JavaScript functions are “free”, not bound to objects. So, it is possible to copy them between objects and call with another this.

But the existence of [[HomeObject]] breaks this principle, as methods remember their objects. It is impossible to change the [[HomeObject]]. The bond turns out to be forever.

The only place where [[HomeObject]] is used - is super. In case a method doesn’t use super, it can still be considered free. But everything may go wrong with super.

Below you can find a case of a wrong super result following copying:

let car = {

  sayHi() {

    console.log(`It’s a car`);

  }

};

// mycar inherits from car

let mycar = {

  __proto__: car,

  sayHi() {

  super.sayHi();

  }

};

let animal = {

  sayHi() {

    console.log("I'm a animal");

  }

};

// dog inherits from animal

let dog = {

  __proto__: animal,

  sayHi: mycar.sayHi // (*)

};

The dog.sayHi() call demonstrates “It’s a car”, which is wrong.

The reason is the following:

  • In the (*) line, the method dog.sayHi is copied from mycar.
  • Its [[HomeObject]] is mycar because it was generated in mycar. You can find no way of changing [[HomeObject]].
  • The dog.sayHi() code hassuper.sayHi() in it. It arises from the mycar taking the method from car.

The diagram below demonstrates all that:

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]])
Reactions

Post a Comment

0 Comments

close