├──Class 的继承
│ ├── 简介
│ │ └─ Class 可以通过`extends`关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。
│ │ └─ `super`在这里表示父类的构造函数,用来新建一个父类的实例对象。
│ │ └─ ES6 规定,子类必须在`constructor()`方法中调用`super()`,否则就会报错。这是因为子类自己的`this`对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用`super()`方法,子类就得不到自己的`this`对象。
│ │ └─ 为什么子类的构造函数,一定要调用`super()`?原因就在于 ES6 的继承机制,与 ES5 完全不同。ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用`super()`方法,因为这一步会生成一个继承父类的`this`对象,没有这一步就无法继承父类。
│ │ └─ 子类无法继承父类的私有属性,或者说,私有属性只能在定义它的 class 里面使用。
│ │ └─ `hello()`是`A`类的静态方法,`B`继承`A`,也继承了`A`的静态方法。
│ │ └─ 如果父类定义了私有属性的读写方法,子类就可以通过这些方法,读写私有属性。
│ ├── Object.getPrototypeOf()
│ │ └─ `Object.getPrototypeOf()`方法可以用来从子类上获取父类。
│ │ └─ 因此,可以使用这个方法判断,一个类是否继承了另一个类。
│ ├── super 关键字
│ │ └─ `super`这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
│ │ └─ 第一种情况,`super`作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次`super`函数。
│ │ └─ 注意,`super`虽然代表了父类`A`的构造函数,但是返回的是子类`B`的实例,即`super`内部的`this`指的是`B`的实例,因此`super()`在这里相当于`A.prototype.constructor.call(this)`。
│ │ └─ `super`作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
│ │ └─ ES6 规定,在子类普通方法中通过`super`调用父类的方法时,方法内部的`this`指向当前的子类实例。
│ │ └─ 由于`this`指向子类实例,所以如果通过`super`对某个属性赋值,这时`super`就是`this`,赋值的属性会变成子类实例的属性。
│ │ └─ 如果`super`作为对象,用在静态方法之中,这时`super`将指向父类,而不是父类的原型对象。
│ │ └─ `super`在静态方法之中指向父类,在普通方法之中指向父类的原型对象。另外,在子类的静态方法中通过`super`调用父类的方法时,方法内部的`this`指向当前的子类,而不是子类的实例。
│ │ └─ `console.log(super)`当中的`super`,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明`super`的数据类型,就不会报错。
│ │ └─
│ ├── 类的 prototype 属性和_ _proto_ _属性
│ │ └─ 大多数浏览器的 ES5 实现之中,每一个对象都有`__proto__`属性,指向对应的构造函数的`prototype`属性。Class 作为构造函数的语法糖,同时有`prototype`属性和`__proto__`属性,因此同时存在两条继承链。
│ │ └─ 子类的`__proto__`属性,表示构造函数的继承,总是指向父类。
│ │ └─ 子类`prototype`属性的`__proto__`属性,表示方法的继承,总是指向父类的`prototype`属性。
│ │ └─
│ ├── 实例的 _ _proto_ _ 属性
│ │ └─ 子类实例的`__proto__`属性的`__proto__`属性,指向父类实例的`__proto__`属性。也就是说,子类的原型的原型,是父类的原型。
│ │ └─ 通过子类实例的`__proto__.__proto__`属性,可以修改父类实例的行为。
│ ├── 原生构造函数的继承
│ │ └─ 原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
│ │ └─ - Boolean() - Number() - String() - Array() - Date() - Function() - RegExp() - Error() - Object()
│ │ └─ 之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过`Array.apply()`或者分配给原型对象都不行。原生构造函数会忽略`apply`方法传入的`this`,也就是说,原生构造函数的`this`无法绑定,导致拿不到内部属性。
│ │ └─ ES5 是先新建子类的实例对象`this`,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,`Array`构造函数有一个内部属性`[[DefineOwnProperty]]`,用来定义新属性时,更新`length`属性,这个内部属性无法在子类获取,导致子类的`length`属性行为不正常。
│ │ └─ ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象`this`,然后再用子类的构造函数修饰`this`,使得父类的所有行为都可以继承。
│ │ └─ ES6 可以自定义原生数据结构(比如`Array`、`String`等)的子类,这是 ES5 无法做到的。
│ │ └─ `NewObj`继承了`Object`,但是无法通过`super`方法向父类`Object`传参。这是因为 ES6 改变了`Object`构造函数的行为,一旦发现`Object`方法不是通过`new Object()`这种形式调用,ES6 规定`Object`构造函数会忽略参数。
│ ├── Mixin 模式的实现
│ │ function mix(...mixins) {
│ │ class Mix {
│ │ constructor() {
│ │ for (let mixin of mixins) {
│ │ copyProperties(this, new mixin());
│ │ }
│ │ }
│ │ }
│ │
│ │ for (let mixin of mixins) {
│ │ copyProperties(Mix, mixin);
│ │ copyProperties(Mix.prototype, mixin.prototype);
│ │ }
│ │
│ │ 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);
│ │ }
│ │ }
│ │ }
│ │