📖 Inheritance in JavaScript

Inheritance is a fundamental concept in Object-Oriented Programming (OOP) that allows one class to inherit properties and methods from another. This promotes code reuse, reduces redundancy, and helps to create a more organized and scalable codebase. In JavaScript, inheritance is implemented using the extends and super keywords.

What is Inheritance?

Inheritance allows a class (known as a subclass or child class) to inherit the properties and methods of another class (known as a superclass or parent class). This means that the child class can use the functionality of the parent class without having to rewrite the code. Additionally, the child class can have its own properties and methods, or it can override the inherited ones.

Using the extends Keyword

The extends keyword is used to create a subclass that inherits from a parent class. When a class extends another class, it inherits all of the parent class's properties and methods.

class Person {
    constructor(name) {
        this.name = name;
    }

    greet() {
        return `Hello, my name is ${this.name}`;
    }
}

class Student extends Person {
    constructor(name, studentID) {
        super(name); // Calls the parent class's constructor
        this.studentID = studentID;
    }

    getDetails() {
        return `${this.name} (ID: ${this.studentID})`;
    }
}

const student = new Student('Jane Doe', 'S67890');
console.log(student.greet()); // Outputs: Hello, my name is Jane Doe
console.log(student.getDetails()); // Outputs: Jane Doe (ID: S67890)

In this example, the Student class extends the Person class. This means that Student inherits the greet method from Person. The Student class also adds a new method, getDetails, and a new property, studentID.

Using the super Keyword

The super keyword is used to call the constructor of the parent class. This is necessary when a subclass has its own constructor but still needs to initialize properties inherited from the parent class.

class Person {
    constructor(name) {
        this.name = name;
    }
}

class Student extends Person {
    constructor(name, studentID) {
        super(name); // Calls the constructor of Person
        this.studentID = studentID;
    }
}

const student = new Student('John Doe', 'S12345');
console.log(student.name); // Outputs: John Doe
console.log(student.studentID); // Outputs: S12345

Here, super(name) in the Student constructor calls the constructor of the Person class, passing the name parameter to it. This ensures that the name property is correctly initialized by the parent class before adding additional properties like studentID in the child class.

Overriding Methods in the Subclass

One of the key benefits of inheritance is that subclasses can override methods inherited from the parent class. This allows subclasses to provide specific implementations for methods that are more appropriate for their context.

class Person {
    constructor(name) {
        this.name = name;
    }

    greet() {
        return `Hello, my name is ${this.name}`;
    }
}

class Student extends Person {
    constructor(name, studentID) {
        super(name);
        this.studentID = studentID;
    }

    greet() {
        return `${super.greet()} and my student ID is ${this.studentID}`;
    }
}

const student = new Student('Jane Doe', 'S67890');
console.log(student.greet()); // Outputs: Hello, my name is Jane Doe and my student ID is S67890

In this example, the Student class overrides the greet method it inherited from the Person class. It still calls the parent class’s greet method using super.greet(), but it extends the functionality by including the student’s ID.

Common Pitfalls and Best Practices

While inheritance is powerful, it's important to be mindful of a few common pitfalls.

Overusing Inheritance
Inheritance should be used when there is a clear “is-a” relationship between the classes. For example, a Student is a Person. Avoid using inheritance simply to share code between classes that do not have a logical parent-child relationship.
Understanding Method Overriding
When overriding methods, always consider whether the original method from the parent class should be called using super. This ensures that the subclass doesn’t inadvertently lose important functionality from the parent class.
Using Composition Where Appropriate
Sometimes, composition (where one class contains an instance of another class) can be a better choice than inheritance, especially when the relationship is more of a “has-a” rather than “is-a” relationship.

Summary

Inheritance
Allows a class to inherit properties and methods from another class, promoting code reuse.
extends Keyword
Used to create a subclass that inherits from a parent class.
super Keyword
Used to call the constructor and methods of the parent class within a subclass.
Method Overriding
Allows a subclass to provide a specific implementation of a method that is inherited from the parent class.

Putting It Into Action

Inheritance in a School System
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>School System</title>
</head>
<body>

    <h1>School System</h1>

    <div id="output"></div>

    <script>
        // Base class Person
        class Person {
            constructor(name) {
                this.name = name;
            }

            greet() {
                return `Hello, my name is ${this.name}`;
            }

            outputToDOM(message) {
                const outputDiv = document.getElementById('output');
                const p = document.createElement('p');
                p.textContent = message;
                outputDiv.appendChild(p);
            }
        }

        // Student class extending Person
        class Student extends Person {
            constructor(name, studentID) {
                super(name); // Calls the constructor of Person
                this.studentID = studentID;
            }

            getDetails() {
                return `${this.name} (ID: ${this.studentID})`;
            }
        }

        const student = new Student('John Doe', 'S12345');
        // console output
        console.log(student.greet()); // Outputs: Hello, my name is John Doe
        console.log(student.getDetails()); // Outputs: John Doe (ID: S12345)

        // DOM output
        student.outputToDOM(student.greet());
        student.outputToDOM(student.getDetails());
    </script>

</body>
</html>

In this example, the Student class inherits from the Person class. The greet method is inherited from Person, while the Student class adds a new method getDetails to provide more specific information about the student.

Challenge

Now that you've seen how inheritance works in a simple school system, extend this example with the following challenge.

  • Create a new class called Teacher that extends the Person class. Add a subject property and a method to return the teacher's details.
  • Override the greet method in the Teacher class to include the subject they teach.

In order to check your learning, you should attempt to create a solution before revealing the provided solution below.

// JavaScript Code for the Challenge Solution

class Person {
    constructor(name) {
        this.name = name;
    }

    greet() {
        return `Hello, my name is ${this.name}`;
    }
}

class Teacher extends Person {
    constructor(name, subject) {
        super(name); // Calls the constructor of Person
        this.subject = subject;
    }

    greet() {
        return `${super.greet()} and I teach ${this.subject}`;
    }

    getDetails() {
        return `${this.name} teaches ${this.subject}`;
    }
}

const teacher = new Teacher('Mr. Smith', 'Mathematics');
console.log(teacher.greet()); // Outputs: Hello, my name is Mr. Smith and I teach Mathematics
console.log(teacher.getDetails()); // Outputs: Mr. Smith teaches Mathematics
                

References