Implementation of Inheritance Mechanism in ECMAScript

Implementation of inheritance mechanism

To implement the inheritance mechanism in ECMAScript, you can start with the base class you want to inherit from. All classes defined by developers can be used as base classes. For security reasons, local classes and host classes cannot be used as base classes, which can prevent public access to compiled browser-level code, as this code can be used for malicious attacks.

After selecting the base class, you can create its subclass. Whether to use the base class is entirely up to you. Sometimes, you may want to create a base class that cannot be used directly, which is only used to provide general functions for subclasses. In this case, the base class is considered an abstract class.

Although ECMAScript does not strictly define abstract classes like other languages, it does indeed create some classes that are not allowed to be used. Usually, we call such classes abstract classes.

The subclass created will inherit all properties and methods of the superclass, including the implementation of constructors and methods. Remember, all properties and methods are public, so the subclass can directly access these methods. The subclass can also add new properties and methods that are not present in the superclass, or override the properties and methods of the superclass.

The ways of inheritance

Like other features, there are not only one way to implement inheritance in ECMAScript. This is because the inheritance mechanism in JavaScript is not explicitly defined, but rather achieved through imitation. This means that all the details of inheritance are not completely handled by the interpreter. As a developer, you have the right to decide on the most suitable inheritance method.

Below, I will introduce several specific inheritance methods.

Object masquerading

When the original ECMAScript was conceptualized, there was no intention to design object masquerading (object masquerading). It developed after developers began to understand how functions work, especially how to use the this keyword in the function environment.

The principle is as follows: the constructor uses the this keyword to assign values to all properties and methods (i.e., using the class declaration constructor method). Since the constructor is just a function, it can make the ClassA constructor a method of ClassB, and then call it. ClassB will receive the properties and methods defined in the ClassA constructor. For example, define ClassA and ClassB as follows:

function ClassA(sColor) {
    this.color = sColor;
    this.sayColor = function () {
        alert(this.color);
    };
}
function ClassB(sColor) {
}

Remember? The keyword this refers to the object currently being created by the constructor. However, in this method, this points to the object owned by the method. This principle is to establish the inheritance mechanism by treating ClassA as a regular function, rather than as a constructor. The inheritance mechanism can be implemented using the constructor ClassB as follows:

function ClassB(sColor) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
}

In this code, the method newMethod is assigned to ClassA (please remember that the function name is just a pointer to it). Then the method is called, passing it the parameter sColor of the ClassB constructor. The last line of code deletes the reference to ClassA, so it can no longer be called in the future.

All new properties and methods must be defined after the code line that deletes the new method. Otherwise, it may overwrite the related properties and methods of the superclass:

function ClassB(sColor, sName) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

To prove that the previous code is effective, you can run the following example:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//Output "blue"
objB.sayColor();	//Output "red"
objB.sayName();		// Output "John"

Object masquerading can implement multiple inheritance

Interestingly, object masquerading can support multiple inheritance. That is to say, a class can inherit from multiple superclasses. The multiple inheritance mechanism represented by UML is shown in the figure below:

Inheritance Mechanism UML Diagram Example

For example, if there are two classes ClassX and ClassY, and ClassZ wants to inherit from both of them, the following code can be used:

function ClassZ() {
    this.newMethod = ClassX;
    this.newMethod();
    delete this.newMethod;
    this.newMethod = ClassY;
    this.newMethod();
    delete this.newMethod;
}

Try It Yourself (TIY)

There is a drawback, if there are two classes ClassX and ClassY that have the same-named properties or methods, ClassY has higher priority because it inherits from the class at the end. Apart from this minor problem, it is easy to implement a multiple inheritance mechanism with object spoofing.

Due to the popularity of this inheritance method, the third edition of ECMAScript added two methods to the Function object, namely call() and apply().

The call() method

The call() method is the most similar to the classic object spoofing method. Its first parameter is used as the object for 'this'. All other parameters are passed directly to the function itself. For example:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.call(obj, "The color is ", "a very nice color indeed.");

In this example, the function sayColor() is defined outside the object, even though it does not belong to any object, it can still refer to the keyword 'this'. The color attribute of the object obj is equal to 'blue'. When calling the call() method, the first parameter is obj, indicating that the value of the 'this' keyword in the sayColor() function should be assigned to obj. The second and third parameters are strings. They match the parameters sPrefix and sSuffix in the sayColor() function. The final message "The color is blue, a very nice color indeed." will be displayed.

To use this method with the object spoofing method of the inheritance mechanism, simply replace the assignment, call, and deletion code in the first three lines:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.call(this, sColor);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

Try It Yourself (TIY)

Here, we need to make the keyword 'this' in ClassA equal to the newly created ClassB object, so 'this' is the first parameter. The second parameter sColor is a unique parameter for both classes.

The apply() method

The apply() method has two parameters, used as the object for 'this' and an array of arguments to be passed to the function. For example:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));

This example is the same as the previous one, but now the apply() method is called. When calling the apply() method, the first parameter is still obj, indicating that the value of the this keyword in the sayColor() function should be obj. The second parameter is an array consisting of two strings that match the parameters sPrefix and sSuffix in the sayColor() function. The generated message is still "The color is blue, a very nice color indeed.", which will be displayed.

This method is also used to replace the assignment, call, and deletion of the new method in the first three lines:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, new Array(sColor));
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

Similarly, the first parameter is still this, and the second parameter is an array with only one value, color. The entire arguments object of ClassB can be passed as the second parameter to the apply() method:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, arguments);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

Try It Yourself (TIY)

Of course, parameters can only be passed as an object if the parameter order in the superclass is exactly the same as in the subclass. If not, a separate array must be created, placing parameters in the correct order. In addition, the call() method can also be used.

Prototype Chain (prototype chaining)

This form of inheritance is originally used for the prototype chain in ECMAScript. The previous chapter introduced the prototype method of defining a class. The prototype chain extends this method and implements the inheritance mechanism in an interesting way.

As learned in the previous chapter, the prototype object is a template, and all objects to be instantiated are based on this template. In summary, any property or method of the prototype object is passed to all instances of that class. The prototype chain utilizes this feature to implement the inheritance mechanism.

If the class is redefined using the prototype method in the previous example, it will take the following form:

function ClassA() {
}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB() {
}
ClassB.prototype = new ClassA();

The magic of the prototype method lies in the highlighted blue code line. Here, the prototype property of ClassB is set to an instance of ClassA. This is quite interesting because we want all properties and methods of ClassA, but we do not want to individually assign them to the prototype property of ClassB. Is there a better way than assigning an instance of ClassA to the prototype property?

Note:Call the constructor of ClassA without passing any parameters. This is the standard practice in the prototype chain. Make sure the constructor has no parameters.

Similar to object pseudo-casting, all properties and methods of the subclass must appear after the prototype property is assigned, because all methods assigned before it will be deleted. Why? Because the prototype property is replaced with a new object, and the original object that added the methods will be destroyed. Therefore, the code to add the name property and sayName() method to the ClassB class is as follows:

function ClassB() {
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
    alert(this.name);
};

You can test this code by running the following example:

var objA = new ClassA();
var objB = new ClassB();
objA.color = "blue";
objB.color = "red";
objB.name = "John";
objA.sayColor();
objB.sayColor();
objB.sayName();

Try It Yourself (TIY)

In addition, the instanceof operator behaves uniquely in the prototype chain. For all instances of ClassB, instanceof returns true for both ClassA and ClassB. For example:

var objB = new ClassB();
alert(objB instanceof ClassA); // Output "true"
alert(objB instanceof ClassB); // Output "true"

In the weakly typed world of ECMAScript, this is an extremely useful tool, but it cannot be used when object pseudo-casting.

The disadvantage of the prototype chain is that it does not support multiple inheritance. Remember, the prototype chain will overwrite the class's prototype property with another type of object.

Mixed mode

This inheritance method defines a class using a constructor, not using any prototype. The main problem with object pseudo-casting is that it must use the constructor method, which is not the best choice. However, if the prototype chain is used, it is not possible to use a constructor with parameters. How should developers choose? The answer is simple: use both.

In the previous chapter, we have explained the best way to create a class is to define properties with a constructor and define methods with a prototype. This method is also applicable to the inheritance mechanism, using object to inherit the properties of the constructor, and using the prototype chain to inherit the methods of the prototype object. Rewrite the previous example using these two methods, as follows:

function ClassA(sColor) {
    this.color = sColor;
}
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB(sColor, sName) {
    ClassA.call(this, sColor);
    this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
    alert(this.name);
};

In this example, the inheritance mechanism is implemented by two highlighted blue lines of code. In the first highlighted line of code, the object is used to inherit the sColor property of the ClassA class in the ClassB constructor. In the second highlighted line of code, the methods of the ClassA class are inherited through the prototype chain. Since this mixed method uses the prototype chain, the instanceof operator can still work correctly.

The following example tests this code:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//Output "blue"
objB.sayColor();	//Output "red"
objB.sayName();	//Output "John"

Try It Yourself (TIY)