JS语言理解04 继承的总结

继承相关知识还是有点乱,来总结一下。

本质上,JS里面的继承都是通过原型链实现的(除了实例属性之外),原型链继承的关键就是对象的__proto__属性,它应该指向另外一个对象的prototype

构造函数的继承

ES5里面最常用的继承方法是以下两种:

  1. 在子类内部执行父类(call/apply改变this),继承实例属性
  2. 子类的prototype等于父类的一个实例(new 父类),继承实例属性和原型属性

ES6里面使用classextends实现的继承

下面具体来看(下面提到的属性,除非特殊说明,都是泛指属性和方法)

继承实例属性

在子类的内部让父类调用call或者apply的方法,这样只能继承父类的实例属性,不能继承原型上的属性。

1
2
3
function Child() {
Father.call(this)
}

继承原型属性

(1)直接让子类的原型等于父类的原型:

1
2
Child.prototype = Father.prototype;
Child.prototype.constructor = Child;

(2)利用Object.setPrototypeOf方法

使用Object.setPrototypeOf方法来让指定Child.prototype.__proto__等于Father.prototype,实现了原型属性的继承(相当于new过程的一部分)

1
Object.setPrototypeOf(Child.prototype, Father.prototype);

(3)利用中间函数

第三种种方法是对第一种方法的改进,避免子类的原型更改影响到父类的原型,利用了一个中间函数:

1
2
3
4
const Temp = function(){};
Temp.prototype = Father.prototype;
Child.prototype = new Temp()
Child.prototype.constructor = Child;

(4)通过父类的原型对象进行遍历、拷贝,从而实现继承:

1
2
3
for (const i in Parent.prototype) {
Child.prototype[i] = Parent.prototype[i]
}

同时继承实例属性和原型属性

(1)让子对象的原型成为父对象的实例:

1
2
Child.prototype = new Father();
Child.prototype.constructor = Child;

(2)通过ES6中的方法classextends实现继承:

1
2
3
4
5
6
7
8
class Father {
}

class Child extends Father {
constructor() {
super();
}
}

要注意的是,class只能在原型链上定义方法,所以也只能继承原型方法,而不能继承原型属性。

非构造函数的继承

让一个对象去继承一个不相关对象,由于这两个对象都是普通对象,不是构造函数,所以无法使用构造函数方法实现继承。

通过Object.create实现

Object.create方法是直接通过原型,而非模拟类,来实现普通对象(非构造函数)之间的继承

1
const child = Object.create(father, [childDescriptors])

实际上实现的是child.__proto__ === father

通过Object.setPrototypeOf实现

Object.setPrototypeOfObject.create都是更改原型链(设置__proto__属性)的手段,Object.getPrototypeOf(a)是用来获取对象的__proto__属性

1
Object.setPrototypeOf(child, father)

这个方法实现的也是child.__proto__ === father

通过object函数

可以自己编写一个函数,实现上面Object.setPrototypeOf()Object.create()的(部分)功能

1
2
3
4
5
function object(parent) {    
function Child() {}    
Child.prototype = parent;    
return new Child();  
}

实际上,可以将这个方法看做Object.create()的简易的polyfill。

通过拷贝实现

可以父对象进行遍历、拷贝实现

浅拷贝:

1
2
3
4
5
6
7
8
function extendCopy(p) {    
var c = {};    
for (var i in p) {      
c[i] = p[i];    
}    
c.uber = p;    
return c;  
}

深拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
const type = Object.prototype.toString.call(p[i]).match(/\s(.+)\]/)[1].toLowerCase();
switch (type) {
case 'array': {
c[i] = [];
deepCopy(p[i], c[i]);
break;
}
case 'object': {
c[i] = {};
deepCopy(p[i], c[i]);
break;
}
default: {
c[i] = p[i];
break;
}
}
}
return c;
}

关于深拷贝、浅拷贝可以参考以前写过的笔记《JS05 JS中的拷贝》

ES6中的继承

ES6里面使用classextends实现的继承,实际上是ES5中的继承方法的语法糖,它可以实现实例属性、原型方法、静态属性的继承

  1. 类的实例属性是通过class内部的constructor里的super实现的
  2. 类的原型方法是通过Object.setPrototypeOf(Child.prototype, Father.prototype)实现的(
  3. 类的静态属性是通过Object.setPrototypeOf(Child, Father)实现的

使用ES6的继承方法,与ES5中的继承方法相比,不同点主要有以下下几点:

(1)实现的顺序

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。

ES6的继承机制完全不同,子类一开始并没有自己的this,而是需要先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

(2)静态方法和静态属性

ES6的继承方法可以继承父类的静态方法和静态属性,这一点是ES5的继承方法做不到的。

(3)无法继承原型属性,只能继承原型方法

由于在class内部定义在constructor之外的方法实际上都是原型方法,并不能定义原型属性,所以继承的自然也只能是原型方法,而无法继承原型属性

(4)原型方法的可枚举性

由于class内部定义的方法都是不可枚举的,这一点与ES5的行为是不一致的

(5)变量提示

ES5的类是通过函数实现的,存在变量提升;而class不存在变量提升

new的过程

在通过构造函数实例化一个对象(new)的过程中

1
let p = new Person()

发生了以下的过程:

1
2
3
4
5
6
7
8
9
10
11
// 1 新建一个对象
let p = {};

// 2 修改p的__proto__属性,实现原型链的继承
p.__proto__ === Person.protype

// 3 将Person的作用域改为p,也就是说让Person中的this指向p,为p添加实例属性
Person.call(p)

// 4 返回p
return p

new运算的返回值默认返回this,但显式的返回值时,如果返回值的是基本类型,则忽略返回值,仍然返回this,如果返回值是引用类型,则直接返回该返回值作为对象的结果