Inheritance plays a crucial role in all kinds of programming languages, and is often used in JavaScript to build the base of the front-end engineering foundation library, which is a key part of JavaScript to learn.
Inheritance enables a subclass to have various methods and attributes of its parent class. ES6 introduced the concept of class to make it easier for us to learn and understand, but class is just a syntactic sugar. The actual underlying implementation is the same as before: inheritance is implemented using prototype chains and constructors. Let’s take a look at the various methods of inheritance in JavaScript.
Prototype chain inheritance
Stereotype chain inheritance is the main method to implement inheritance. The basic idea is to use stereotypes to make one reference type inherit the attributes and methods of another reference type.
This method mainly involves constructors, prototypes and instances, and there is a certain relationship among them:
Each constructor has a prototype property that points to the function’s prototype object; Each stereotype object has a constructor property pointing to the constructor; Each instance has a proto attribute that points to the constructor’s prototype object.
Let’s start with the following code:
function Parent() {
this.name = "parent";
this.interest = ["eat"];
}
function Child() {
this.type = "child";
}
Child.prototype = new Parent();
let child = new Child();
console.log(child.name, child.type);
Copy the code
In the above code, Parent and Child objects are defined, and inheritance is implemented between the two objects by creating an instance of Parent and assigning that instance to Child.prototype. The essence of this method implementation is to override the prototype object.
The parent class attributes and methods can be accessed in an instance of a subclass. The parent class attributes and methods can be accessed in an instance of a subclass.
let child1 = new Child();
child1.interest.push("run");
console.log(child.interest, child1.interest); // [ 'eat', 'run' ] [ 'eat', 'run' ]
Copy the code
In the code above, I created a new subclass child1 and changed the interest property, but the interest property of the original child was also changed.
The reason for this problem is simple: because both instances use the same stereotype object, their memory space is shared. As one changes, so does the other, and this is one of the drawbacks of using archetypal chain inheritance.
Another disadvantage is that there is no way to pass arguments to the constructor of the parent class without affecting all object instances.
Because of the above, prototype-chain inheritance is rarely used in practice alone.
Constructor inheritance
To address the problem of stereotype attribute sharing, developers have begun to use a technique called constructor Stealing, sometimes called forged objects or classical inheritance.
The basic idea behind using a constructor is to use call or apply to copy the properties and methods specified by this from the parent class into instances created by subclasses. Because this object is bound at run time based on the execution environment of the function.
Let’s look at the following code:
function Parent(name) {
this.name = name;
this.interest = ["eat"];
}
Parent.prototype.getName = function () {
return this.name;
};
function Child(name) {
Parent.call(this, name);
this.type = "child";
}
let child1 = new Child('child1');
child1.interest.push("run");
console.log(child1.interest); // [ 'eat', 'run' ]
let child2 = new Child('child2');
console.log(child2.interest); // [ 'eat' ]
console.log(child1.getName()); / / an error
Copy the code
From the results of the above code, this method solves the disadvantages of stereotype chain inheritance, but there is still a problem: once there are methods defined by the parent class on the prototype object, the subclass will not be able to inherit those methods.
We can conclude that constructor implementation inheritance has the following advantages and disadvantages:
- It keeps reference type attributes of the parent class from being shared;
- You can pass parameters to the constructor of the parent class.
Disadvantages:
- Only instance properties and methods of the parent class can be inherited, not stereotype properties or methods.
Combinatorial inheritance (stereotype chain + constructor)
Combination inheritance, also known as pseudo-classical inheritance. It is an inheritance approach that combines the prototype chain with the technique of borrowing constructors, exploiting the best of both.
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.
The code is as follows:
function Parent() {
this.name = "parent";
this.interest = ["eat"];
}
Parent.prototype.getName = function () {
return this.name;
};
function Child() {
Parent.call(this);
this.type = "child type";
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
let child1 = new Child();
let child2 = new Child();
child1.interest.push("run");
console.log(child1.interest, child2.interest); // [ 'eat', 'run' ] [ 'eat' ]
console.log(child1.getName()); // parent
console.log(child2.getName()); // parent
Copy the code
According to the results of code execution, composite inheritance avoids the defects of prototype chain inheritance and constructor inheritance, and combines the advantages of both.
But there is a problem with composite inheritance: in any case, the supertype constructor is called twice. Parent executes twice, first when the Child’s prototype is changed and then when Parent is called through the call method.
The three approaches described above revolve around constructors. How do you implement inheritance if it’s a normal object in JavaScript?
Primary inheritance
The principle of this method is that with the help of prototypes, you can create new objects based on existing objects, saving the step of creating custom types.
function object(o) {
function W(){
}
W.prototype = o;
return new W();
}
Copy the code
Old-style inheritance is normalized by the new object.create () method in ES5, which takes two parameters: an Object to be used as a prototype for the new Object and, optionally, an Object to define additional properties for the new Object.
Let’s take a look at how inheritance is implemented for ordinary objects.
let parent = {
name: "parent".interest: ["eat"].getName: function () {
return this.name; }};let parent1 = Object.create(parent);
parent1.name = "parent1";
parent1.interest.push("sleep");
let parent2 = Object.create(parent);
parent2.name = "parent2";
parent2.interest.push("run");
console.log(parent1.name); // parent1
console.log(parent1.name === parent1.getName()); // true
console.log(parent1.interest); // [ 'eat', 'sleep', 'run' ]
console.log(parent2.name); // parent
console.log(parent2.name === parent2.getName()); // true
console.log(parent2.interest); // [ 'eat', 'sleep', 'run' ]
Copy the code
From the above code you can see that there is a problem with reference type data sharing, because the object.create method can implement shallow copies of some objects.
See my previous article on shallow copying: This time, get to grips with deep and shallow copying of JavaScript
Disadvantages of old-style inheritance: Multiple instances of reference type attributes point to the same memory, which can be tampered with.
Let’s take a look at another type of inheritance that optimizes on top of the original inheritance: parasitic inheritance.
Parasitic inheritance
Parasitic inheritance is an enhanced version of the original type inheritance. The original type inheritance can obtain a shallow copy of the target object and then enhance it by adding some methods. Such inheritance method is called parasitic inheritance.
Parasitic inheritance has the same advantages and disadvantages as original inheritance, but for inheritance of common objects, parasitic inheritance adds more methods on the basis of parent class than original inheritance. Let’s take a look at the implementation code:
let parent = {
name: "parent".interest: ["eat"."run"].getName: function () {
return this.name; }};function clone(original) {
let clone = Object.create(original);
clone.getInterest = function () {
return this.interest;
};
return clone;
}
let parent1 = clone(parent);
console.log(parent1.getName()); // parent
console.log(parent1.getInterest()); // [ 'eat', 'run' ]
Copy the code
As you can see from the code above, parent1 is an instance generated by parasitic inheritance and has both getName and getInterest methods.
Parasitic combinatorial inheritance
In essence, parasitic combination inheritance is an enhanced version of parasitic inheritance. This is the best way to avoid the inevitable two calls to the superclass constructor in composite inheritance, and it is the relatively best of all inheritance methods.
The code is as follows:
function clone(parent, child) {
Use object.create to reduce the need for one more constructor in composite inheritance
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent() {
this.name = "parent";
this.interest = ["eat"."run"];
}
Parent.prototype.getName = function () {
return this.name;
};
function Child() {
Parent.call(this);
this.type = "child type";
}
clone(Parent, Child);
Child.prototype.getInterest = function () {
return this.interest;
};
let child1 = new Child();
let child2 = new Child();
child1.interest.push("sleep");
console.log(child1.getName()); // parent
console.log(child1.getInterest()); // [ 'eat', 'run', 'sleep' ]
console.log(child2.getName()); // parent
console.log(child2.getInterest()); // [ 'eat', 'run' ]
Copy the code
It can be seen from this code that this parasitic combinatorial inheritance 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.
ES6 has a new Class syntax sugar and provides the extends extends keyword. Let’s take a look at the usage of extends and the underlying implementation logic.
ES6 Class inheritance
Class can be inherited through the extends keyword. Subclasses must call the super method from the constructor method or they will get an error when creating a new instance. This is because the subclass’s this object must be molded by the parent’s constructor to get the same instance attributes and methods as the parent, and then processed to add the subclass’s instance attributes and methods. If you don’t call super, your subclasses don’t get this.
In most browsers’ ES5 implementations, each object has a Proto property that points to the prototype property of the corresponding constructor. Class is the syntax sugar of the constructor, and has both prototype and __proto__ attributes, so there are two inheritance chains:
- The __proto__ attribute of a subclass, which indicates constructor inheritance, always points to the parent class;
- The __proto__ attribute of the protoclass’s prototype attribute, which indicates method inheritance, always points to the prototype attribute of the parent class.
class Parent {
constructor(name) {
this.name = name;
}
getName() {
return this.name; }}class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
getAge() {
return this.age; }}let child = new Child("zhangsan".25);
console.log(child.getName());
console.log(child.getAge());
console.log(Child.__proto__ === Parent); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true
Copy the code
During actual project development, Babel was used to compile ES6 code into ES5 due to browser compatibility issues. Let’s take a look at what extends looks like when compiled into an ES5 syntax. Here’s the translated code:
"use strict";
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol= = ="function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol= = ="function" && obj.constructor === Symbol&& obj ! = =Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _instanceof(left, right) { if(right ! =null && typeof Symbol! = ="undefined" && right[Symbol.hasInstance]) { return!!!!! right[Symbol.hasInstance](left); } else { return left instanceofright; }}function _inherits(subClass, superClass) {
if (typeofsuperClass ! = ="function"&& superClass ! = =null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true.configurable: true}});if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this.arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect= = ="undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy= = ="function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean[],function () {})); return true; } catch (e) { return false; }}function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _classCallCheck(instance, Constructor) { if(! _instanceof(instance, Constructor)) {throw new TypeError("Cannot call a class as a function"); }}function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); }}function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
var Parent = /*#__PURE__*/function () {
function Parent(name) {
_classCallCheck(this, Parent);
this.name = name;
}
_createClass(Parent, [{
key: "getName".value: function getName() {
return this.name; }}]);returnParent; } ();var Child = /*#__PURE__*/function (_Parent) {
_inherits(Child, _Parent);
var _super = _createSuper(Child);
function Child(name, age) {
var _this;
_classCallCheck(this, Child);
_this = _super.call(this, name);
_this.age = age;
return _this;
}
_createClass(Child, [{
key: "getAge".value: function getAge() {
return this.age; }}]);return Child;
}(Parent);
Copy the code
As you can see from the compiled code, parasitic combinatorial inheritance is also used, which proves that this approach is a better way to solve inheritance.
If you find this helpful:
1. Click “like” to support it, so that more people can see this article
2, pay attention to the public account: FrontGeek technology (FrontGeek), we learn and progress together.