Six common inheritance methods of JS

Posted by gfX on Thu, 16 Sep 2021 02:19:56 +0200

Inheritance can make subclasses have various methods and properties of the parent class. Think about a few questions first:

  1. How many implementations are there for JS inheritance?

  2. Which inheritance method is used to implement the extensions keyword of ES6?

1, Several ways to implement inheritance in JS

First: prototype chain inheritance

Prototype chain inheritance is one of the more common inheritance methods, which involves constructors, prototypes and instances. There is a certain relationship among them, that is, each constructor has a prototype object, the prototype object contains a pointer to the constructor, and the example contains a pointer to the prototype object.

  function Parent1() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
  }
  function Child1() {
    this.type = 'child2';
  }
  Child1.prototype = new Parent1();
  console.log(new Child1());

The above code seems to have no problem. Although the methods and properties of the parent class can be accessed, there is a potential problem. Let me give another example to illustrate this problem.

  let s1 = new Child1();
  let s2 = new Child2();
  s1.play.push(4);
  console.log(s1.play, s2.play);

  After this code is executed on the console, you can see the results as follows:

  Obviously, I only changed the play attribute of s1. Why did s2 change? The reason is simple because the two instances use the same prototype object. Their memory space is shared. When one changes, the other also changes, which is a disadvantage of using prototype chain inheritance.

So to solve this problem, we have to look at other inheritance methods. Next, let's look at the second method that can solve the problem of prototype attribute sharing.

Second: constructor inheritance (with the help of call)

  function Parent1(){
    this.name = 'parent1';
  }

  Parent1.prototype.getName = function () {
    return this.name;
  }

  function Child1(){
    Parent1.call(this);
    this.type = 'child1'
  }

  let child = new Child1();
  console.log(child);  // no problem
  console.log(child.getName());  // Will report an error

  This result can be obtained by executing the above code.

  You can see that the last printed child is displayed on the console. In addition to the type attribute of Child1, it also inherits the name attribute of Parent1. In this way, although the subclass can get the attribute value of the parent class, which solves the disadvantages of the first inheritance method, the problem is that once there are self-defined methods in the parent prototype object, the subclass will not inherit these methods. The console execution results in this case are shown in the figure below.

 

Therefore, from the above results, we can see the advantages and disadvantages of constructor implementation inheritance. It makes the reference properties of the parent class not shared, and optimizes the disadvantages of the first inheritance method; But the following disadvantages are also obvious - you can only inherit the instance properties and methods of the parent class, not the prototype properties or methods.

The above two inheritance methods have their own advantages and disadvantages. Combining the advantages of the two, the following combined inheritance method is produced.

The third: combination inheritance (the first two combinations)

This method combines the advantages and disadvantages of the first two inheritance methods. The combined inheritance code is as follows.

  function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
  }

  Parent3.prototype.getName = function () {
    return this.name;
  }
  function Child3() {
    // Call Parent3() the second time
    Parent3.call(this);
    this.type = 'child3';
  }

  // Calling Parent3() for the first time
  Child3.prototype = new Parent3();
  // Manually hang up the constructor and point to its own constructor
  Child3.prototype.constructor = Child3;
  var s3 = new Child3();
  var s4 = new Child3();
  s3.play.push(4);
  console.log(s3.play, s4.play);  // No interaction
  console.log(s3.getName()); // Normal output 'parent3'
  console.log(s4.getName()); // Normal output 'parent3'

By executing the above code, you can see the output of the console, and the problems of method 1 and method 2 have been solved.

  However, a new problem is added here: through annotation, we can see that Parent3 is executed twice. The first time is when the prototype of Child3 is changed, and the second time is when Parent3 is called through the call method. Therefore, if Parent3 is constructed more than once, it will incur more performance overhead, which we don't want to see.

The above description focuses more on the constructor. How to implement inheritance for ordinary JavaScript objects?

  The fourth is prototype inheritance

What must be mentioned here is the Object.create method in ES5. This method receives two parameters: one is the object used as the prototype of the new object, and the other is the object that defines additional properties for the new object (optional parameters).

  let parent4 = {
    name: "parent4",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };

  let person4 = Object.create(parent4);
  person4.name = "tom";
  person4.friends.push("jerry");
  let person5 = Object.create(parent4);
  person5.friends.push("lucy");

  console.log(person4.name);
  console.log(person4.name === person4.getName());
  console.log(person5.name);
  console.log(person4.friends);
  console.log(person5.friends);

As can be seen from the above code, the inheritance of ordinary objects can be realized through the method of Object.create, which can not only inherit properties, but also inherit the method of getName. Please see the execution result of this code.

The first result is "tom", which is easy to understand. person4 inherits the name attribute of parent4, but it is customized on this basis.

The second is the inherited getName method to check whether its name is the same as the value in the attribute. The answer is true.

The third result "parent4" is also easy to understand. person5 inherits the name attribute of parent4 and does not overwrite it. Therefore, the attribute of the parent object is output.

The last two output results are the same. Here you can think of the knowledge points of shallow copy in lecture 02. About the "sharing" of reference data types, in fact, the Object.create method can realize shallow copy for some objects.

The disadvantages of this inheritance method are also obvious. The reference type properties of multiple instances point to the same memory, which may be tampered with. Next, let's take a look at another inheritance method optimized based on this inheritance - parasitic inheritance.

  Fifth: parasitic inheritance

Using prototype inheritance, you can obtain a shallow copy of the target object, and then enhance the ability of this shallow copy and add some methods. This inheritance method is called parasitic inheritance.

Although its advantages and disadvantages are the same as prototype inheritance, parasitic inheritance adds more methods on the basis of parent class than prototype inheritance for the inheritance of ordinary objects. So let's see how the code is implemented.

   let parent5 = {
    name: "parent5",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };

  function clone(original) {
    let clone = Object.create(original);
    clone.getFriends = function() {
      return this.friends
    };
    return clone;
  }

  let person5 = clone(parent5);
  console.log(person5.getName());
  console.log(person5.getFriends());

Through the above code, we can see that person5 is an instance generated through parasitic inheritance. It not only has the method of getName, but also has the method of getFriends. The results are shown in the following figure.

  Sixth: parasitic combinatorial inheritance

Combined with the inheritance method mentioned in the fourth, the Object.create method to solve the inheritance problem of ordinary objects, we modified the advantages and disadvantages of the previous inheritance methods, and obtained the parasitic combined inheritance method, which is also the relatively optimal inheritance method among all inheritance methods. The code is as follows.

  function clone (parent, child) {
    // Using Object.create instead can reduce the process of one more construction in composite inheritance
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
  }

  function Parent6() {
    this.name = 'parent6';
    this.play = [1, 2, 3];
  }
   Parent6.prototype.getName = function () {
    return this.name;
  }
  function Child6() {
    Parent6.call(this);
    this.friends = 'child5';
  }

  clone(Parent6, Child6);

  Child6.prototype.getFriends = function () {
    return this.friends;
  }

  let person6 = new Child6();
  console.log(person6);
  console.log(person6.getName());
  console.log(person6.getFriends());

It can be seen from this code that this parasitic combined inheritance method can basically solve the shortcomings of the previous inheritance methods, better achieve the desired results of inheritance, reduce the construction times and reduce the performance overhead. Let's take a look at the execution results of the above code.

 

You can see the results printed by person6. The properties are inherited, and the methods are OK. You can output the expected results.

On the whole, parasitic combinatorial inheritance is the best of the six inheritance methods. In addition, ES6 also provides the inherited keyword extends. Let's take a look at the underlying implementation inheritance logic of extends.

2, The extensions keyword of ES6 implements logic

We can use the extensions syntax sugar in ES6 and use keywords to directly implement JavaScript inheritance. However, if we want to deeply understand how the extensions syntax sugar is implemented, we have to deeply study the underlying logic of extensions.

class Person {
  constructor(name) {
    this.name = name
  }
  // Prototype method
  // That is, Person.prototype.getName = function() {}
  // It can be abbreviated as getName() {...}
  getName = function () {
    console.log('Person:', this.name)
  }
}

class Gamer extends Person {
  constructor(name, age) {
    // If there is a constructor in the subclass, you need to call super() before using "this".
    super(name)
    this.age = age
  }
}

const asuna = new Gamer('Asuna', 20)
asuna.getName() // The method of the parent class was successfully accessed

By compiling and decoding, it can be found that the extensions inner step also adopts the parasitic combination inheritance mode.

Total:

Topics: Javascript html5 html css