preface
Inheritance is one of the most discussed topics in object-oriented programming. Many object-oriented languages support two types of inheritance: interface inheritance, which is inheritance of function signatures, and implementation inheritance, which is inheritance of actual methods. Interface inheritance is not possible in ECMAScript because functions are not signed. As a result, ECMAScript only supports implementation inheritance, which is mainly implemented through prototype chains.
This description is taken from JavaScript Advanced Programming (4th edition) and follows the beginning of the chapter. At the same time, a lot of the content in this article is self-reference, also can be regarded as personal reading notes and summary
There are many different ways of inheritance in JavaScript, including prototype chain inheritance, embeded constructor inheritance, prototype inheritance, parasitic inheritance, and parasitic combinatorial inheritance. With the advent of ECMAScript 2015, also known as ES6, a new way of defining classes has been introduced, along with the extends keyword
In this article, we’ll look at inheritance in JavaScript in more detail
Prototype chain inheritance
Pre-knowledge point
When it comes to inheritance in JavaScript, there is only one structure that can implement this feature: objects. Every object in JavaScript has an internal pointer [[Prototype]] (or private __proto__, now removed from the Web standard) to its Prototype, which is essentially an object with its own Prototype object. This nesting of layers creates a chain of archetypes, which is the key to JavaScript inheritance. Null, by definition, has no prototype, so it is the last link in the prototype chain
When a JavaScript object accesses its properties, it will not only look for the object itself, but also search up the prototype chain layer by layer until it finds or reaches the end of the prototype chain and fails to find an error
implementation
Combining the above two features, we can easily imagine how prototype chain inheritance is implemented:
Set the prototype of the subclass to an instance of the parent class, so that the instance of the subclass can access the attributes of the parent class prototype through the constructed prototype chain, using the code described as follows:
function SuperType() {
this.property = 5
}
SuperType.prototype.getSuperValue = function () {
console.log("SuperValue is: " + this.property);
}
function SubType() {
this.subproperty = 10
}
// Inherits SuperType and sets SubType as an instance of SuperType
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
// Adding a property or method to a stereotype must be done after the stereotype is set, otherwise it will be overwritten
SubType.prototype.getSubValue = function() {
console.log("SubValue is: " + this.subproperty);
}
const instance = new SubType()
instance.getSuperValue() // SuperValue is: 5
instance.getSubValue() // SubValue is: 10
Copy the code
In the above code, we create a SuperType class and a SubType class, and set the stereotype of SubType to an instance of SuperType and reset the constructor on the overridden SubType stereotype to a SubType function. The diagram between the two classes and the last instance created is as follows:
As you can see from the figure, since the Prototype of SubType has been replaced with an instance of the SuperType class, properties on the SuperType Prototype can be accessed through the internal pointer [[Prototype]]. Also, since it is an instance of SuperType class, the SubType prototype will contain the property property of SuperType. If the property is assigned, the property property will be created and assigned on the instance. Property on the prototype is not modified
Existing problems
This method can implement the inheritance of the parent class method well, but there are some problems in inheriting the reference attributes of the parent class. From the above we can see that the instance attributes of the parent class become the stereotype attributes of the child class when inherited, while the reference attributes of the stereotype are shared by all instances. In most cases we do not want this, we want each instance to have its own reference instance, for example:
There is a teacher class, contains a property students to represent the students, and then a math teacher class inherits this teacher class, by this math teacher class created two instances of math teacher class 1, math teacher class 2, then they need to share the property of students. But the two teachers are not in the same class, which leads to confusion
A second problem with stereotype chain inheritance is that subclasses cannot pass arguments to the constructor of their parent class when instantiating, because doing so is equivalent to modifying the stereotype directly, affecting all instances. Combined with the problem of reference attributes above, the result is that stereotype chain inheritance is rarely used alone
Embezzled constructors
To solve the aforementioned inheritance problem of reference attributes, a technique of stealing constructors (also known as object spoofing or classical inheritance) was proposed. The basic idea is to call the parent class’s constructor from the subclass’s constructor. Because the constructor is essentially a function, you can add attributes to the created instance object by assigning the parent constructor to execute in the context of the newly created instance object using call() or apply(). The specific implementation code is as follows:
function SuperType() {
this.colors = ['red'.'green'.'blue']}function SubType() {
// embezzle the constructor
SuperType.call(this)}const instance1 = new SubType()
instance1.colors.push('black')
const instance2 = new SubType()
instance2.colors.push('white')
const instance3 = new SubType()
console.log(instance1) // SubType { colors: [ 'red', 'green', 'blue', 'black' ] }
console.log(instance2) // SubType { colors: [ 'red', 'green', 'blue', 'white' ] }
console.log(instance3) // SubType { colors: [ 'red', 'green', 'blue' ] }
Copy the code
In effect, the parent constructor is called every time an instance is created through new, adding attributes to the instance so that each instance has its own attributes and does not interfere with each other
In addition, the stolen constructor solves the problem of not passing arguments to the parent constructor, as shown in the following example:
function SuperType(name) {
this.name = name
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}
const instance = new SubType('Joe'.21)
console.log(instance) // SubType {name: 'zhang3 ', age: 21}
Copy the code
Existing problems
The main disadvantage of a stolen constructor is that it does not involve the stereotype, so you cannot access the properties and methods defined on the parent stereotype. If you want to implement an inherited parent method, you have to define the method on the parent constructor, so the function cannot be reused. Because of the above, the method of stealing constructors is not used alone
Combination of inheritance
Composite inheritance, also called pseudo-classical inheritance, is a combination of prototype chain inheritance and stolen constructor. The basic idea is: when the constructor calls the parent constructor, the prototype is set as the instance of the parent class. In this way, you can create subclass instances that have access to properties and methods on the parent class prototype, while giving each instance its own properties
The code implementation is roughly as follows:
function SuperType(name) {
this.colors = ['red'.'green'.'blue']
this.name = name
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, age) {
// steal the constructor and inherit the attributes
SuperType.call(this, name)
this.age = age
}
// Set the prototype of SubType to an instance of SuperType
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
// Adding a property or method to a stereotype must be done after the stereotype is set, otherwise it will be overwritten
SubType.prototype.sayAge = function() {
console.log(this.age)
}
const instance1 = new SubType('Joe'.21)
const instance2 = new SubType('bill'.23)
instance1.colors.push('black')
console.log(instance1.colors) // [ 'red', 'green', 'blue', 'black' ]
instance1.sayName() / / zhang SAN
instance1.sayAge() / / 21
console.log(instance2.colors) // [ 'red', 'green', 'blue' ]
instance2.sayName() / / li si
instance2.sayAge() / / 23
Copy the code
Combinatorial inheritance makes up for each other’s shortcomings by using the advantages of prototype chain inheritance and stealing constructors. Combinatorial inheritance also preserves the instanceof operator and isPrototypeOf() method to identify whether the class is inherited from:
// add the combination inheritance code
instance1 instanceof SubType // true
instance1 instanceof SuperType // true
Copy the code
Existing problems
Composite inheritance is the most used inheritance pattern in JavaScript, but it is not perfect. It also has performance problems. The main efficiency problem is that the superclass constructor is always called twice, once in the constructor of the subclass when the instance of the subclass is created, and once when the prototype of the subclass is created
This problem is not unsolvable, and the specific causes and solutions of this problem will be mentioned in detail later
Primary inheritance
The Prototypal Inheritance here is different from the Prototypal chain Inheritance proposed by Douglas Crockford in his 2006 article Prototypal Inheritance in JavaScript. It is an inheritance approach that does not involve strictly constructors, and the starting point is that information can be shared between objects through stereotypes, even without custom types.
For those interested, read Douglas Crockford here
Douglas Crockford gives a function like this in his article:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
Copy the code
This function creates a temporary constructor F(), takes the passed object to be inherited as a prototype of F(), and returns an instance of the temporary type, essentially object() being a shallow copy of the passed object. This method is suitable for creating an object in addition to an existing one. Here’s an example:
Let person = {name: 'color ', colors: ['red', 'green', 'blue']} let anotherPerson = object (person) anotherPerson. Name = 'bill' anotherPerson. The colors. The push let (' black ') YetAnotherPerson = object (person) yetAnotherPerson. Name = 'Cathy' yetAnotherPerson. Colors. Push (' white ') console.log(person.colors) // ["red", "green", "blue", "black", "white"] person.colors === anotherPerson.colors // true person.colors === yetAnotherPerson.colors // true anotherPerson.colors === yetAnotherPerson.colors // trueCopy the code
In this example, you can see that the original object and the two new objects created from it share the same reference type colors array, which is actually a shallow copy of Person twice
ECMAScript5 normalized the concept of type inheritance by adding the Object.create() method. This method takes two arguments, the first argument proto, which is the Object to be used as the prototype for the new Object, and the second argument propertiesObject, which is an optional Object to set additional properties to the new Object. See the second argument to Object.defineProperty(). An attribute defined in this way overshadows an attribute of the same name on the stereotype
Here’s an example:
let person = {
name: 'Joe'.colors: ['red'.'green'.'blue']}let anotherPerson = Object.create(person)
anotherPerson.name = 'bill'
anotherPerson.colors.push('black')
let yetAnotherPerson = Object.create(person, {
name: {
value: 'Cathy'
}
})
yetAnotherPerson.colors.push('white')
console.log(person.colors) // ["red", "green", "blue", "black", "white"]
Copy the code
Primitive inheritance is ideal for situations where you don’t need to create separate constructors, but you need to share information between objects
Existing problems
As mentioned earlier, the old object and the two new objects created from it share the same reference type, just like when using stereotype chain inheritance
Parasitic inheritance
Parasitic inheritance is a close approximation of native inheritance, and the idea behind it is similar to the parasitic constructor and factory pattern: Create a function that implements inheritance, enhance objects in some way, and then return objects. This is also a pattern pioneered by Crockford. The basic parasitic inheritance pattern code is implemented as follows:
function createAnother(original) {
// Create a new object based on Original
let clone = Object.create(original)
// Enhance the new object created
clone.sayHi = function() {
console.log('hi')}return clone
}
let person = {
name: 'Joe'.sayName() {
console.log(this.name)
}
}
let anotherPerson = createAnother(person)
anotherPerson.sayHi() // hi
anotherPerson.sayName() / / zhang SAN
Copy the code
This method of inheritance also does not consider the type or constructor, but inherits from the original object. At the same time, because new functions are added to the object, the new function is difficult to reuse, which is similar to stealing the constructor pattern
Parasitic combinatorial inheritance
Parasitic composite inheritance can be regarded as the best mode of reference type inheritance. It solves the problem that composite inheritance will call the parent constructor twice and improves the efficiency of code execution.
Combinatorial inheritance efficiency problem
In order to understand how parasitic combinatorial inheritance solves the efficiency problem of combinatorial inheritance, we first analyze the causes of the efficiency problem of combinatorial inheritance
Review the code implementation of composite inheritance:
function SuperType(name) {
this.colors = ['red'.'green'.'blue']
this.name = name
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, age) {
// Call SuperType() for the second time
SuperType.call(this, name)
this.age = age
}
// Call SuperType() for the first time
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function() {
console.log(this.age)
}
const instance = new SubType('Joe'.21)
Copy the code
The code above calls the superclass constructor SuperType() the first time when the SubType prototype is set, and then the second time when the constructor is invoked when the SubType instance is created. Now start to parse the changes in the prototype chain step by step in this process:
- In the beginning, subtypes are just like normal functions, with their prototype:
- Set the stereotype of SubType to an instance of SuperType
SubType.prototype = new SuperType
Add sayAge method to SubType prototype:
- When creating an instance of SubType, call the parent class constructor to add its own attributes inherited from the parent class to the instance:
From the above pictures, it can be seen intuitively that:
On the same instance, there are two sets of name and age attributes, one on the instance itself and the other on the stereotype, and this set of attributes on the stereotype is not necessary or needed, which is the result of calling the superclass constructor twice
Implementation of parasitic composite inheritance
The basic idea of parasitic combination inheritance is as follows:
- For superclass attributes, use the superclass constructor again
- For the inheritance of the parent prototype, the method of parasitic inheritance is adopted, that is, take a copy of the parent prototype, use it to create a new object, and then set the new object returned as the prototype of the child class, and then add methods to enhance it
The specific code is as follows:
function SuperType(name) {
this.colors = ['red'.'green'.'blue']
this.name = name
}
SuperType.prototype.sayName = function() {
console.log(this.name)
}
function SubType(name, age) {
// Call SuperType() only once
SuperType.call(this, name)
this.age = age
}
// Parasitic inheritance is used here:
// Returns a new object modeled after supertype. prototype, enhanced later by adding methods
SubType.prototype = Object.create(SuperType.prototype)
// Fixed default constructor missing when replacing prototypes
SubType.prototype.constructor = SubType
// The Object returned by object.create is used as a prototype, so there is no problem that the function cannot be reused
SubType.prototype.sayAge = function() {
console.log(this.age)
}
const instance = new SubType('Joe'.21)
Copy the code
The above code only executes the parent constructor once, thus avoiding unnecessary attributes on subtype.prototype and improving efficiency. The prototype chain remains the same. You can use the instanceof operator or isPrototypeOf() to test whether the object inherits from the class
The succession of ES6
The inheritance methods described above only use ECMAScript5’s features, but the code to implement inheritance is verbose or problematic. In ECMAScript6, the class keyword was introduced to give JavaScript the ability to formally define classes (for those unfamiliar with ES6 classes). Classes defined using the class keyword can use the extends keyword to extend any object that has a Constructor ([[Constructor]]) and a Prototype ([[Prototype]])
In addition, the super keyword is added to refer to the stereotype of the parent class, the super constructor can be called in the constructor, and the static method defined on the inherited class can be called through super in the static method. This keyword can only be used in derived classes
class SuperType {
constructor(name) {
this.colors = ['red'.'green'.'blue']
this.name = name
}
sayNamen() {
console.log(this.name)
}
// Define static methods
static staticMethod() {
console.log('This is a staticMethod')}}class SubType extends SuperType {
constructor(name, age) {
// Do not call super again before referring to this, since super is equivalent to returning an instance of the parent class and assigning it to this
super(name) // equivalent to super.constructor()
this.age = age
}
sayAge() {
console.log(this.age)
}
static test() {
// Call a static method defined on the parent class
super.staticMethod()
}
}
const instance = new SubType('Joe'.21)
instance.sayName() // "/"
instance.test() // "This is a staticMethod"
Copy the code
Abstract base class
Abstract base classes are classes that can be inherited by other classes but cannot be instantiated themselves.
The class or function called by the new keyword can be obtained via new.target, so you can use this feature to detect whether new.target is an abstract base class at instantiation time, and to prevent instantiation if so:
class AbstractClass {
constructor() {
if(new.target == AbstractClass) {
throw new Error('Abstract base class cannot be instantiated')}}}class SubClass extends AbstractClass{}
new SubClass() // SubClass {}
new AbstractClass() // "The abstract base class cannot be instantiated"
Copy the code
Multiple inheritance
ES6 does not explicitly support multi-class inheritance, but we can model this behavior with existing features
The extends keyword can be followed by an expression that is valid as long as the result of the expression is a class that can be inherited. Suppose A Person class inherits classes A, B, and C, then you need to implement A process where B inherits from A, C inherits from B, and Person inherits from C. This approach is called blending
The code implementation is as follows:
class A {
foo() {
console.log('foo')}}// This is a function that returns a class that inherits the passed argument SuperClass
let B = (SuperClass) = > class extends SuperClass {
bar() {
console.log('bar')}}let C = (SuperClass) = > class extends SuperClass {
baz() {
console.log('baz')}}The result is a class that inherits all the parameters passed in
function mixin(BaseClass, ... Mixins) {
return Mixins.reduce((accumulator, current) = > current(accumulator), BaseClass)
}
class Person extends mixin(A.B.C) {}
Copy the code
Write in the last
Because JavaScript is based on the Prototype object-oriented system, the essence of inheritance is actually objects through the internal [[Prototype]] chain association, and then through the “delegate” data sharing. When no corresponding property or method is requested on an object, the request is delegated to another object on the prototype chain. Classes are an optional design pattern. JavaScript doesn’t even have classes, only objects. ES6’s new class keyword is just syntactic sugar
The above points are based on my reading of other books, and I think that understanding these principles will help us think about why inheritance is used, when inheritance is used, and how inheritance is used, as well as deepen our understanding of JavaScript