Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Inheritance of ES6 Class


May 08, 2021 ES6


Table of contents


1. Introduction

Class can be inherited through 继承 Class keyword, which is much clearer and easier than ES5's implementation of inheritance by modifying the prototype chain. extends

  1. class Point {
  2. }
  3. class ColorPoint extends Point {
  4. }

The code above defines a ColorPoint class that inherits all the properties and methods of the Point class through the extends keyword. H owever, because no code was deployed, the two classes were exactly the same, equivalent to copying a Point class. Below, let's add code inside ColorPoint.

  1. class ColorPoint extends Point {
  2. constructor(x, y, color) {
  3. super(x, y); // 调用父类的constructor(x, y)
  4. this.color = color;
  5. }
  6. toString() {
  7. return this.color + ' ' + super.toString(); // 调用父类的toString()
  8. }
  9. }

In the above code, the super keyword appears in both the constructor method and the toString method, which here represents the constructor of the parent class and is used to create the this object of the parent class.

The sub-class must call the super method in the constructor method, otherwise an error will be reported when the new instance is created. T his is because the child class's own this object must first be shaped through the constructor of the parent class, get the same instance properties and methods as the parent class, and then process it, plus the child class's own instance properties and methods. If you don't call the super method, the sub-class won't get the this object.

  1. class Point { /* ... */ }
  2. class ColorPoint extends Point {
  3. constructor() {
  4. }
  5. }
  6. let cp = new ColorPoint(); // ReferenceError

In the code above, ColorPoint inherits the parent class Point, but its constructor does not call the super method, causing the new instance to report an error.

Inheritance of ES5 essentially creates the instance object this of the child class before adding the method of the parent class to this (Parent.apply(this). ES6's inheritance mechanism is completely different, essentially adding the properties and methods of the parent class instance object to this (so the super method must be called first), and then modifying this with the constructor of the sub-class.

If the sub-class does not define the constructor method, it is added by default, as follows. That is, any sub-class has a constructor method, with or without an explicit definition.

  1. class ColorPoint extends Point {
  2. }
  3. // 等同于
  4. class ColorPoint extends Point {
  5. constructor(...args) {
  6. super(...args);
  7. }
  8. }

Another thing to note is that in the constructor of the sub-class, the this keyword can only be used after the super is called, otherwise an error will be reported. This is because the child class instance is built, and based on the parent class instance, only the super method can call the parent class instance.

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. }
  7. class ColorPoint extends Point {
  8. constructor(x, y, color) {
  9. this.color = color; // ReferenceError
  10. super(x, y);
  11. this.color = color; // 正确
  12. }
  13. }

In the above code, the constructor method of the sub-class uses the this keyword before it is called super, and the result is incorrect, and it is correct to put it after the super method.

Here's the code that generated the sub-class instance.

  1. let cp = new ColorPoint(25, 8, 'green');
  2. cp instanceof ColorPoint // true
  3. cp instanceof Point // true

In the code above, instance object cp is an instance of both ColorPoint and Point classes, which is exactly the same as ES5 behaves.

Finally, the static method of the parent class is also inherited by the child class.

  1. class A {
  2. static hello() {
  3. console.log('hello world');
  4. }
  5. }
  6. class B extends A {
  7. }
  8. B.hello() // hello world

In the above code, hello() is a static method of Class A, and B inherits A, which also inherits the static method of Class A.

2. Object.getPrototypeOf()

Object.getPrototypeOf can be used to get the parent class from a child class.

  1. Object.getPrototypeOf(ColorPoint) === Point
  2. // true

Therefore, you can use this method to determine whether one class inherits another.

3. Super keyword

super keyword super can be used 函数 a function nor as 对象 object. In both cases, it is used in completely different ways.

In the first case, super is called as a function and represents the constructor of the parent class. ES6 requires that the constructor of the sub-class perform the super function once.

  1. class A {}
  2. class B extends A {
  3. constructor() {
  4. super();
  5. }
  6. }

In the code above, super() in the constructor of sub-class B represents the constructor that calls the parent class. This is necessary, otherwise the JavaScript engine will report an error.

Note that super represents the constructor of parent class A, but returns an instance of sub-class B, that is, this inside super refers to an instance of B, so super() is here equivalent to A.prototype.constructor.call (this).

  1. class A {
  2. constructor() {
  3. console.log(new.target.name);
  4. }
  5. }
  6. class B extends A {
  7. constructor() {
  8. super();
  9. }
  10. }
  11. new A() // A
  12. new B() // B

In the code above, new.target points to a function that is currently executing. A s you can see, when super() executes, it points to the constructor of sub-class B, not the constructor of parent class A. That is, this inside super() points to B.

As a function, super() can only be used in the constructors of sub-classes, and errors are reported elsewhere.

  1. class A {}
  2. class B extends A {
  3. m() {
  4. super(); // 报错
  5. }
  6. }

In the above code, super() is used in the m method of Class B, resulting in a syntax error.

In the second case, when super is an object, in the normal method, it points to the prototype object of the parent class, and in a static method, to the parent class.

  1. class A {
  2. p() {
  3. return 2;
  4. }
  5. }
  6. class B extends A {
  7. constructor() {
  8. super();
  9. console.log(super.p()); // 2
  10. }
  11. }
  12. let b = new B();

In the above code, super.p() in sub-class B is used as an object. At this point to A.prototype in the normal method, super.p() is equivalent to A.prototype.p().

It is important to note here that because super points to the prototype object of the parent class, methods or properties defined on the parent class instance cannot be called through super.

  1. class A {
  2. constructor() {
  3. this.p = 2;
  4. }
  5. }
  6. class B extends A {
  7. get m() {
  8. return super.p;
  9. }
  10. }
  11. let b = new B();
  12. b.m // undefined

In the code above, p is the property of the parent class A instance, which super.p does not reference.

If the property is defined on the prototype object of the parent class, super is accessed.

  1. class A {}
  2. A.prototype.x = 2;
  3. class B extends A {
  4. constructor() {
  5. super();
  6. console.log(super.x) // 2
  7. }
  8. }
  9. let b = new B();

In the code above, property x is defined above A.prototype, so super.x can get its value.

ES6 states that when a method of the parent class is called through super in a child class normal method, this inside the method points to the current child class instance.

  1. class A {
  2. constructor() {
  3. this.x = 1;
  4. }
  5. print() {
  6. console.log(this.x);
  7. }
  8. }
  9. class B extends A {
  10. constructor() {
  11. super();
  12. this.x = 2;
  13. }
  14. m() {
  15. super.print();
  16. }
  17. }
  18. let b = new B();
  19. b.m() // 2

In the above code, super.print() calls A.prototype.print(), but the this inside A.prototype.print() points to an instance of sub-class B, resulting in an output of 2 instead of 1. That is, the super.print.call (this) is actually performed.

Because this points to a sub-class instance, if you assign a value to a property through super, super is this, and the assigned property becomes the property of the sub-class instance.

  1. class A {
  2. constructor() {
  3. this.x = 1;
  4. }
  5. }
  6. class B extends A {
  7. constructor() {
  8. super();
  9. this.x = 2;
  10. super.x = 3;
  11. console.log(super.x); // undefined
  12. console.log(this.x); // 3
  13. }
  14. }
  15. let b = new B();

In the code above, super.x is assigned 3, which is equivalent to this.x is assigned 3. And when you read super.x, you read A.prototype.x, so you return undefined.

If super is used as an object in a static method, it points to the parent class, not the prototype object of the parent class.

  1. class Parent {
  2. static myMethod(msg) {
  3. console.log('static', msg);
  4. }
  5. myMethod(msg) {
  6. console.log('instance', msg);
  7. }
  8. }
  9. class Child extends Parent {
  10. static myMethod(msg) {
  11. super.myMethod(msg);
  12. }
  13. myMethod(msg) {
  14. super.myMethod(msg);
  15. }
  16. }
  17. Child.myMethod(1); // static 1
  18. var child = new Child();
  19. child.myMethod(2); // instance 2

In the above code, super points to the parent class in a static method and to the parent's prototype object in the normal method.

In addition, when the method of the parent class is called through super in the static method of the child class, this inside the method points to the current child class, not to the instance of the child class.

  1. class A {
  2. constructor() {
  3. this.x = 1;
  4. }
  5. static print() {
  6. console.log(this.x);
  7. }
  8. }
  9. class B extends A {
  10. constructor() {
  11. super();
  12. this.x = 2;
  13. }
  14. static m() {
  15. super.print();
  16. }
  17. }
  18. B.x = 3;
  19. B.m() // 3

In the code above, static method B.m inside, super.print points to the static method of the parent class. This in this method points to an instance of B, not B.

Note that when using super, you must explicitly specify whether to use it as a function or as an object, or an error will be reported.

  1. class A {}
  2. class B extends A {
  3. constructor() {
  4. super();
  5. console.log(super); // 报错
  6. }
  7. }

In the code above, the super .log in console is not clear whether to use as a function or as an object, so the JavaScript engine reports errors when it parses code. At this point, if the data type of super is clearly indicated, no errors are reported.

  1. class A {}
  2. class B extends A {
  3. constructor() {
  4. super();
  5. console.log(super.valueOf() instanceof B); // true
  6. }
  7. }
  8. let b = new B();

In the code above, super.valueOf() indicates that super is an object, so no errors are reported. At the same time, because super makes this point to an instance of B, super.valueOf() returns an instance of B.

Finally, because objects always inherit other objects, you can use the super keyword in either object.

  1. var obj = {
  2. toString() {
  3. return "MyObject: " + super.toString();
  4. }
  5. };
  6. obj.toString(); // MyObject: [object Object]

4. The class's prototype property and proto property

In most browsers' ES5 implementations, each object has __proto__ property that points to the prototype property of the prototype Class the syntax 语法糖 constructor, with both the prototype property and the proto property, so there are two inheritance chains at the same time.

(1) The proto property of the sub-class, which represents the inheritance of the constructor, always points to the parent class.

(2) The proto property of the child class prototype property, which represents the inheritance of the method and always points to the prototype property of the parent class.

  1. class A {
  2. }
  3. class B extends A {
  4. }
  5. B.__proto__ === A // true
  6. B.prototype.__proto__ === A.prototype // true

In the above code, the proto property of the child class B points to the parent class A, and the proto property of the prototype property of the child class B points to the prototype property of the parent class A.

This results because the inheritance of the class is implemented in the following pattern.

  1. class A {
  2. }
  3. class B {
  4. }
  5. // B 的实例继承 A 的实例
  6. Object.setPrototypeOf(B.prototype, A.prototype);
  7. // B 继承 A 的静态属性
  8. Object.setPrototypeOf(B, A);
  9. const b = new B();

The Extended Object chapter gives an implementation of the Object.setPrototypeOf method.

  1. Object.setPrototypeOf = function (obj, proto) {
  2. obj.__proto__ = proto;
  3. return obj;
  4. }

So you get the results above.

  1. Object.setPrototypeOf(B.prototype, A.prototype);
  2. // 等同于
  3. B.prototype.__proto__ = A.prototype;
  4. Object.setPrototypeOf(B, A);
  5. // 等同于
  6. B.__proto__ = A;

These two inheritance chains make it understandable that, as an object, the prototype of the child class (B) (proto property) is the parent class (A), and as a constructor, the prototype object (prototype property) of the child class (B) is an instance of the prototype object (the prototype property) of the parent class.

  1. B.prototype = Object.create(A.prototype);
  2. // 等同于
  3. B.prototype.__proto__ = A.prototype;

The extends keyword can be followed by several types of values.

  1. class B extends A {
  2. }

A in the code above, as long as it is a function with the prototype property, can be inherited by B. Because functions have prototype properties (except function.prototype functions), A can be any function.

Let's discuss two scenarios. First, the sub-class inherits the Object class.

  1. class A extends Object {
  2. }
  3. A.__proto__ === Object // true
  4. A.prototype.__proto__ === Object.prototype // true

In this case, A is actually a copy of the constructor Object, and an instance of A is an instance of Object.

In the second case, there are no inheritances.

  1. class A {
  2. }
  3. A.__proto__ === Function.prototype // true
  4. A.prototype.__proto__ === Object.prototype // true

In this case, A, as a base class (i.e. without any inheritance), is a normal function, so it inherits Function.prototype directly. H owever, A calls back an empty object (that is, an Object instance), so A.prototype. Proto points to the prototype property of the constructor (Object).

The proto property of the instance

子类 proto property of the proto property of the child class instance, pointing to the proto property of the parent class instance. That is, the prototype of the child class is the prototype of the parent class.

  1. var p1 = new Point(2, 3);
  2. var p2 = new ColorPoint(2, 3, 'red');
  3. p2.__proto__ === p1.__proto__ // false
  4. p2.__proto__.__proto__ === p1.__proto__ // true

In the code above, ColorPoint inherits Point, resulting in the former prototype being the latter prototype.

Therefore, through the proto . Proto property, you can modify the behavior of the parent class instance.

  1. p2.__proto__.__proto__.printName = function () {
  2. console.log('Ha');
  3. };
  4. p1.printName() // "Ha"

The code above adds a method to the Point class on instance p2 of ColorPoint, and the result affects instance p1 of Point.

5. Inheritance of the native constructor

原生构造 that are built into a language and are typically used to 数据结构 EcMAScript's native constructors are roughly the following.

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

Previously, these native constructors could not be inherited, for example, they could not define a sub-class of Array themselves.

  1. function MyArray() {
  2. Array.apply(this, arguments);
  3. }
  4. MyArray.prototype = Object.create(Array.prototype, {
  5. constructor: {
  6. value: MyArray,
  7. writable: true,
  8. configurable: true,
  9. enumerable: true
  10. }
  11. });

The code above defines a MyArray class that inherits Array. However, the behavior of this class is completely inconsistent with Array.

  1. var colors = new MyArray();
  2. colors[0] = "red";
  3. colors.length // 0
  4. colors.length = 0;
  5. colors[0] // "red"

This happens because sub-classes cannot obtain the internal properties of the native constructor, either through Array.apply() or assigned to prototype objects. The native constructor ignores the this passed in by the apply method, which means that the native constructor's this cannot be bound, resulting in no internal properties.

ES5 is an instance object of a new child class, this, and then adds the properties of the parent class to the child class, which cannot inherit the native constructor because the internal properties of the parent class cannot be obtained. For example, the Array constructor has an internal property, DefineOwnProperty, that updates the length property when defining a new property, which cannot be obtained in a sub-class, causing the length property of the sub-class to behave abnormally.

In the following example, we want a normal object to inherit the Error object.

  1. var e = {};
  2. Object.getOwnPropertyNames(Error.call(e))
  3. // [ 'stack' ]
  4. Object.getOwnPropertyNames(e)
  5. // []

In the code above, we want to make the normal object e have instance properties of the Error object by writing error.call(e). H owever, Error.call() completely ignores the first incoming argument and returns a new object, e itself without any change. This proves that Error.call(e) cannot inherit the native constructor.

ES6 allows the inherited native constructor to define the child class, because ES6 is the instance object this that first creates the parent class, and then decorates this with the constructor of the child class, so that all the behavior of the parent class can be inherited. Here is an example of inheriting Array.

  1. class MyArray extends Array {
  2. constructor(...args) {
  3. super(...args);
  4. }
  5. }
  6. var arr = new MyArray();
  7. arr[0] = 12;
  8. arr.length // 1
  9. arr.length = 0;
  10. arr[0] // undefined

The code above defines a MyArray class that inherits the Array constructor so that an instance of the array can be generated from MyArray. This means that ES6 can customize sub-classes of native data structures (such as Array, String, etc.), which ES5 cannot do.

The above example also shows that the extends keyword can be used not only to inherit classes, but also to inherit native constructors. T herefore, you can define your own data structure on the basis of the native data structure. Here's an array with version functionality.

  1. class VersionedArray extends Array {
  2. constructor() {
  3. super();
  4. this.history = [[]];
  5. }
  6. commit() {
  7. this.history.push(this.slice());
  8. }
  9. revert() {
  10. this.splice(0, this.length, ...this.history[this.history.length - 1]);
  11. }
  12. }
  13. var x = new VersionedArray();
  14. x.push(1);
  15. x.push(2);
  16. x // [1, 2]
  17. x.history // [[]]
  18. x.commit();
  19. x.history // [[], [1, 2]]
  20. x.push(3);
  21. x // [1, 2, 3]
  22. x.history // [[], [1, 2]]
  23. x.revert();
  24. x // [1, 2]

In the above code, VersionedArray generates a version snapshot of its current state via the commit method and places it in the history property. T he revert method is used to reset the array to the most recently saved version. In addition, VersionedArray is still a normal array on which all native array methods can be called.

The following is an example of a custom Error sub-class that can be used to customize the behavior when an error is reported.

  1. class ExtendableError extends Error {
  2. constructor(message) {
  3. super();
  4. this.message = message;
  5. this.stack = (new Error()).stack;
  6. this.name = this.constructor.name;
  7. }
  8. }
  9. class MyError extends ExtendableError {
  10. constructor(m) {
  11. super(m);
  12. }
  13. }
  14. var myerror = new MyError('ll');
  15. myerror.message // "ll"
  16. myerror instanceof Error // true
  17. myerror.name // "MyError"
  18. myerror.stack
  19. // Error
  20. // at MyError.ExtendableError
  21. // ...

Note that there is a behavioral difference between inheriting a sub-class of Object.

  1. class NewObj extends Object{
  2. constructor(){
  3. super(...arguments);
  4. }
  5. }
  6. var o = new NewObj({attr: true});
  7. o.attr === true // false

In the code above, NewObj inherits Object, but cannot pass a reference to the parent class Object through the super method. This is because ES6 changes the behavior of the Object constructor, and once it is discovered that the Object method is not called in the form of new Object(), ES6 states that the Object constructor ignores the arguments.

6. Implementation of the Mixin pattern

Mixin to the synthesis of multiple objects into a new object with interfaces for each member. Its simplest implementation is as follows.

  1. const a = {
  2. a: 'a'
  3. };
  4. const b = {
  5. b: 'b'
  6. };
  7. const c = {...a, ...b}; // {a: 'a', b: 'b'}

In the code above, the c object is a object and a composition of the b object, with an interface between the two.

Here's a more complete implementation that "mixes" interfaces from multiple classes into another class.

  1. function mix(...mixins) {
  2. class Mix {
  3. constructor() {
  4. for (let mixin of mixins) {
  5. copyProperties(this, new mixin()); // 拷贝实例属性
  6. }
  7. }
  8. }
  9. for (let mixin of mixins) {
  10. copyProperties(Mix, mixin); // 拷贝静态属性
  11. copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  12. }
  13. return Mix;
  14. }
  15. function copyProperties(target, source) {
  16. for (let key of Reflect.ownKeys(source)) {
  17. if ( key !== 'constructor'
  18. && key !== 'prototype'
  19. && key !== 'name'
  20. ) {
  21. let desc = Object.getOwnPropertyDescriptor(source, key);
  22. Object.defineProperty(target, key, desc);
  23. }
  24. }
  25. }

The mix function of the above code, you can combine multiple objects into one class. When used, simply inherit the class.

  1. class DistributedEdit extends mix(Loggable, Serializable) {
  2. // ...
  3. }