Inheritance plays an important role in various programming languages. Due to the "natural" flexibility of JavaScript, JS urgently needs a reusable and standardized solution in some scenarios. Classes and inheritance naturally appear in the public's vision.
With the continuous in-depth study of JavaScript, we suddenly found that the code used by many people for "inheritance" function is different one day, so there are two questions for us to answer——
- How many implementations are there for JS inheritance?
- Which inheritance method is used to implement the extensions keyword of ES6?
Born in the sky: Extensions of es6
After the release of es6, Jser was surprised to find that a "syntax sugar" - class keyword was added to the JavaScript specification. It largely imitates the specification of process oriented language, including the keyword extends used for class inheritance!
How to use Kangkang first:
class Person { constructor(name) { this.name = name } // The so-called function is the prototype method // The following is equivalent to Person.prototype.getName = function() {} in es5 // It can also be abbreviated as getName() {...} getName = function() { console.log('Person:', this.name) } } class Yxm 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 Yxm('mxc', 18) asuna.getName() // The method of the parent class was successfully accessed
Because this is the ES6 code, it is said that there is a browser compatibility problem. If you encounter a browser that does not support ES6, you have to use babel to compile the ES6 code into es5. (there are also many online ES6 to es5 tools, which are easy to use)
We can also do this to see what the extensions are after escaping:
function _possibleConstructorReturn (self, call) { // ... return call && (typeof call === 'object' || typeof call === 'function') ? call : self; } function _inherits (subClass, superClass) { // You can see it 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 whether this is constructed by Parent _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));
OK, now let's skip one of the details and consider the first question raised at the beginning of the article:
Several ways to implement inheritance in js
- First: prototype chain inheritance
Prototype chain inheritance is one of the earliest and most common inheritance methods. There is a certain relationship among the constructors, prototypes and instances involved: 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!
function Parent_one() { this.name = 'parent'; this.play = [1, 2, 3] } function Child_one() { this.type = 'child2'; } Child_one.prototype = new Parent_one(); console.log(new Child_one());
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.
var s1 = new Child_one(); var s2 = new Child_one(); 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.
- Second: constructor inheritance (with the help of call)
The code is as follows:
function Parent_one() { this.name = 'parent'; } Parent_one.prototype.getName = function() { return this.name; } function Child_one() { Parent_one.call(this); // a key! this.type = 'child' } let child = new Child_one(); 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 first printed child is displayed on the console, except child_ In addition to the attribute type of one, it also inherits parent_ Attribute name of one. In this way, the subclass can get the attribute value of the parent class, which solves the disadvantage of the first inheritance method
However, the problem is that once the methods defined by the parent class (existing in the prototype chain) exist in the parent class prototype object, the child class will not inherit these methods. Just like the error report in the second print above.
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 third: combined inheritance (combining the first two methods)
This method combines the advantages and disadvantages of the first two inheritance methods. The combined inheritance code is as follows.
function Parent_thr() { this.name = 'parent'; this.play = [1, 2, 3]; } Parent_thr.prototype.getName = function() { return this.name; } function Child_thr() { // Call parent for the second time_ thr() Parent_thr.call(this); this.type = 'child'; } // Call parent for the first time_ thr() Child_thr.prototype = new Parent_thr(); // Manually hang up the constructor and point to its own constructor Child_thr.prototype.constructor = Child_thr; var s3 = new Child_thr(); var s4 = new Child_thr(); s3.play.push(4); console.log(s3.play, s4.play); // No interaction console.log(s3.getName()); // Normal output 'parent' console.log(s4.getName()); // Normal output 'parent'
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: we can see the parent through annotation_ THR is executed twice. The first time is to change child_ When using the prototype of thr, the second time is to call parent through the call method_ THR, then parent_ We don't want to see that thr is constructed more than once, which results in more performance overhead.
Moreover, the above description focuses more on the constructor, so 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's see how ordinary objects implement inheritance through a piece of code.
let parent_for = { name: "parent", friends: ["p1", "p2", "p3"], getName: function() { return this.name; } }; let person_for = Object.create(parent_for); person_for.name = "tom"; person_for.friends.push("yxm"); let person_fiv = Object.create(parent_for); person_fiv.friends.push("mxc"); console.log(person_for.name); console.log(person_for.name === person_for.getName()); console.log(person_fiv.name); console.log(person_for.friends); console.log(person_fiv.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, person_for inherits parent_ The name attribute of for, 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 "parent" is also easier to understand, person_fiv inherits the parent_ The name attribute of for is not overwritten, so the attribute of the parent object is output.
The last two output results are the same. This is actually about the "sharing" of reference data types. Most front-end developers should know that the Object.create method can implement shallow copies for some objects.
However, 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.
- 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 based on the parent class than prototype inheritance for the inheritance of ordinary objects:
let parent_fiv = { name: "parent", 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 person_fiv = clone(parent_fiv); console.log(person_fiv.getName()); console.log(person_fiv.getFriends());
Through the above code, we can see person_fiv is an instance generated through parasitic inheritance. It not only has the method of getName, but also has the method of getFriends. The result is shown in the figure below.
As you can see from the final output, person_fiv adds the getFriends method through the clone method to make person_fiv, an ordinary object, adds another method in the inheritance process. This inheritance method is parasitic inheritance.
- Sixth: parasitic combinatorial inheritance (the ultimate solution!)
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 Parent_six() { this.name = 'parent'; this.play = [1, 2, 3]; } Parent_six.prototype.getName = function() { return this.name; } function Child_six() { Parent_six.call(this); this.friends = 'child'; } clone(Parent_six, Child_six); Child_six.prototype.getFriends = function() { return this.friends; } let person_six = new Child_six(); console.log(person_six); console.log(person_six.getName()); console.log(person_six.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 person_ The properties and methods of the results printed by six are inherited, and the expected results can be output!
At this time, looking back at the code escaped by extensions above, we can see from the source code that it also adopts the parasitic combination inheritance method. Does that also mean that this method is a better way to solve inheritance?
It can be seen that the code after es6 escape above is different from that of "parasitic combination"——
- New keyword verification: call_ The classCallCheck method determines whether there is a new keyword before the current function call
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
If there is a new keyword before the constructor is executed, create an empty object inside the constructor, point the proptype of the constructor to the proto of the empty object, and point this to the empty object. As above_ classCallCheck: this instanceof Parent returns true. If there is no new in front of the constructor, the proptype of the constructor will not appear on the prototype chain of this and return false.