Class inheritance

Introduction

Class can be inherited through the extends keyword, which is much clearer and more convenient than ES5's inheritance by modifying the prototype chain.

class Point {}

class ColorPoint extends Point {}

The above code defines a ColorPoint class, which inherits all the attributes and methods of the Point class through the extends keyword. But since there is no code deployed, these two classes are exactly the same, which is equivalent to copying a Point class. Next, we add code inside ColorPoint.

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // call the parent class constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + "" + super.toString(); // call the toString() of the parent class
  }
}

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

The subclass must call the super method in the constructor method, otherwise an error will be reported when creating an instance. This is because the subclass’s own this object must first be shaped by the parent class’s constructor to obtain the same instance properties and methods as the parent class, and then process it, plus the subclass’s own instance properties and method. If the super method is not called, the subclass will not get the this object.

class Point {
  /* ... */
}

class ColorPoint extends Point {
  constructor() {}
}

let cp = new ColorPoint(); // ReferenceError

In the above code, ColorPoint inherits the parent class Point, but its constructor does not call the super method, resulting in an error when creating an instance.

The essence of ES5 inheritance is to first create the instance object this of the subclass, and then add the method of the parent class to this (Parent.apply(this)). The inheritance mechanism of ES6 is completely different. The essence is to first add the attributes and methods of the instance object of the parent class to this (so the super method must be called first), and then use the constructor of the subclass to modify this.

If the subclass does not define the constructor method, this method will be added by default. The code is as follows. In other words, no matter whether it is explicitly defined or not, any subclass has a constructor method.

class ColorPoint extends Point {}

// Equivalent to
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

Another thing to note is that in the subclass's constructor, you can use the this keyword only after calling super, otherwise an error will be reported. This is because the construction of the subclass instance is based on the superclass instance, and only the super method can call the superclass instance.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // correct
  }
}

In the above code, the constructor method of the subclass uses the this keyword before calling super, the result is an error, and it is correct after the super method.

Below is the code to generate an instance of the subclass.

let cp = new ColorPoint(25, 8, "green");

cp instanceof ColorPoint; // true
cp instanceof Point; // true

In the above code, the instance object cp is an instance of the two classes ColorPoint and Point at the same time, which is exactly the same as the behavior of ES5.

Finally, the static methods of the parent class will also be inherited by the child class.

class A {
  static hello() {
    console.log("hello world");
  }
}

class B extends A {}

B.hello(); // hello world

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

Object.getPrototypeOf()

The Object.getPrototypeOf method can be used to get the parent class from the subclass.

Object.getPrototypeOf(ColorPoint) === Point;
// true

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

super keyword

The keyword super can be used as a function or as an object. In these two cases, its usage is completely different.

In the first case, when super is called as a function, it represents the constructor of the parent class. ES6 requires that the constructor of a subclass must execute the super function once.

class A {}

class B extends A {
  constructor() {
    super();
  }
}

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

Note that although super represents the constructor of the parent class A, it returns an instance of the subclass B, that is, this in super refers to an instance of B, so super () is equivalent to A.prototype.constructor.call(this) here.

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A(); // A
new B(); // B

In the above code, new.target points to the currently executing function. As you can see, when super() is executed, it points to the constructor of the subclass B, not the constructor of the parent class A. In other words, this inside super() points to B.

As a function, super() can only be used in the constructor of a subclass, and it will report an error when used in other places.

class A {}

class B extends A {
  m() {
    super(); // error
  }
}

In the above code, super() is used in the m method of the B class, which will cause a syntax error.

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

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

In the above code, super.p() in the subclass B uses super as an object. At this time, super points to A.prototype in ordinary methods, so super.p() is equivalent to A.prototype.p().

It should be noted here that since super points to the prototype object of the parent class, methods or properties defined on instances of the parent class cannot be called by super.

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
bm; // undefined

In the above code, p is the attribute of the parent class A instance, and super.p cannot refer to it.

If the attribute is defined on the prototype object of the parent class, super can get it.

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x); // 2
  }
}

let b = new B();

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

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

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
bm(); // 2

In the above code, although super.print() calls A.prototype.print(), this inside A.prototype.print() points to an instance of subclass B, resulting in The output is 2, not 1. In other words, what is actually executed is super.print.call(this).

Since this points to a subclass instance, if a property is assigned via super, then super is this, and the assigned property will become the property of the subclass instance.

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

In the above code, super.x is assigned a value of 3, which is equivalent to assigning a value of 3 to this.x. When reading super.x, it reads A.prototype.x, so undefined is returned.

If super is used as an object in a static method, then super will point to the parent class instead of the prototype object of the parent class.

class Parent {
  static myMethod(msg) {
    console.log("static", msg);
  }

  myMethod(msg) {
    console.log("instance", msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

In the above code, super points to the parent class in the static method, and points to the prototype object of the parent class in the ordinary method.

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

class A {
  constructor() {
    this.x = 1;
  }
  static print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  static m() {
    super.print();
  }
}

Bx = 3;
Bm(); // 3

In the above code, in the static method Bm, super.print points to the static method of the parent class. The this in this method refers to B, not an instance of B.

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

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super); // error
  }
}

In the above code, the super in console.log(super) cannot be seen whether it is used as a function or as an object, so the JavaScript engine will report an error when parsing the code. At this time, if the data type of super can be clearly indicated, no error will be reported.

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super.valueOf() instanceof B); // true
  }
}

let b = new B();

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

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

var obj = {
  toString() {
    return "MyObject: " + super.toString();
  },
};

obj.toString(); // MyObject: [object Object]

Prototype properties and __proto__ properties of the class

In the ES5 implementation of most browsers, every object has a __proto__ property, which points to the prototype property of the corresponding constructor. As the syntactic sugar of the constructor, Class has both the prototype property and the __proto__ property, so there are two inheritance chains at the same time.

(1) The __proto__ attribute of the subclass indicates the inheritance of the constructor and always points to the parent class.

(2) The __proto__ property of the prototype property of the subclass indicates the inheritance of the method and always points to the prototype property of the parent class.

class A {}

class B extends A {}

B.__proto__ === A; // true
B.prototype.__proto__ === A.prototype; // true

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

This result is because the inheritance of the class is implemented in accordance with the following pattern.

class A {}

class B {}

// The instance of B inherits the instance of A
Object.setPrototypeOf(B.prototype, A.prototype);

// B inherits the static properties of A
Object.setPrototypeOf(B, A);

const b = new B();

The chapter "Object Extensions" gave the implementation of the Object.setPrototypeOf method.

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
};

Therefore, the above result is obtained.

Object.setPrototypeOf(B.prototype, A.prototype);
// Equivalent to
B.prototype.__proto__ = A.prototype;

Object.setPrototypeOf(B, A);
// Equivalent to
B.__proto__ = A;

These two inheritance chains can be understood as follows: as an object, the prototype (__proto__ attribute) of the subclass (B) is the parent (A); as a constructor, the subclass (B) The prototype object (prototype property) is an instance of the parent class's prototype object (prototype property).

B.prototype = Object.create(A.prototype);
// Equivalent to
B.prototype.__proto__ = A.prototype;

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

class B extends A {}

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

Below, two cases are discussed. In the first type, subclasses inherit the Object class.

class A extends Object {}

A.__proto__ === Object; // true
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 is no inheritance.

class A {}

A.__proto__ === Function.prototype; // true
A.prototype.__proto__ === Object.prototype; // true

In this case, A as a base class (that is, there is no inheritance) is an ordinary function, so it directly inherits Function.prototype. However, the call to A returns an empty object (ie, an instance of Object), so A.prototype.__proto__ points to the prototype property of the constructor (Object).

The __proto__ properties of the instance

The __proto__ property of the __proto__ property of the subclass instance points to the __proto__ property of the parent class instance. In other words, the prototype of the prototype of the child class is the prototype of the parent class.

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, "red");

p2.__proto__ === p1.__proto__; // false
p2.__proto__.__proto__ === p1.__proto__; // true

In the above code, ColorPoint inherits Point, causing the prototype of the former to be the prototype of the latter.

Therefore, through the __proto__.__proto__ property of the subclass instance, the behavior of the superclass instance can be modified.

p2.__proto__.__proto__.printName = function () {
  console.log("Ha");
};

p1.printName(); // "Ha"

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

Native constructor inheritance

Native constructor refers to the built-in constructor of the language, which is usually used to generate data structures. The native constructors of ECMAScript are roughly as follows.

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

In the past, these native constructors could not be inherited. For example, you could not define a subclass of Array yourself.

function MyArray() {
  Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true,
  },
});

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

var colors = new MyArray();
colors[0] = "red";
colors.length; // 0

colors.length = 0;
colors[0]; // "red"

This happens because the subclass cannot get the internal properties of the native constructor, either through Array.apply() or assigning to the prototype object. The native constructor ignores the this passed by the apply method, that is, the this of the native constructor cannot be bound, resulting in the inability to get the internal properties.

ES5 is to create a new instance object this of the subclass first, and then add the properties of the parent class to the subclass. Since the internal properties of the parent class cannot be obtained, the native constructor cannot be inherited. For example, the Array constructor has an internal property [[DefineOwnProperty]], which is used to update the length property when defining a new property. This internal property cannot be obtained in the subclass, resulting in the behavior of the length property of the subclass unusual.

In the following example, we want an ordinary object to inherit the Error object.

var e = {};

Object.getOwnPropertyNames(Error.call(e));
// ['stack']

Object.getOwnPropertyNames(e);
// []

In the above code, we want to use Error.call(e) to make the ordinary object e have the instance attributes of the Error object. However, Error.call() completely ignores the first parameter passed in, and instead returns a new object, without any change in e itself. This proves that Error.call(e) cannot inherit the native constructor.

ES6 allows subclasses to be defined by inheriting native constructors, because ES6 first creates an instance object this of the parent class, and then modifies this with the constructor of the subclass, so that all behaviors of the parent class can be inherited. The following is an example of inheriting Array.

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length; // 1

arr.length = 0;
arr[0]; // undefined

The above code defines a MyArray class, which inherits the Array constructor, so an instance of the array can be generated from MyArray. This means that ES6 can customize subclasses of native data structures (such as Array, String, etc.), which is not possible with ES5.

The above example also shows that the extends keyword can be used not only to inherit classes, but also to inherit native constructors. Therefore, you can define your own data structure based on the native data structure. The following is an array with version function defined.

class VersionedArray extends Array {
  constructor() {
    super();
    this.history = [[]];
  }
  commit() {
    this.history.push(this.slice());
  }
  revert() {
    this.splice(0, this.length, ...this.history[this.history.length - 1]);
  }
}

var x = new VersionedArray();

x.push(1);
x.push(2);
x; // [1, 2]
x.history; // [[]]

x.commit();
x.history; // [[], [1, 2]]

x.push(3);
x; // [1, 2, 3]
x.history; // [[], [1, 2]]

x.revert();
x; // [1, 2]

In the above code, VersionedArray will use the commit method to generate a version snapshot of its current state and store it in the history property. The revert method is used to reset the array to the latest saved version. In addition, VersionedArray is still an ordinary array, and all native array methods can be called on it.

The following is an example of a custom Error subclass, which can be used to customize the behavior when an error is reported.

class ExtendableError extends Error {
  constructor(message) {
    super();
    this.message = message;
    this.stack = new Error().stack;
    this.name = this.constructor.name;
  }
}

class MyError extends ExtendableError {
  constructor(m) {
    super(m);
  }
}

var myerror = new MyError("ll");
myerror.message; // "ll"
myerror instanceof Error; // true
myerror.name; // "MyError"
myerror.stack;
// Error
// at MyError.ExtendableError
// ...

Note that there is a [behavior difference](http://stackoverflow.com/questions/36203614/super-does-not-pass-arguments-when-instantiating-a-class-extended-from -object).

class NewObj extends Object {
  constructor() {
    super(...arguments);
  }
}
var o = new NewObj({ attr: true });
o.attr === true; // false

In the above code, NewObj inherits Object, but it cannot pass parameters to the parent class Object through the super method. This is because ES6 has changed the behavior of the Object constructor. Once it is found that the Object method is not called in the form of new Object(), ES6 stipulates that the Object constructor will ignore parameters.

Implementation of Mixin Mode

Mixin refers to the synthesis of multiple objects into a new object, and the new object has the interface of each constituent member. Its simplest implementation is as follows.

const a = {
  a: "a",
};
const b = {
  b: "b",
};
const c = { ...a, ...b }; // {a:'a', b:'b'}

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

Below is a more complete implementation that "mixes" the interfaces of multiple classes into another class.

function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // copy instance properties
      }
    }
  }

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // copy static properties
    copyProperties(Mix.prototype, mixin.prototype); // copy prototype properties
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if (key !== "constructor" && key !== "prototype" && key !== "name") {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

The mix function of the above code can combine multiple objects into one class. When using, just inherit this class.

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