This is the 20th day of my participation in the August More Text Challenge.
An exploration of the concept of inheritance
-
When it comes to the concept of inheritance, let’s start with a classic example.
-
First define a Class (Class) called car, car attributes include color, tires, brand, speed, displacement, etc., from the car this Class can be derived from “car” and “truck” two classes, so you can add a car on the basis of the car attributes, add a trunk to the truck to add a large container. Cars and trucks are not the same, but both belong to the class of cars, so from this example we can elaborate on the inheritance relationship between cars, cars and trucks.
-
Inheritance allows subclasses to have the same methods and attributes as their parent class, such as in the example above where “car” and “van” inherit the attributes of the car, respectively, without having to define the attributes of the car in “car” again. When “car” inherits “car”, it can also redefine some attributes of the car, rewrite or overwrite some attributes and methods, so that it can obtain different attributes and methods from “car”, the parent class.
With the basic concepts of inheritance in mind, let’s take a look at some of the ways in which JavaScript implements inheritance.
JS implementation of several ways of inheritance
The first type: prototype chain inheritance
- Prototype chain inheritance is one of the common inheritance methods, which involves the constructor, prototype and instance, there is a certain relationship between the three, that is, each constructor has a prototype object, the prototype object contains a pointer to the constructor, and the instance contains a pointer to the prototype object.
Let’s look at the code.
function Parent1() {
this.name = 'parent1';
this.play = [1.2.3]}function Child1() {
this.type = 'child2';
}
Child1.prototype = new Parent1();
console.log(new Child1());
Copy the code
The above code appears to be fine, although the methods and attributes of the parent class are accessible, but there is a potential problem, and I’ll give you another example to illustrate the problem.
var s1 = new Child1();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play);
Copy the code
After executing this code on the console, you can see the following result:
I only changed the play property of S1, so why did S2 change too? The reason is simple, because both instances use the same stereotype object. Their memory space is shared, and when one changes, the other changes, which is a disadvantage of using prototype chain inheritance.
To solve this problem, we need to look at other inheritance methods. Let’s look at a second approach to solving the stereotype property sharing problem.
Second: Constructor inheritance (with call)
Look directly at the code, as shown below.
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()); / / complains
Copy the code
By executing the code above, you get something like this.
- You can see that the last printed child is displayed on the console, inheriting the Parent1 attribute name in addition to the type attribute of Child1. In this way, the subclass can get the attribute values of the parent class, which solves the drawbacks of the first method of inheritance. However, the problem is that once there are methods defined by the parent class in the prototype object, the subclass cannot inherit those methods. The console execution result in this case is shown below.
Therefore, the above results show the advantages and disadvantages of the constructor implementation inheritance. It makes the reference attributes of the parent class not shared, which optimizes the disadvantages of the first method of inheritance. However, there is an obvious disadvantage that only instance properties and methods of the parent class can be inherited, not stereotype properties or methods.
The above two inheritance methods have their own advantages and disadvantages, so combining the advantages of the two, the following combination inheritance method is generated.
Third: Combinatorial inheritance (the first two combinations)
This method combines the advantages and disadvantages of the previous 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() for the second time
Parent3.call(this);
this.type = 'child3';
}
Call Parent3() for the first time
Child3.prototype = new Parent3();
// Hang the constructor manually and point to your own constructor
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // Do not affect each other
console.log(s3.getName()); // Normal output 'parent3'
console.log(s4.getName()); // Normal output 'parent3'
Copy the code
By executing the code above, you can see the output from the console, and the previous problems with method one and method two have been resolved.
But here’s a new problem: Parent3 (Child3, Child3, Child3); Parent3 (Child3, Child3, Child3, Child3, Child3); This is not what we want to see.
So is there a better way to solve this problem? Read on for a sixth inheritance method to better solve this problem.
The above is more around constructors, so how do you implement inheritance for ordinary objects in JavaScript?
I have to mention the object.create method in ES5, which takes two parameters: an Object to be used as a prototype for the new Object, and an Object (optional) to define additional properties for the new Object.
Let’s take a look at how inheritance is implemented for ordinary objects through a piece of code.
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);
Copy the code
As you can see from the above code, the Object. Create method can inherit from ordinary objects, not only the attributes, but also the getName method.
-
The first result, “Tom,” is easier to understand, and person4 inherits the parent4 name attribute, but customizes it from there.
-
The second is the inherited getName method that checks if its name is the same as the value in the property, and the answer is true.
-
The third result, “parent4,” is also easier to understand. Person5 inherits the name attribute of parent4 without overwriting it, so it prints the attributes of the parent object.
-
The Object. Create method can be used to implement shallow copies of some objects.
Then the disadvantages of this method of inheritance are also obvious. Multiple instances of reference type attribute point to the same memory, there is the possibility of tampering. Next, let’s take a look at another inheritance method optimized on the basis of this inheritance — parasitic inheritance.
Fifth: parasitic inheritance
The method of inheritance is called parasitic inheritance, in which a shallow copy of the target object is obtained using the original inheritance, and then the power of the shallow copy is enhanced by adding some methods.
Although it has the same advantages and disadvantages as original inheritance, parasitic inheritance still adds more methods to the parent class than original inheritance for common object inheritance. So let’s see how the code works.
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());
Copy the code
From the code above, we can see that person5 is an instance generated by parasitic inheritance, and not only does it have a getName method, but it also has a getFriends method at the end, as shown in the figure below.
As you can see from the final output, person5 uses the Clone method to add the getFriends method, thus adding another method to the generic object’s inheritance process, which is called parasitic inheritance.
Parasitic combinatorial inheritance addresses some of the drawbacks I mentioned in the third combinatorial inheritance approach above, namely the waste of calling the parent class’s constructor twice.
The sixth kind: parasitic combinatorial inheritance
Combined with the inheritance method mentioned in the fourth method and the Object. Create method to solve the inheritance problem of common objects, we reformed the advantages and disadvantages of the previous inheritance methods and obtained the parasitic combination inheritance method, which is also the relatively optimal inheritance method among all inheritance methods. The code is as follows.
function clone (parent, child) {
// Use object.create to reduce the need for multiple constructs 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());
Copy the code
It can be seen from this code that this parasitic combinatorial inheritance method can basically solve the shortcomings of the previous inheritance methods, better achieve the desired results of inheritance, but also reduce the number of construction, reduce the cost of performance. Let’s take a look at the execution results of the above code.
You can see the result printed by Person6, where the properties are inherited and the methods are fine, and the expected results are printed.
On the whole, parasitic combinatorial inheritance is the best inheritance among the six inheritance modes. In addition, ES6 provides the extends keyword for inheritance, so let’s take a look at the logic behind extends that implements inheritance.
We can take advantage of the extends sugar in ES6. It’s easy to implement JavaScript inheritance directly using keywords, but if you want to learn more about how the extends sugar is implemented, You have to dig deep into the underlying logic of extends.
Let’s take a look at how inheritance can be implemented directly using extends.
class Person {
constructor(name) {
this.name = name
}
// Prototype method
Prototype. GetName = function() {}
GetName () {... }
getName = function () {
console.log('Person:'.this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// If there is a constructor in a subclass, you need to call super() first before using "this".
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna'.20)
asuna.getName() // Successfully access the method of the parent class
Copy the code
Because of browser compatibility issues, if you encounter a browser that does not support ES6, you will need to use Babel to compile ES6 code into ES5, so that some browsers that do not support the new syntax can run.
So what does extends look like in the end? Let’s take a look at the translated code snippet.
function _possibleConstructorReturn (self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function')? call : self; }function _inherits (subClass, superClass) {
// It can be seen here
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false.writable: true.configurable: true}});if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Parent = function Parent () {
// Verify if Parent constructs this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child () {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this.arguments));
}
return Child;
}(Parent));
Copy the code
As can be seen from the compiled source code above, it also adopts the parasitic combination inheritance method, so it also proves that this method is a better way to solve inheritance.
Object. Create is used to divide the different inheritance methods. Finally, parasitic combinatorial inheritance is the optimal inheritance method modified by combinatorial inheritance.
To sum up, we can see that different inheritance methods have different advantages and disadvantages, and we need to have a thorough understanding of the advantages and disadvantages of each method, so that in daily development, we can choose the inheritance method most suitable for the current scenario.
Development, in the front of the daily work, developers tend to ignore the relevant systemic study for inheritance, but because of the inheritance method is more, the method to realize each detail is scattered, many developers are difficult to have a systematic, overall understanding, cause inefficient, and the code is difficult to further improve.