This paper combs the seven common inheritance schemes in JavaScript

📖 preface

In the front-end interview, inheritance is a common question, and the interviewer will always try his best to ask you. For example, what kinds of inheritance do you know? What is the difference between these inheritance methods? What are the advantages and disadvantages of these inheritance methods? What are the characteristics of each?

One move is fatal. When I first met this problem on Monday, I only remember that there are several inheritance methods, but I can't say. Therefore, knowledge is still too floating on the surface.

Therefore, write this article to summarize the knowledge points of various inheritance methods. Learn together~ ✨

📔 Read the article first

Before really entering the explanation of this article, let's use a mind map to understand the content involved in this article. See the figure below for details 👇

Let's start with the explanation of this article~

📝 1, Basic knowledge preparation

1. Definition of succession

Quote a sentence from Javascript high-level language programming:

Inheritance is the most discussed topic in object-oriented language (OO language). Many OO languages support two types of inheritance: interface inheritance and implementation inheritance. The former can only inherit the method signature, while the latter can inherit the actual method.

Then, interface inheritance is unlikely to exist in ECMAScript because the function is not signed.

Therefore, implementing inheritance is the only inheritance method supported by ECMAScript, and this is mainly realized through the prototype chain.

2. Mode of succession

After understanding the definition, let's take a look at how many methods there are for inheritance. See the figure below for details 👇

📚 2, 6 common inheritance methods

1. Prototype chain inheritance 💡

(1) Relationship between constructors, prototypes, and instances

The relationship among constructor, prototype and instance is as follows: each constructor has a prototype object, a property on the prototype refers back to the constructor, and the instance has an internal pointer to the prototype.

(2) Basic thought

The basic idea of prototype chain inheritance is to inherit the properties and methods of multiple reference types through the prototype. Its core is to take the instance of the parent class as the prototype of the child class.

(3) Implement prototype chain inheritance

The above content may seem a little abstract. Let's use an example to experience prototype chain inheritance. The specific codes are as follows:

// Parent class function
function SuperType() {
    // Parent class definition properties
    this.property = true;
}

// Parent class definition method
SuperType.prototype.getSuperValue = function() {
    return this.property;
}

// Subclass function
function SubType() {
    this.subproperty = false;
}

/**
 * Key points:
 * By creating an instance of the parent SuperType,
 * And assign the instance to the subclass subtype prototype
 */
SubType.prototype = new SuperType();
// Subclass definition new method
SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

// Create an instance of a subclass
let instance = new SubType();
// The subclass calls the method of the parent class
console.log(instance.getSuperValue()); // true

(4) Legend description

According to the above code, let's use a diagram to represent the inheritance relationship of this code. As shown in the figure below:

Locate the top right corner of the picture, and we will also analyze it from top right to bottom left.

First, we will create an instance of the constructor SuperType and prototype it as a subclass, that is, SuperType prototype . After that, the instance is created, and a content pointer on the instance points to the prototype SuperType of the parent class prototype . After that, we come to the third step. There is a property on the prototype that will refer back to the constructor SuperType. Step 4: for constructors, each constructor has its own prototype, so it will refer back to SuperType prototype .

According to the above description, if you look at the relationship and basic ideas of (1) and (2), will it be much clearer.

Similarly, the following steps ⑤ ~ ⑧ are the same as the above steps. You can understand the corresponding again by yourself. We won't elaborate here.

(5) Destroy prototype chain

Another important point to understand is to create a prototype method in an object literal way, which destroys the previous prototype chain, because it is equivalent to rewriting the prototype chain. The following example is shown:

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
};

function SubType() {
    this.subproperty = false;
}

// Inherit SuperType
SubType.prototpe = new SuperType();
// Adding a new method through the object literal will invalidate the previous line
SubType.prototype = {
    getSubValue() {
        return this.subproperty;
    },
    someOtherMoethod(){
        return false;
    }
}

let instance = new SubType();
console.log(instance.getSuperValue()); 
// TypeError: instance.getSuperValue is not a function

In the above code, the prototype of the subclass SubType is overwritten by an Object literal after it is assigned as an instance of SuperType. The overwritten prototype has actually become an Object instance instead of an instance of SuperType. Therefore, the prototype chain is broken after being assigned to the Object literal. At this time, SubType and SuperType will no longer have any relationship.

(6) Advantages and disadvantages

1) Advantages:

  • The methods of the parent class can be reused.

2) Disadvantages:

  • All reference type data of the parent class will be shared by all subclasses, that is, once the reference type data of a subclass is changed, other subclasses will also be affected.
  • A subtype instance cannot pass arguments to a parent type constructor. The reason is that once parameters are passed, the previous parameters will be overwritten when the same parameters are passed due to the factors of the first problem.

2. Stealing constructor inheritance 💡

(1) Basic thought

For the above two problems of prototype chain inheritance, the prototype chain will not be used alone. Therefore, in order to solve the problem caused by the inclusion of reference values in the prototype, we introduce a new inheritance method called "stealing constructor". This technique is also known as object camouflage or classic inheritance.

The basic idea is to use the constructor of the parent class to strengthen the subclass instance, which is equivalent to assigning the instance of the parent class to the subclass (without using the prototype).

(2) Mode of use

A function is a simple object that executes code in a specific context, so you can use the apply() and call() methods to execute the constructor in the context of the newly created object.

(3) Implement prototype chain inheritance

We use an example to implement prototype chain inheritance. The specific codes are as follows:

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

function SubType(name, age) {
    // Inherit SuperType and pass parameters. Here is the core
    SuperType.call(this, 'monday');
    // Instance properties
    this.age = age;
}

let instance1 = new SubType("monday", 18);
instance1.colors.push("gray");
console.log(instance1.name); // monday
console.log(instance1.age); // 18
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'gray' ]

let instance2 = new SubType();
console.log(instance2.colors); // [ 'red', 'blue', 'green' ]

You can see that in the above example, the SuperType constructor is borrowed by using the call() method. In this way, when creating an instance of a SubType, a copy of the attributes in the SuperType will be copied.

At the same time, compared with the prototype chain, one advantage of stealing constructor is that it can pass parameters to the parent constructor in the child constructor. You can see the above code. We are using SuperType Call(), you can pass the parameter directly to the SuperType of the parent class.

(4) Advantages and disadvantages

1) Advantages:

  • The constructor of a subclass can pass parameters to the constructor of a parent class.

2) Disadvantages:

  • Only instance properties and methods of the parent class can be inherited, and prototype properties and methods of the parent class cannot be inherited.
  • Reuse cannot be realized. Every time a new subclass is created, a copy of the parent class instance function will be generated, which greatly affects the performance.

3. Combination inheritance 💡

(1) Basic thought

Because the prototype chain and the way of stealing constructor inheritance have some defects, they can not be used alone. To this end, we have introduced a new inheritance method, combinatorial inheritance. Combinatorial inheritance is also called pseudo classical inheritance. It combines the advantages of prototype chain and stealing constructor.

The basic idea of its implementation is: use the prototype chain to inherit the properties and methods on the prototype, and then steal the constructor to inherit the properties on the instance.

In this way, the reuse of functions is realized by defining methods on the prototype, and each instance can be guaranteed to have its own properties.

(2) Implement composite inheritance

Let's use an example to implement composite inheritance. The specific codes are as follows:

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    // Inherit attribute → borrow constructor to inherit attribute on instance
    SuperType.call(this, name);
    this.age = age;
}

// Inherit method → inherit the properties and methods of the prototype through the prototype
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log(this.age);
}

let instance1 = new SubType('Monday', 18);
instance1.colors.push('gray');
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'gray' ]
instance1.sayName(); // Monday
instance1.sayAge(); // 18

let instance2 = new SubType('Tuesday', 24);
console.log(instance2.colors); // [ 'red', 'blue', 'green' ]
instance2.sayName(); // Tuesday
instance2.sayAge(); // 24

As you can see, in the above code, the attribute on the SuperType instance of the parent class is inherited by stealing the constructor. At the same time, it successfully inherits the properties and methods on the SuperType prototype of the parent class through the prototype.

(3) Legend

Let's use a graph to show the above results. The details are as follows:

(4) Advantages and disadvantages

1) Advantages:

  • Avoid the defects of prototype chain and stealing constructor inheritance, and realize the inheritance of instances and prototypes.
  • Composite inheritance is a common inheritance method in javascript, and retains the ability of instanceof operator and isPrototypeOf() method to identify composite objects.

2) Disadvantages:

  • The instance properties and methods in the parent class exist in both the instance of the child class and the prototype of the child class, which will occupy more memory.
  • Therefore, when using subclasses to create an instance object, there will be two identical properties and methods in its prototype.

4. Prototype inheritance 💡

(1) Basic thought

The basic idea of prototype inheritance implementation is to assign an object directly to the prototype of the constructor. The following code is shown:

function object(obj) {
    function F(){}
    F.prototype = obj;
    return new F();
}

The above code was mentioned in an article written by Douglas Crockford in 2006. This article introduces an inheritance method that does not involve constructors in a strict sense, and its starting point is that even without custom types, information sharing between objects can be realized through prototypes.

In the above code, object() executes the object passed in once and points the prototype of F directly to the incoming object.

(2) Implement prototype inheritance

Let's use an example to implement prototype inheritance. The specific codes are as follows:

function object(obj){
    function F(){}
    F.prototype = obj;
    return new F();
  }

let person = {
    name: 'Monday',
    friends: ['January', 'February', 'March']
};

let otherPerson = object(person);
otherPerson.name = 'Friday';
otherPerson.friends.push('April');

let anotherPerson = object(person);
anotherPerson.name = 'Sunday';
anotherPerson.friends.push('May');

console.log(person.friends); // [ 'January', 'February', 'March', 'April', 'May' ]

As you can see, all friend s are copied to the person object. However, this method is rarely used, which is somewhat similar to the pattern of prototype chain inheritance, so it will not be used alone.

(3) Advantages and disadvantages

1) Advantages:

  • It is suitable for situations where you do not need to create a separate constructor, but you still need to share information between objects.

2) Disadvantages:

  • When inheriting the reference type data of multiple instances, the points are the same, so there is the possibility of tampering.
  • Unable to pass parameter.
  • Object already exists in ES5 The method of create () can replace the object method above.

5. Parasitic inheritance 💡

(1) Basic thought

The basic idea of parasitic inheritance is to enhance the object and return the object in some way based on the above prototype inheritance.

(2) Implement parasitic inheritance

Next, we use an example to implement parasitic inheritance. The specific code is as follows:

// object function
function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}

// The main function is to add properties and methods to the constructor to enhance the function
function createAnother(original) {
  // Create a new object by calling a function
  let clone = object(original);
  // Enhance this object in some way
  clone.sayHello = function() {
    console.log('hello');
  }
  // Return object
  return clone; 
}

let person = {
  name: 'Monday',
  friends: ['January', 'February', 'March']
};

let anotherPerson = createAnother(person);
anotherPerson.sayHello(); // hello

As you can see, the content of the object is enhanced by creating a new constructor, createnotify. And then return this object for our use.

(3) Advantages and disadvantages

1) Advantages:

  • It is suitable for scenarios that only focus on the object itself, not the data type and constructor.

2) Disadvantages:

  • Adding a function to an object makes the function difficult to reuse, similar to stealing constructor inheritance in Section 2 above.
  • When inheriting the reference type data of multiple instances, the points are the same, so there is the possibility of tampering.
  • Unable to pass parameter.

6. Parasitic combinatorial inheritance 💡

(1) Basic thought

The basic idea of parasitic combinatorial inheritance is to realize inheritance by combining stolen constructor and parasitic pattern.

(2) Implement parasitic composite inheritance

We use an example to show parasitic composite inheritance. The specific codes are as follows:

function inheritPrototype(subType, superType) {
    // create object
    let prototype = Object.create(superType.prototype);
    // Enhanced object
    prototype.constructor = subType;
    // Specify object
    subType.prototype = prototype;
}

// The parent class initializes instance properties and prototype properties
function SuperType(name) {
    this.name = name;
    this.friends = ['January', 'February', 'March'];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}

// The constructor is used to transfer parameters to subclass instance properties (support parameter transfer and avoid tampering)
function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

// Using the characteristics of parasitic inheritance, the prototype of the parent class is pointed to the child class
inheritPrototype(SubType, SuperType);

// Prototype properties of new subclasses
SubType.prototype.sayAge = function() {
    console.log(this.age);
}

let instance1 = new SubType('Ida', 18);
let instance2 = new SubType('Hunter', 23);

instance1.friends.push('April');
console.log(instance1.friends); // [ 'January', 'February', 'March', 'April' ]
instance1.friends.push('May');
console.log(instance2.friends); // [ 'January', 'February', 'March' ]

You can see that for parasitic composite inheritance, it borrows the constructor to pass parameters to the instance properties of the subclass. At the same time, by using the characteristics of parasitic inheritance, it uses the inheritPrototype constructor to point the prototype of the parent class to the subclass. In this way, the subclass inherits the prototype of the parent class.

At the same time, subclasses can also add their desired prototype attributes on their own prototypes to achieve the effect of inheriting their own prototype methods.

(3) Legend

Let's use a graph to show the above results. The details are as follows:

(4) Advantages and disadvantages

1) Advantages:

  • Parasitic composite inheritance is the best way to inherit reference types.
  • It avoids almost all the defects in the above inheritance methods, and is also the most efficient and widely used.

2) Disadvantages:

  • The implementation process is relatively cumbersome. We should smooth the relationship between father and son and avoid jumping into the vortex of prototype. This is actually not a disadvantage, because it is worth it!

🗞️ 3, Class inheritance

1. Basic concepts

In the above, we talked about various types of inheritance, but this seems to be inseparable from the closed loop of the prototype chain. By 2015, the emergence of ES6 has solved this problem. The class of ES6 can inherit through the extends keyword, which is indirectly clearer and more convenient than that of ES5 by modifying the prototype chain.

Let's take an example to see how class implements inheritance. The specific codes are as follows:

class Point{
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}

class ColorPoint extends Point {
    constructor(x, y, color) {
        // Call the constructor(x, y) of the parent class
        // Only the super method can return an instance of the parent class
        super(x, y); 
        this.color = color;
    }
    
    toString() {
        return this.color + ' ' + super.toString() {
            // Call the toString() of the parent class
        }
    }
}

You can see that the attributes and methods of the parent class are inherited through the extends keyword, which seems to be much more convenient than the parasitic composite inheritance above.

Next, let's continue to look at other uses.

2. Object.getPrototypeOf()

In ES6, object The getprototypeof method can be used to get the parent class from the child class. For example:

Object.getPrototypeof(ColorPoint) === Point
// true

Therefore, you can use this method to determine whether a class inherits another class.

3. super keyword

We saw the keyword super above, which is mainly used to return the instance of the parent class. In practical applications, super can be used as a function or as an object. Next, let's talk about these two situations.

(1) Use as a function

In the first case, when super is called as a function, it represents the constructor of the parent class. ES6 requires that the constructor of a subclass must execute the super function once. Examples are as follows:

class A {}

class B extends A {
    constructor() {
        super();
    }
}

super() in the above code calls the constructor of parent class A, but returns an instance of child class B, that is, this inside super refers to B. Therefore, super() here is equivalent to a.prototype constructor. call(this) .

It is worth noting that as a function, super() can only be used in the constructor of subclasses, and it will report an error in other places. For example:

class A {}

class B extends A {
	m() {
		super(); // report errors
	}
}

As you can see, in the above case, the use of super() in the m method of class B will cause syntax errors.

(2) Use as object

In the second case, when super is the object, it points to the prototype object of the parent class in the normal method and points to the parent class in the static method. Let's analyze these two situations one by one.

1) In the common method

Let's look at a piece of code, as follows:

class A {
	p() {
		return 2;
	}
}

class B extends A {
	constructor() {
		super();
		console.log(super.p()); // 2
	}
}

let b = new B();

In the above code, super in subclass B P () is to use super as an object. According to the above description, super points to the prototype object of the parent class in the normal method. Therefore, super here P () equals a.prototype p() . So the final output is 2.

Continue, because super points to the prototype object of the parent class, methods or properties defined on the parent class instance cannot be called through super. For example:

class A {
	constructor() {
		this.p = 2;
	}
}

class B extends A {
	get m() {
		return super.p; // Defined on a normal method, so super points to the prototype object of the parent class
	}
}

let b = new B();
b.m // undefined

In the above code, super is invoked in the common method, so it means that super points to the parent object of the parent class. However, b.m wants to point directly to the attribute of the instance of parent class A. naturally, it cannot be searched.

We can modify the code to define the attribute on the prototype object of the parent class, so that super can find the specific attribute. The details are as follows:

class A {}
A.prototype.x = 2;

class B extends A {
	constructor() {
		super();
		console.log(super.x); // 2
	}
}

let b = new B();

As you can see, attribute x is now defined on A.prototype, so super X can get its value smoothly.

2) In static methods

Above, we talked about the call in ordinary methods when super is used as an object. Now let's take a look at the call in static methods.

First, we talked about a point that when super is called in a static method, it points to its parent class. Let's look at a piece of code:

class Parent {
	static myMethod(msg) {
		console.log('static', msg);
	}
	
	myMethod(msg) {
		console.log('instance', msg);
	}
}

class Child extends Parent {
	static myMethod(msg) {
		super.myMethod(msg);
	}
	
	myMethods(msg) {
		super.myMethods(msg);
	}
}

Child.myMethod(1); // static 1
let child = new Child();
child.myMethod(2); // instance 2

As you can see, when using child directly When calling mymethod (1), it indicates that the static method is called directly. When super is called in a static method, it points to the static method in its parent class, so print static 1.

Continue. The following child instantiates an object through new, and then calls the instance. Therefore, ordinary methods are called at this time, so instance 1 is finally printed out.

When using super, you must pay attention to the fact that you must explicitly specify whether to use it as a function or as an object, otherwise an error will be reported. For example:

class A {}
class B extends A {
	constructor() {
		super();
		console.log(super); // report errors
	}
}

As you can see, if you reference like the above, you can't see whether it is referenced as a function or as an object. At this time, the js engine will report an error when parsing the code.

Then we have to clearly indicate the data type of super to judge our results. For example:

class A {}
class B extends A {
	constructor() {
		super();
		// object.valueOf() represents the original value of the returned object
		console.log(super.valueOf() instanceof B); // true
	}
}

In the above code, we use object Valueof () to indicate that super is an object, which is recognized by the js engine and finally printed successfully.

4. prototype attribute and__ proto __ attribute

(1) class inheritance chain

In most browser ES5 implementations, each object has__ proto__ Property, pointing to the prototype property of the corresponding constructor.

class, as the syntax sugar of constructor, has both prototype attribute and__ proto__ Property, so there are two inheritance chains at the same time. namely:

  • Subclass__ proto__ Property represents the inheritance of the constructor and always points to the parent class.
  • Of the subclass prototype attribute__ proto__ Property represents the inheritance of a method and always points to the prototype property of the parent class.

Let's use a piece of code to demonstrate:

class A {

}

class B extends A {

}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

In the above code, subclass B__ proto__ Property always points to parent class A and the prototype property of child class B__ proto__ Property points to the prototype property of parent class A.

These two prototype chains can be understood as:

  • When used as an object, the prototype of subclass B (_proto_ attribute) is parent class A;
  • When used as a constructor, the prototype of subclass B (prototype attribute) is an instance of the parent class.

(2) Inheritance under special circumstances

Three special inheritance situations are discussed below. The details are as follows:

The first case: subclasses inherit the Object class. Let's start with a code:

class A extends Object {
    
}

A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true

In this case, A is actually A copy of the constructor Object, and the instance of A is an instance of Object.

The second case: there is no inheritance. Let's start with a code:

class A {

}

A.__proto__ === Function.prototype // true
B.prototype.__proto__ === Object.prototype // true

In this case, as A base class, A does not have any inheritance relationship. And class is the syntax sugar of the constructor, so at this time, it can be said that A is an ordinary function. Therefore, A directly inherits function prototype .

It is worth noting that after A call, an empty Object (i.e. an Object instance) is returned, so A.prototype. _proto_ points to the prototype attribute of the constructor Object.

The third case: subclass inherits null. Let's start with a code:

class A extends null {
	
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true

The above situation is very similar to the second situation. A is also an ordinary function, so it directly inherits function prototype .

It is worth noting that the object returned after A call does not inherit any methods, so its__ proto__ Point to function Prototype actually executes the following code:

class C extends null {
	constructor() {
		return Object.create(null);
	}
}

(3) The proto property of the instance

For subclasses, their instances are__ proto__ Attribute__ proto__ Property always points to an instance of the parent class__ proto__ Properties. That is, the prototype of the child class is the prototype of the parent class.

Let's look at a piece of code, as follows:

let p1 = new Point(2, 3);
let p2 = new ColorPoint(2, 3, 'green');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

In the above code, the subclass ColorPoint inherits Point, so the prototype of the former prototype is the prototype of the latter.

Therefore, we can use the subclass instance__ proto__.__proto__ Property to modify the behavior of the parent class instance. The details are as follows:

p2.__proto__.__proto__.printName = function() {
	console.log('Monday');
};

p1.printName(); // 'Monday'

As you can see, the Point instance p1 is affected by adding methods to the Point class on the ColorPoint instance p2.

📑 4, Conclusion

We mentioned the six inheritance methods and class inheritance above. In today's development scenarios, it is basically parasitic combination inheritance and class inheritance. I believe that through the above understanding, we have a new understanding of javascript inheritance.

Here, the explanation of js inheritance is over! Hope to help you!

If the article is wrong or incomprehensible, please leave a message in the comment area~ 💬

🐣 One More Thing

(: references)

book 👉 ES6 Book Introduction to ES6 standards

book 👉 Hongbao book "JavaScript advanced programming" Fourth Edition

Strange fate with dream 👉 Notes on javascript Advanced Programming: Inheritance

(: Fan Wai Pian)

  • Pay attention to the official account of Monday's research room. First, we will focus on quality articles.
  • If this article is useful to you, remember to leave a footprint jio before you go~
  • The above is the whole content of this article! See you next time! 👋👋👋

Keywords: Javascript Front-end Class inheritance

Added by OLG on Mon, 03 Jan 2022 00:48:45 +0200