JavaScript Inheritance

Some classes can inherit from others. Inheritance allows you to reduce the amount of code in the derived classes. For example, take the following classes:

class Person{
    name;
    age;
    
    print(){
        console.log(`Name: ${this.name} Age: ${this.age}`);
    }
}
class Employee{
    name;
    age;
    company;
    print(){
        console.log(`Name: ${this.name} Age: ${this.age}`);
    }
    work(){
        console.log(`${this.name} works in ${this.company}`);
    }
}
 
const volume = new Person();
tomname = "Tom";
tom.age= 34;
const bob = new Employee();
bob.name="Bob";
bob.age = 36;
bob.company = "Google";
tom.print(); // Name: Tom Age: 34
bob print(); // Name: Bob Age: 36
bob work(); // Bob works in Google

Two classes are defined here – Person, which represents a person, and Employee, which represents an employee of the enterprise. Both classes work great, we can create objects of them, but we also see that the Employee class repeats the functionality of the Person class, since the employee is also a person, for which we can also define the name and age properties and the print method.

Inheritance allows one class to automatically get the functionality of other classes and thus reduce the amount of code. To inherit one class from another, use the extends keyword :

class Base{}
class Derived extends Base{}

After the name of the derived class, the extends keyword is placed, followed by the name of the class from which we want to inherit the functionality.

So, let’s change the Person and Employee classes by applying inheritance:

class Person{
    name;
    age;
    
    print(){
        console.log(`Name: ${this.name}  Age: ${this.age}`);
    }
}
class Employee extends Person{
    company;
    work(){
        console.log(`${this.name} works in ${this.company}`);
    }
}
 
const tom = new Person();
tom.name = "Tom"; 
tom.age= 34;
const bob = new Employee();
bob.name = "Bob"; 
bob.age = 36; 
bob.company = "Google";
tom.print();    // Name: Tom  Age: 34
bob.print();    // Name: Bob  Age: 36
bob.work();     // Bob works in Google

The Employee class now inherits from the Person class. In this respect, the Person class is also called the base or parent class, and the Employee class is also called the derived or descendant class. Since the Employee class inherits functionality from Person, we do not need to redefine the name, age properties, and the print method in it. As a result, the code of the Employee class turned out to be shorter, and the result of the program is the same.

Class inheritance with a constructor

Along with all the functionality, the derived class also inherits the constructor of the base class. For example, let’s define a constructor in the Person base class:

class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    print(){
        console.log(`Name: ${this.name}  Age: ${this.age}`);
    }
}
class Employee extends Person{
    company;
    work(){
        console.log(`${this.name} works in ${this.company}`);
    }
}
 
const tom = new Person("Tom", 34);
tom.print();    // Name: Tom  Age: 34

const sam = new Employee("Sam", 25);    // inherited constructor
sam.print();    // Name: Sam  Age: 25

In this case, the Person class defines a constructor with two parameters. In this case, the Employee class inherits it and uses it to create an Employee object.

Defining a constructor in a derived class and the super keyword.

A derived class can also define its own constructor. If a derived class defines a constructor, then the base class constructor must be called in it. To call a derived class to the functionality of the base class, including to call the constructor of the base class, the super keyword is used.

class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    print(){
        console.log(`Name: ${this.name}  Age: ${this.age}`);
    }
}
class Employee extends Person{
    
    constructor(name, age, company){
        super(name, age);
        this.company = company;
    }
    work(){
        console.log(`${this.name} works in ${this.company}`);
    }
}
 
const tom = new Person("Tom", 34);
tom.print();    // Name: Tom  Age: 34

const sam = new Employee("Sam", 25, "Google");
sam.print();    // Name: Sam  Age: 25
sam.work();     // Sam works in Google

The Employee class defines its constructor with three parameters, the first line of which is a call to the constructor of the Person base class:

super(name, age);

Since the constructor of the Person class has two parameters, two values ​​are passed to it accordingly. In this case, the base class constructor must be called before accessing the properties of the current object through this.

Overriding base class methods.

A derived class, as in the case of a constructor, can override the methods of the base class. So, in the example above, the print()Person class method prints out the person’s name and age. But what if we want the method to print() display the company for the worker as well? In this case, we can define our own method in the Employee class print():

class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    print(){
        console.log(`Name: ${this.name}  Age: ${this.age}`);
    }
}
class Employee extends Person{
    
    constructor(name, age, company){
        super(name, age);
        this.company = company;
    }
    print(){
        console.log(`Name: ${this.name}  Age: ${this.age}`);
        console.log(`Company: ${this.company}`);
    }
    work(){
        console.log(`${this.name} works in ${this.company}`);
    }
}
const sam = new Employee("Sam", 25, "Google");
sam.print();    // Name: Sam  Age: 25 // Company: Google

However, in the code above, we see that the first line of the method print()in the Employee class is essentially the same code as the method print()in the Person class. In this case, it’s just one line, but in another situation, the repeated code could be longer. And in order not to repeat ourselves, we can again simply refer to the implementation of the method of the print()parent class Person through super :

class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    print(){
        console.log(`Name: ${this.name}  Age: ${this.age}`);
    }
}
class Employee extends Person{
    
    constructor(name, age, company){
        super(name, age);
        this.company = company;
    }
    print(){
        super.print();
        console.log(`Company: ${this.company}`);
    }
    work(){
        console.log(`${this.name} works in ${this.company}`);
    }
}
const sam = new Employee("Sam", 25, "Google");
sam.print();    // Name: Sam  Age: 25 // Company: Google

That is, in this case, the call

super.print();

represents a call to the implementation of the method from the base class. Thus, with the help of this and super , we can delimit the call to the functionality of the current class or its base class.

Inheritance and private fields and methods

When inheriting, keep in mind that a derived class can access any functionality of the base class, except for private fields and methods. For example:

class Person{
    #name;
    constructor(name, age){
        this.#name = name;
        this.age = age;
    }
    print(){
        console.log(`Name: ${this.#name} Age: ${this.age}`);
    }
}
class Employee extends Person{
    
    constructor(name, age, company){
        super(name, age);
        this.company = company;
    }
    print(){
        superprint();
        console.log(`Company: ${this.company}`);
    }
    work(){
        console.log(`${this.#name} works in ${this.company}`); // ! Error - field #name not available from Employee
    }
}

In this case, the field #namein the Person class is defined as private, so it is only available within that class. An attempt to refer to this field in the derived class Employee will result in an error, regardless of whether it is accessed through this.#name or super.#name.