There are many ways to implement Javascript inheritance. Although I have learned it before, I have not integrated it. This time I will sort out the knowledge of Javascript language inheritance. For detailed Javascript inheritance, I recommend the Little Red Book ———— Javascript Advanced Programming.

Although ES6 introduced the concept of class, it is easy for us developers to learn and understand, but class is just a syntactic sugar, in fact, the underlying implementation is the same as before, using prototype chains and constructors to implement inheritance. So if you want to be solid in the basics of Javascript, you still need to learn these things.

In Javascript inheritance implementation, there are currently prototype chain inheritance method, constructor inheritance method, combination inheritance method and so on, I will illustrate a pair of these methods.

1. Prototype chain inheritance

The prototype chain inheritance method is implemented by using the prototype of Javascript. In Javascript, any function has two properties: prototype and __proto__, and every object has a __proto__ attribute. The value of the __proto__ attribute in the object is derived from the prototype attribute of the function that constructed the object. From prototype and __proto__, we construct the prototype chain, and then use the prototype chain to implement inheritance.

A code example is shown below

function Animal() {
    this.type = 'Cat'
    this.name = 'Nini'
    this.hobbies = ['eat fish'.'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}

function Cat() {
    this.age = '1'
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

let smallCat = new Cat()
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini

let bigCat = new Cat()
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball', 'sleep' ]
Copy the code

From the above example, we can see the advantages of prototype chain inheritance:

  • Multiple instances reference reusable properties and methods together, rather than creating the data again for each instance

Disadvantages:

  • All attributes are shared by the instance, which means that if the attribute is a primitive data type, the instance cannot change the value of the attribute, because the instance will add a new attribute of the same name. We can only operate on the new attribute, as shown in the code above
smallCat.name = 'Kiki' // The smallCat object now has a new name property. If we access this property, we get the new property instead of the name property on the prototype
console.log(smallCat.name) // 'Kiki'
console.log(bigCat.name) // 'Nini'
Copy the code

If the attribute is a reference attribute, modifying the content of the data to which the attribute refers will affect all instances (note that the attribute is not directly assigned; if the attribute is directly assigned, just like the primitive data type, a new attribute is created on the instance itself), as in the previous code instance

smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball', 'sleep' ]
Copy the code

Constructor inheritance

The basic principle of constructor inheritance is to use methods such as call and apply to specify the value of this to implement the inheritance of attributes from the parent class, as shown in the following example

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish'.'play ball']}function Cat(type, name) {
    Animal.call(this, type, name)
    this.age = '1'
    this.say = (a)= > {
        console.log('type is ' + this.type + ' name is ' + this.name); }}let smallCat = new Cat('Cat'.'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini

let bigCat = new Cat('Cat'.'Nicole')
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball' ]
bigCat.say() // type is Cat name is Nicole
Copy the code

As you can see from the above example, the advantages of constructor inheritance are

  • All instances do not share reference attributes, which means that each instance has an independent copy of the attributes inherited from its parent class. Any modification of the data content of a reference attribute by one instance does not affect the other instances

  • You can pass parameters to a parent function

Disadvantages:

  • Since all properties and methods are no longer shared by all instances, those public properties and methods are created repeatedly, causing additional memory overhead

3. Combinatorial inheritance (combination of stereotype chain inheritance and constructor inheritance)

In fact, from the previous analysis, we can know that both prototype chain inheritance and constructor inheritance have their own advantages and disadvantages, which are not perfect for our development implementation. Stereotype chain inheritance shares all attributes and methods with all instances. That is to say, if we want to personalize the data content of the inherited reference attributes in one instance, this operation will affect other instances at the same time, which may cause certain problems for our development. Constructor inheritance makes a separate copy of all properties and methods for each instance. Although it achieves data isolation between instances, repeated and meaningless copies of properties and methods that are supposed to be common add additional memory overhead.

Therefore, the combined inheritance method absorbs the advantages of the two methods and avoids the disadvantages of each method. It is a feasible inheritance method, and the code is as follows

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish'.'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}
function Cat(type, name) {
    Animal.call(this, type, name) // Constructor inheritance
    this.age = '1'
}
Cat.prototype = new Animal() // Prototype chain inheritance
Cat.prototype.constructor = Cat

let smallCat = new Cat('smallCat'.'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is smallCat name is Nini

let bigCat = new Cat('bigCat'.'Nicole')
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
bigCat.say() // type is bigCat name is Nicole
Copy the code

The idea of combinatorial inheritance method is to put the common attributes and methods on the prototype of the parent class, and then use the prototype chain inheritance to realize the inheritance of the common attributes and methods, and for the attributes that can be customized for each instance, adopt the constructor inheritance method to realize each instance has a unique copy of such attributes.

4. Original type inheritance

Primitive inheritance is implemented by passing an object as a prototype to create an object into a function that builds a new object, for example

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

The Object. Create () method is the same as Object. Create () method

let Animal = {
    type: 'Cat'.name: 'Nini'.hobbies: ['eat fish'.'play ball']}function createCat(o) {
    function F() {}
    F.prototype = o
    return new F()
}

let smallCat = createCat(Animal)
let bigCat = createCat(Animal)
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
bigCat.name = 'Nicole' // Add a name attribute directly to the bigCat object, not to modify the name attribute on the prototype
console.log(smallCat.name); // 'Nini'
console.log(bigCat.name); // 'Nicole'
console.log(bigCat.__proto__.name); // The name attribute on the 'Nini' prototype remains unchanged
Copy the code

In fact, the original type inheritance method is somewhat similar to the prototype chain inheritance, in that all attributes and methods are placed on the prototype. If all instances are created using the same object as the prototype, then the problems encountered by the prototype chain inheritance method also have.

More thoughts on prototypal inheritance

In the study of inheritance of the original type, I think if you create each instance, introduced to the parent of the object is different, but they are all belongs to a parent class of objects, so if we apply the public properties and methods on the prototype of the parent class, customizable attributes on the superclass constructor, that also can achieve a reasonable inheritance, The specific code is as follows

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish'.'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}
function createCat(o) {
    function F() {}
    F.prototype = o
    return new F()
}

let smallCat = createCat(new Animal('smallCat'.'Nini'))
let bigCat = createCat(new Animal('bigCat'.'Nicole'))
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
Copy the code

This idea looks good, but I think there are still some problems, compared to the aforementioned combined inheritance, every time this method in the instance is created, we will be new a new parent class instance, it caused the waste of memory, while the combination of inheritance ensures the instance of the parent class is limited only to the new time, The attributes that can be customized are stored in the instances of each subclass to ensure that the data does not affect each other. We can see the specific differences in the picture below

Parasitic inheritance

Parasitic inheritance is similar to the implementation of primitive inheritance, except that it adds some form of enhancement to the object in the function that creates the instance, and then returns the object. In the function that creates a subinstance, an instance is created using the same inherited method, attributes and methods are added to the instance, and the instance is returned

function createCat(o) {
    let cloneObj = Object.create(o)
    cloneObj.say = function (){ // Add a say method to the instance
        console.log('type is ' + this.type + ' name is ' + this.name);
    }
    return cloneObj
}

let Animal = {
    type: 'Cat'.name: 'Nini'.hobbies: ['eat fish'.'play ball']}let smallCat = createCat(Animal)
let bigCat = createCat(Animal)
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini
bigCat.say() // type is Cat name is Nini
Copy the code

Through the code above we can see clearly that the shortcoming of parasitic inherit to the prototype chain and constructors inherited weakness, that is to say by parasitic inheritance created instance, if you modify the content of its reference property on the prototype, the other instances will be affected, and each time you create an instance, Those public properties and methods are created once.

Parasitic combinatorial inheritance

We mentioned above that composite inheritance is a good way to implement inheritance, allowing each instance to have inherited customizable properties and methods, but also to share common methods and properties. However, there are some optimizations that can be made to this method. The one that needs to be optimized is that when combinatorial inheritance occurs, the parent class constructor is called twice

function Cat(type, name) {
    Animal.call(this, type, name) // The parent constructor is called once
    this.age = '1'
}
Cat.prototype = new Animal() // The parent constructor is called once
Cat.prototype.constructor = Cat
Copy the code

In fact, the prototype child function only needs to point to the public properties and methods, not to the entire parent function instance. Since we put the public properties and methods we need to inherit on the parent function, So we can consider giving the child’s prototype indirect access to the parent’s prototype. An example of the code implemented is shown below

// Use parasitic inheritance to give the child's prototype access to the parent's prototype
function createObj(child, parent) {
    let prototype = Object.create(parent.prototype) 
    // This object lacks the properties and methods that the child functions inherit from parent. Call, and contains only one property that points to the parent function's prototype
    prototype.constructor = child
    child.prototype = prototype
}
createObj(Cat, Animal)
Copy the code

Finally, the complete implementation code for parasitic combinatorial inheritance is as follows

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish'.'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}
function Cat(type, name) {
    Animal.call(this, type, name)
    this.age = '1'
}

function createObj(child, parent) {
    let prototype = Object.create(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
createObj(Cat, Animal)

let smallCat = new Cat('smallCat'.'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is smallCat name is Nini

let bigCat = new Cat('bigCat'.'Nicole')
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
bigCat.say() // type is bigCat name is Nicole
Copy the code

Therefore, the parasitic combinatorial inheritance has absorbed the advantages of the combinatorial inheritance and avoided creating unnecessary and redundant attributes on the prototype of the sub-function, and the parasitic combinatorial inheritance is also an ideal and good inheritance method realization at present.

conclusion

In fact, the key point of Javascript inheritance is to make sure that private attributes and methods and public attributes and methods are handled separately. Private attributes and methods need to be unique to each instance to ensure that data changes do not affect each other. Public attributes and methods need to be placed on the prototype of the parent class to ensure that they are not created repeatedly.