16 Class 的继承

109 阅读5分钟
├──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);
│   │       }
│   │     }
│   │   }
│   │