Inheritance as a basic skill and interview test point, must be mastered. While a small company may only let you write inheritance by hand (usually parasitic combinative inheritance), a large company will require you to thoroughly analyze the pros and cons of each inheritance. This article will give you a thorough understanding of JavaScript inheritance and its pros and cons to stay ahead in the cold.

Prototype chain inheritance

The previous article, “Prototyping/Prototyping Chains from an Emotional Point of view”, introduced what prototyping and prototyping chains are. Let’s briefly recall the relationship between the constructor, prototype, and prototype chain: Each constructor has a prototype property that points to the prototype object, and each prototype object has a constructor pointer to the constructor, and each instance object contains an internal pointer to the prototype object [[prototype]]. If we make the stereotype object equal to another constructor instance, the stereotype object will contain a pointer to another stereotype. So layer by layer, step by step, you have a chain of prototypes.

According to the introduction above, we can write the prototype chain inheritance.

function Vehicle(powerSource) {
  this.powerSource = powerSource;
  this.components = ['seat'.'the wheel'];
}

Vehicle.prototype.run = function() {
  console.log('running~');
};

function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;
}

Car.prototype.playMusic = function() {
  console.log('sing~');
};

// Assign an instance of the parent constructor to the prototype of the child constructor
Car.prototype = new Vehicle();

const car1 = new Car(4);
Copy the code

In the above example, we first define a constructor called vehicle, which has two properties: drive mode and component, and a prototype method called run. Next we define a constructor called car, which has a tire count property and a music playing method. We assign an instance of Vehicle to the Car prototype and create an instance named CAR1.

However, this approach has several disadvantages:

  • Operations on reference types by multiple instances can be tampered with

  • The constructor property on the stereotype of the subtype has been overridden

  • Adding attributes and methods to subtype stereotypes must be done after replacing stereotypes

  • Cannot pass arguments to the parent type constructor while creating an instance of a subtype

Disadvantages 1

As you can see from the figure above, instance attributes of the parent class are added to the stereotype of the instance. When the stereotype’s attributes are of reference type, data tampering will occur.

Let’s add a new instance called car2 and append a new element to car2.components. Print car1 and find that car1.components has also changed. This is where multiple instance operations on reference types can be tampered with.

const car2 = new Car(8);

car2.components.push('the lamps and lanterns');

car2.components; // [' seat ', 'wheel ',' lamp ']
car1.components; // [' seat ', 'wheel ',' lamp ']
Copy the code

Defect 2

The way lead to the Car. The prototype. The constructor is rewritten, it points to the Vehicle rather than a Car. So you will need to manually the Car. The prototype. The constructor refers back to the Car.

Car.prototype = new Vehicle();
Car.prototype.constructor === Vehicle; // true

// Override Car. Prototype's constructor property to point to Car
Car.prototype.constructor = Car;
Copy the code

Disadvantages of 3

Car. Prototype = new Vehicle(); The Car prototype object was overwritten so that the playMusic method was overwritten, so adding a prototype method to a subclass must be done after replacing the prototype.

function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;
}

Car.prototype = new Vehicle();

// Adding a stereotype method to a subclass must be done after replacing the stereotype
Car.prototype.playMusic = function() {
  console.log('sing~');
};
Copy the code

Disadvantages of 4

Obviously, there is no way to pass a parameter to the constructor of the parent class when creating a CAR instance, that is, no way to initialize the powerSource property.

const car = new Car(4);

// You can only modify the attributes of the parent class after the instance is created
car.powerSource = 'gas';
Copy the code

Borrow constructor inheritance

This method is also known as forged object or classical inheritance. It essentially calls the constructor of the parent class when an instance of the subclass is created.

function Vehicle(powerSource) {
  this.powerSource = powerSource;
  this.components = ['seat'.'the wheel'];
}

Vehicle.prototype.run = function() {
  console.log('running~');
};

function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;

  // Inherits attributes from the parent class and can pass arguments
  Vehicle.call(this.'gas');
}

Car.prototype.playMusic = function() {
  console.log('sing~');
};

const car = new Car(4);
Copy the code

The advantage of using classical inheritance is that you can pass arguments to the parent class, and the method does not override the child class’s stereotype, so it does not break the child class’s stereotype method. In addition, since each instance makes a copy of the attributes in the parent class, there is no problem with multiple instances tampering with reference types (because the instance attributes of the parent class are no longer in the stereotype).

However, the disadvantage is obvious. We can’t find any trace of the run method, because this method can only inherit the instance properties and methods of the parent class, not the properties and methods of the prototype.

Recalling constructors from the previous article, to put public methods where all instances can access them, we typically put them in constructor prototypes. If borrowed constructor inheritance were to work, it would be obvious to write the public method in the constructor instead of its prototype, which would be wasteful when creating multiple instances.

Combination of inheritance

Composite inheritance absorbs the advantages of the above two approaches. It uses a stereotype chain to implement inheritance of stereotype methods and borrows constructors to implement inheritance of instance attributes.

function Vehicle(powerSource) {
  this.powerSource = powerSource;
  this.components = ['seat'.'the wheel'];
}

Vehicle.prototype.run = function() {
  console.log('running~');
};

function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;
  Vehicle.call(this.'gas'); // Call the parent class a second time
}

Car.prototype = new Vehicle(); // Call the parent class for the first time

// Fix the pointer to the constructor
Car.prototype.constructor = Car;

Car.prototype.playMusic = function() {
  console.log('sing~');
};

const car = new Car(4);
Copy the code

Although this method succeeds in inheriting the attributes and methods of the parent class, it calls the parent class twice. The first time the constructor of the parent class is called, Car. Prototype gets powerSource and Components; When the Car constructor is called to generate the instance, the Vehicle constructor is called again, and powerSource and Components are created on the instance. According to the rules of the stereotype chain, these two properties on the instance mask the two properties of the same name on the stereotype chain.

Primary inheritance

This method uses prototypes to create new objects based on existing objects.

We first create a function called object, then create an empty function F inside it, point its prototype to the object passed in, and finally return an instance of the function. Essentially, object() makes a shallow copy of the passed object.

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

const cat = {
  name: 'Lolita'.friends: ['Yancey'.'Sayaka'.'Mitsuha'],
  say() {
    console.log(this.name); }};const cat1 = object(cat);
Copy the code

While this approach is neat, there are still some problems. Because original type inheritance is equivalent to shallow copy, reference types can be tampered with by multiple instances. In the following example, we append an element to cat1.friends, causing cat. Friends to be tampered with.

cat1.friends.push('Hachi');

cat.friends; // ['Yancey', 'Sayaka', 'Mitsuha', 'Hachi']
Copy the code

If you’ve read the Polyfill of Object.create(), this code should be familiar. The method, which regulates type-as-usual inheritance, takes two parameters: the first is passed in as an object to be used as a prototype for the new object, and the second is passed in as an attribute descriptor object, or NULL. Detailed documentation about this API can click on the Object. The create () | JavaScript API full resolution

const cat = {
  name: 'Lolita'.friends: ['Yancey'.'Sayaka'.'Mitsuha'],
  say() {
    console.log(this.name); }};const cat1 = Object.create(cat, {
  name: {
    value: 'Kitty'.writable: false.enumerable: true.configurable: false,},friends: {
    get() {
      return ['alone']; ,}}});Copy the code

Parasitic inheritance

This approach creates a function that simply encapsulates the inheritance process, enhances the object internally in some way, and finally returns the object as if it really did all the work.

const cat = {
  name: 'Lolita'.friends: ['Yancey'.'Sayaka'.'Mitsuha'],
  say() {
    console.log(this.name); }};function createAnother(original) {
  const clone = Object.create(original); // Get a copy of the source object

  clone.gender = 'female';

  clone.fly = function() {
    // Enhance this object
    console.log('I can fly.');
  };

  return clone; // Return this object
}

const cat1 = createAnother(cat);
Copy the code

As with old-form inheritance, this approach results in reference types being tampered with by multiple instances. Furthermore, the FLY method exists in instances rather than prototypes, so function reuse is not an issue.

Parasitic combinatorial inheritance

Above we talked about composite inheritance, which has the disadvantage of calling the parent class twice, so instance attributes of the parent class are created on both the instance and the prototype of the subclass, which causes instance attributes to mask attributes of the same name on the prototype chain.

Fortunately, we have parasitic combinatorial inheritance, which essentially inherits the parent’s prototype through parasitic inheritance and then assigns the result to the child’s prototype. This was arguably the best inheritance before ES6, and it’s dead.

function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype); // Create a copy of the superclass prototype
  prototype.constructor = child; // Point the copy's constructor to the subclass
  child.prototype = prototype; // Assign the copy to the prototype of the subclass
}
Copy the code

And then we’ll try to write an example.

function Vehicle(powerSource) {
  this.powerSource = powerSource;
  this.components = ['seat'.'the wheel'];
}

Vehicle.prototype.run = function() {
  console.log('running~');
};

function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;
  Vehicle.call(this.'gas');
}

inheritPrototype(Car, Vehicle);

Car.prototype.playMusic = function() {
  console.log('sing~');
};
Copy the code

Look at the picture above to see why this is the best approach. It calls the parent class only once, so it avoids creating redundant attributes on the prototype of the subclass, and the prototype chain structure remains unchanged.

To be clear, adding attributes and methods to subtype stereotypes still comes after the inheritPrototype function.

ES6 inheritance

Utilitarianism, with the addition of the class syntax in ES6, these methods have been relegated to interviews. Of course, class is just a syntactic sugar, and its core idea is still parasitic combinatorial inheritance. Let’s take a look at how to implement an inheritance using ES6 syntax.

class Vehicle {
  constructor(powerSource) {
    // Use object.assign () to simplify things
    Object.assign(
      this,
      { powerSource, components: ['seat'.'the wheel']},// You can always do it the old-fashioned way
      // this.powerSource = powerSource;
      // this.component.html = [' seat ', 'wheel '];
    );
  }

  run() {
    console.log('running~'); }}class Car extends Vehicle {
  constructor(powerSource, wheelNumber) {
    // Only the super method can call the parent instance
    super(powerSource, wheelNumber);
    this.wheelNumber = wheelNumber;
  }

  playMusic() {
    console.log('sing~'); }}const car = new Car('Nuclear power'.3);
Copy the code

The following code is inherited polyfill, the same idea as parasitic combinatorial inheritance.

function _inherits(subType, superType) {
  // Create object, create a copy of the parent class prototype

  subType.prototype = Object.create(superType && superType.prototype, {
    // Enhance the object to compensate for the loss of the default constructor property by rewriting the prototype
    constructor: {
      value: subType,
      enumerable: false.writable: true.configurable: true,}});if (superType) {
    // Assign the newly created object to the prototype of the subclass
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subType, superType) : (subType.__proto__ = superType); }}Copy the code

Comparison of ES5 and ES6 inheritance

ES5 creates classes with constructors, so function promotion occurs; Class is similar to let and const, so you cannot create an instance and then declare the class, otherwise you will get an error.

// Uncaught ReferenceError: Rectangle is not defined
let p = new Rectangle();

class Rectangle {}
Copy the code

ES5 inheritance essentially creates an instance object of a subclass and then adds the Parent class’s methods to this, parent-call (this).

ES6 inheritance is different, essentially creating an instance object of the parent class, this, and then modifying this with the constructor of the subclass. Because subclasses do not have their own this object, they must first call the super() method of the superclass, or new instances will fail.

The last

Welcome to follow my wechat public number: the front of the attack

reference

JavaScript Advanced Programming (3rd edition) by Nicholas C. Zakas

[Advanced 5-2] Illustrates prototype chains and their inheritance

There are eight inheritance schemes commonly used in JavaScript