Many object-oriented languages support two types of inheritance: interface inheritance and implementation inheritance. Interface inheritance inherits only method signatures, whereas implementation inheritance inherits the actual methods.

Because functions have no signatures, interface inheritance cannot be implemented in ECMAScript, only implementation inheritance is supported, and implementation inheritance is mainly implemented by relying on prototype chains.

The search rules for the prototype chain are: When looking for a property/method in an object, if the object itself does not have the property/method, then the object will look in its built-in __proto__ property, that is, the prototype object of its constructor. If the constructor prototype does not have the property, then the __proto__ property in the constructor prototype will be looked up. Look up one layer at a time.

Six implementations of ES5 inheritance

  1. Prototype chain
  2. Borrowing constructor
  3. Combination of inheritance
  4. Primary inheritance
  5. Parasitic inheritance
  6. Combinatorial parasitic inheritance

Let’s look at the implementation of these six inheritance types in detail.

1. The prototype chain

Its basic idea:

Using stereotype chains, one reference type inherits the properties and methods of another reference type.

Constructor, prototype, and instance relationships:

  • All constructors are functions, and all functions have the Prototype attribute
  • Each stereotype object contains a pointer constructor to the constructor
  • All reference types have a built-in __proto__ attribute pointing to the prototype object of their constructor

⚠️ please keep in mind the above three! Keep these three in mind! Keep these three in mind!

The basic pattern of prototype chain inheritance is as follows:

function One() {
    this.numOne = 1;
}
One.prototype.getNumOne = function() {
    return this.numOne;
}

function Two() {
    this.numTwo = 2;
}
// Two inherits One
Prototype is an instance of One, and its internal pointer __proto__ points to the prototype object of One
Two.prototype = new One();
Two.prototype.getNumTwo = function() {
    return this.numTwo;
}

const two = new Two();
two.getNumOne(); / / 1
Copy the code

Examples and the relationship between constructors and stereotypes are shown below:

Matters needing attention:

  1. All reference types inherit Object by default, and this inheritance is also implemented through the stereotype chain, which points to NULL, which is the end of the stereotype chain
  2. When implementing inheritance using stereotype chains, you cannot use the literal prototype creation method because this overrides the stereotype chain.
  3. The code that adds methods to the stereotype must come after the statement that replaces the stereotype.

Problems with prototype chain inheritance:

  1. Stereotype properties are shared by all instances when they are values that contain reference types. So, generic attributes are defined in the constructor, not in the stereotype chain.
  2. There is no way to pass arguments to the constructor of a parent class without affecting all object instances. In view of these two points, prototype chains are rarely used in practice alone.

2. Borrow constructors

The basic idea is:

The parent constructor is called inside the subtype constructor. Execute the constructor on the newly created object by using the apply() and call() methods.

Basic mode:

function One(num) {
    this.numOne = num;
    this.numList = [1.2];
}
function Two() {
    // Inherit from One
    One.call(this.3);
    this.numTwo = 2;
}
const two = new Two();
two.numList.push(3);
console.log(two.numTwo); / / 2
console.log(two.numOne); / / 3
console.log(two.numList); / / [1, 2, 3]

const two2 = new Two();
two2.numList.push(4); 
console.log(two2.numList); / / [1, 2, 4]
Copy the code

Problems with using constructors to implement inheritance:

Methods are defined in constructors and cannot be reused. Methods defined in the stereotype of the parent type are invisible to the child type, and all types can only use the constructor pattern.

As a result, the technique of borrowing constructors is rarely used in isolation.

3. Combinatorial inheritance

Combinatorial inheritance is an inheritance pattern that combines the prototype chain and the constructor to give full play to the advantages of the two.

The basic idea is:

Inheritance of stereotype properties and methods is implemented using stereotype chains, while inheritance of instance properties is implemented through constructors.

The basic mode is as follows:

function One(num) {
    this.numOne = num;
    this.numList = [1.2];
}
One.prototype.getNumOne = function() {
    return this.numOne;
};
function Two(num1, num2) {
    // When One() is called the second time, the instance object inherits Two attributes of One, masking Two attributes of the same name in prototype two-.prototype
    One.call(this, num1);
    this.numTwo = num2;
}
// The first call to One() inherits One's prototype method. Prototype gets Two properties of One
Two.prototype = new One();
Two.prototype.constructor = Two;
Two.prototype.getNumTwo = function() {
    return this.numTwo;
}
var two1 = new Two(3.5);
two1.numList.push(6); 
console.log(two1.numList); / / [1, 2, 6]
two1.getNumOne(); / / 3
two1.getNumTwo(); / / 5

var two2 = new Two(4.6);
two2.numList.push(7);
console.log(two2.numList); / / [1, 2, 7)
two2.getNumOne(); / / 4
two2.getNumTwo(); / / 6
Copy the code

Characteristics of combinatorial inheritance

Composite inheritance avoids the pitfalls of stereotype chains and constructors and combines the advantages of both, making it the most common inheritance pattern in javascript. The problem with this approach, however, is that in any case, the parent type’s constructor is called twice: once while the subtype prototype is being created, and once inside the constructor.

4. Original type inheritance

The basic idea is:

You must have one object as the basis for another, and then pass that object to the object function, which then modifies the resulting object as required. The object function is as follows:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
Copy the code

As you can see, the returned object is an instance of the constructor F, whose built-in object __proto__ points to the prototype of F, and f.prototype = o; The __proto__ of the returned Object refers to the Object o passed in. Object.create()

Object.cerate() this method behaves the same as the Object function. This method takes two parameters: an object to be used as a prototype for the new object and an object to define additional properties for the new object. The second parameter is optional.

Basic mode:

var person = {
    name: "MMZ".course: ["Math"."English"."History"]};var person1 = Object.create(person);
person1.course.push('Music');
console.log(person1.name); // MMZ
console.log(person1.course); // ["Math", "English", "History", "Music"]

var person2 = Object.create(person, {
    name: {
        value: "MinMin"}}); person2.course.push('PE');
console.log(person2.name); // MinMin
// Attributes that refer to values of type are shared
console.log(person2.course); // ["Math", "English", "History", "Music", "PE"]

console.log(person.course); // ["Math", "English", "History", "Music", "PE"]
Copy the code

Characteristics of original type inheritance:

  1. Object.create() has compatibility issues and is not supported by IE9.
  2. This is useful when there is no need to create a constructor and you just want one object to be similar to another.
  3. Attributes that contain reference-type values are always shared.

Parasitic inheritance

This approach is closely related to the original type inheritance. The idea of inheritance is similar to the parasitic constructor and factory pattern.

The basic idea is as follows:

Create a function that simply encapsulates the inheritance process, enhances the object in some way internally, and then returns the object.

Example:

function createObject(origin) {
    const clone = Object.create(origin);
    clone.sayHi = function() {
        return 'Hi';
    }
    return clone;
}

const person = {
    name: "MMZ".course: ["Math"."English"."History"]};const person1 = createObject(person);
person1.sayHi(); // Hi
Copy the code

The problem of parasitic inheritance

Using parasitic functions to add functions to objects does not make function reuse possible.

6. Combinatorial parasitic inheritance

The basic idea is as follows:

Use parasitic inheritance to inherit the stereotype of the parent type and then assign the result to the stereotype of the child type.

Basic mode:

// inheritPrototype takes two parameters: the constructor for the subtype and the constructor for the parent type
function inheritPrototype(child, parent) {
    // Create a copy of the parent type stereotype
    const prototype = Object.create(parent.prototype); 
    // Add the constructor attribute to the created copy to make up for the loss of the default constructor attribute by rewriting the stereotype
    prototype.constructor = child;
    // Assign the created copy to the prototype of the subtype
    child.prototype = prototype;
}

function One(num1) {
    this.numOne = num1;
    this.numList = [1.2];
}
One.prototype.getNumOne = function() {
    return this.numOne;
}

function Two(num1, num2) {
    One.call(this, num1)
    this.numTwo = num2;
}
inheritPrototype(Two, One);
Two.prototype.getNumTwo = function() {
    return this.numTwo;
}

var two = new Two(3.5);
two.numList.push(6); 
console.log(two.numList); / / [1, 2, 6]
two.getNumOne(); / / 3
two.getNumTwo(); / / 5
Copy the code

The diagram for the combined parasitic inheritance constructor, stereotype, and instance is as follows:

Combination of parasitic characteristics

Combined parasitic inheritance, which only calls the constructor of the parent class once, avoids creating unnecessary and redundant attributes on the prototype object of the subtype, while keeping the prototype chain unchanged, is the most ideal inheritance paradigm for reference types.

ES6 inheritance implementation

Class implements inheritance through the extends keyword.

The sample

class One{
    constructor(name, age) {
        this.name = name;
        this.age = age;
        this.hobby = "Coding"
    }
    getOneInfo() {
        return ` name:The ${this.name}Age:The ${this.age}`; }}// Class Two extends all properties and methods of class One via the extends keyword.
class Two extends One {
    constructor(x, y, gender) {
        // Call the parent class's constructor
        super(x, y);
        this.gender = gender;
    }
    getTwoInfo() {
        // Call getOneInfo() on the parent stereotype. The attributes of the parent are inherited by the subclasses
        return `The ${super.getOneInfo()}Gender:The ${this.gender}Hobbies:The ${this.hobby}`; }}const two = new Two("MMZ"."18"."girl");
two.getTwoInfo(); 
// "Name: MMZ, age: 18, gender: girl, hobby: Coding"
Copy the code

The ⚠️ subclass must call the super method from the constructor method or it will get an error when creating a new instance, as shown below. This is because the subtype’s own this object must be molded by calling the parent’s constructor to get the same attributes and methods as the parent instance, and then processed to add the subtype’s own instance attributes and methods. Only super methods can call superclass instances; without super(), subclasses have no this object of their own. The this keyword can only be used after super is called, otherwise an error will be reported.

class One {}
class Two extends One {
    // Constructor does not call the super method
    constructor() {}}const two = new Two();
Copy the code

The super keyword

The super keyword can be used as either a function or an object.

  1. When called as a function, super represents the constructor of the parent class. ES6 requires that the constructor of a subclass must execute the super function once.
class A {
  constructor() {
    // Return the constructor on which the new command is applied
    console.log(new.target.name); }}class B extends A {
  constructor() {
    / / super () calls the superclass constructor, but returns the instance of the subclass B, namely the super inside this refers to the instance of B super () is equivalent to Amy polumbo rototype here. The constructor. Call (this).
    super();
  }
}
// Check whether B inherits from A
Object.getPrototypeOf(B) === A; // true
// The __proto__ attribute of a subclass always points to the parent class
B.__proto__ === A; // true
// Subclass prototype's __proto__ property always points to the prototype property of the parent class
B.prototype.__proto__ === A.prototype; // true
B.prototype.constructor === B; // true

const a = new A() // A
const b = new B() // B
// The __proto__ attribute of the instance points to the prototype object of the constructor
a.constructor === A; // true
a.__proto__ === A.prototype; // true
b.constructor === B; // true
b.__proto__ === B.prototype; // true
Copy the code

class A {}

class B extends A {
  m() {
    super(a);/ / an error}}Copy the code

  1. When super is an object, in a normal method, it points to the prototype object of the parent class; In static methods, point to the parent class.
class A {
    constructor() {
        this.m = 3;
    }
    p() {
        return 2;
    }
    static q() {
        return 7;
    }
    q() {
        return 8;
    }
}
A.prototype.s = 4;

class B extends A {
    constructor() {
        super(a);// super refers to a.prototype in normal methods, so super.p() equals a.prototype.p ().
        console.log(super.p()); / / 2
        console.log(super.m); // undefined, super refers to the parent class's prototype object, so methods or attributes defined on the parent class instance cannot be called by super
        console.log(super.s); // 4, s is defined on the prototype object of the parent class, super can be fetched.
        console.log(super.q());
    }
    getP() {
        // ES6 specifies that when a method of the parent class is called by super in a subclass normal method, this inside the method refers to the current subclass instance. Equivalent to a super p.c all (this)
        return super.p();
    }
    static getQ() {
        // super refers to the parent class in static methods, not the parent class's prototype object
        return super.q();
    }
    getQ() {
        // The prototype object that points to the parent in a normal method.
        return super.q(); }}let b = new B();
b.getP(); / / 2
b.getQ(); / / 8
B.getQ(); / / 7
Copy the code

Summary: ES5 and ES6 inheritance differences

  • Call (this) creates an instance object of the subclass this, and then adds the attributes and methods of the Parent class to this (parent.call (this)).

  • ES6 has a completely different inheritance mechanism, essentially creating an instance object of the parent class, this (so the super method must be called first), and then modifying this with the constructor of the subclass to implement inheritance.

  • In most browsers’ ES5 implementations, each object has a __proto__ attribute that points to the prototype attribute of the corresponding constructor.

  • Class is the syntactic sugar of the constructor and has both the Prototype and __proto__ attributes, so there are two inheritance chains.

    1. The __proto__ attribute of a subclass, which indicates constructor inheritance, always points to the parent class.
    2. The __proto__ attribute of the prototype attribute, which indicates method inheritance, always points to the Prototype attribute of the parent class.