Skip to content
On this page

Prototype Inheritance

In traditional class-based languages like Java and C++, inheritance essentially extends an existing class to create a new subclass. These languages strictly distinguish between classes and instances, making inheritance a type extension. However, JavaScript uses prototype inheritance, which does not have a direct concept of classes to extend.

Nevertheless, we can still achieve inheritance. Let's revisit the Student constructor function:

javascript
function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

Now, we want to extend Student to create PrimaryStudent. We can start by defining PrimaryStudent:

javascript
function PrimaryStudent(props) {
    // Call the Student constructor to bind this:
    Student.call(this, props);
    this.grade = props.grade || 1;
}

However, calling the Student constructor does not mean PrimaryStudent inherits from Student. The prototype chain of objects created with PrimaryStudent is:

new PrimaryStudent() --> PrimaryStudent.prototype --> Object.prototype --> null

We need to modify the prototype chain to:

new PrimaryStudent() --> PrimaryStudent.prototype --> Student.prototype --> Object.prototype --> null

This way, objects created based on PrimaryStudent can call methods defined in both PrimaryStudent.prototype and Student.prototype.

Using a simple assignment like:

javascript
PrimaryStudent.prototype = Student.prototype;

won't work! This would make PrimaryStudent and Student share the same prototype object, negating the purpose of defining PrimaryStudent.

We need an intermediary object to establish the correct prototype chain, with this intermediary object's prototype pointing to Student.prototype. We can achieve this using an empty function F:

javascript
// PrimaryStudent constructor:
function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// Empty function F:
function F() {}

// Set F's prototype to Student.prototype:
F.prototype = Student.prototype;

// Set PrimaryStudent's prototype to a new F instance:
PrimaryStudent.prototype = new F();

// Fix the constructor reference for PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;

// Define methods on the PrimaryStudent prototype (the new F object):
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};

// Create an instance of PrimaryStudent:
let xiaoming = new PrimaryStudent({
    name: 'Ming',
    grade: 2
});
xiaoming.name; // 'Ming'
xiaoming.grade; // 2

// Verify the prototype chain:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true

// Verify inheritance:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true

This establishes the new prototype chain as follows:

new PrimaryStudent() --> PrimaryStudent.prototype --> Student.prototype --> Object.prototype --> null

Note that function F is only used as a bridge. We only created one instance of new F() and did not alter the original prototype chain defined by Student.

To encapsulate the inheritance process in a reusable inherits() function and hide the definition of F, we can simplify our code:

javascript
function inherits(Child, Parent) {
    let F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

We can reuse this inherits() function:

javascript
function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// Implement the prototype inheritance chain:
inherits(PrimaryStudent, Student);

// Bind additional methods to the PrimaryStudent prototype:
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};

Summary

The implementation of prototype inheritance in JavaScript involves:

  1. Defining a new constructor function and using call() to invoke the constructor of the desired "parent" and bind this.
  2. Using an intermediary function F to establish the prototype chain, preferably through the encapsulated inherits() function.
  3. Continuing to define new methods on the prototype of the new constructor function.
Prototype Inheritance has loaded