虽然ES6的Class继承确实很方便,但是ES5的继承还是要好好了解一下:

参考视频:详解JS继承(超级详细且附实例)

预备知识

构造函数的属性

1
2
3
4
5
6
7
function A(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
this.say = function(){ //实例引用属性(该属性,强调复用,需要共享)
console.log('hello');
}
}

注意:数组和方法都属于’实例引用属性’,但是数组强调私有不共享,方法需要复用共享。在构造函数中,很少有数组形式的引用属性,大部分情况都是:基本属性+方法。

在构造函数中,为了属性(实例基本属性)的私有性、方法(实例引用属性)的复用共享,提倡:将属性封装在构造函数中,将方法定义在原型对象上。

修正constructor指向的意义:任何一个prototype对象都有一个constructor属性,指向它的构造函数(它本身),更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。
在new之后,constructor会指向父类构造函数,如果我们要生成子类构造函数的实例,这些实例的constructor属性会指向父类构造函数,然而它们是靠子类构造函数生成的,constructor属性应该指向子类构造函数。因此,不修改constructor指向的话,会导致继承链的紊乱。

(以上来自阮一峰博客,我目前不清楚继承链紊乱会引起什么后果,最起码在我看来,即便不修改constructor指向,好像也没什么影响?)

文档的原作者说:要修复constructor指向,原因是:不能判断子类实例的直接构造函数,到底是子类构造函数还是父类构造函数

JS继承方式

原型链继承

  • 核心:将父类实例作为子类原型
  • 优点:方法复用
    方法定义在父类的原型上,可以复用父类构造函数的方法,比如say方法。
  • 缺点:
    • 创建子类实例时,无法传父类参数
    • 子类实例共享
    • 继承单一,无法实现多继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Parent(name){
this.name = name || '父亲'; 实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
}
Parent.prototype.say = function(){ //将需要复用、共享的方法定义在父类原型上
console.log('hello');
}
function Child(like){
this.like = like;
}
Child.prototype = new Parent(); //核心,但此时Child.prototype.constructor == Parent;
Child.prototype.constructor = Child; //修正constructor指向

let boy1 = new Child();
let boy2 = new Child();

//优点:共享父类构造函数的say方法
console.log(boy1.say(),boy2.say(),boy1.say === boy2.say); //hello,hello,true

//缺点1:不能传入父类的参数(比如name),只能传子类有的参数like
console.log(boy1.name,boy2.name,boy1.name === boy); //父亲,父亲,true

//缺点2:子类实例共享了父类构造函数的引用属性,比如arr属性
boy1.arr.push(2);
console.log(boy2.arr);//[1,2];
//修改了boy1的arr属性,boy2的arr属性也会变化,
//因为两个实例的原型上(Child.prototype)有了父类构造函数的实例属性arr,所以只要修改了boy1.arr,boy2.arr也变化

借用构造函数

  • 核心:借用父类构造函数来增强子类实例,等于是复制父类的实例属性给子类
  • 优点:实例之间独立
    • 创建子类实例,可以向父类构造函数传参
    • 子类实例不共享父类构造函数的引用属性,如arr
    • 可实现多继承(通过多个call或apply继承多个父类)
  • 缺点:
    • 父类方法不能复用
      由于方法在父构造函数中定义,导致方法不能复用(每次创建子类实例都要创建一遍方法)
    • 子类实例继承不了父类原型上的属性,因为没有用到原型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Parent(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; (该属性,强调私有)
this.say = function(){ //实例引用属性(该属性,强调复用,需要共享)
console.log('hello);
}
}
function Child(name,like){
Parent.call(this,name); //核心,拷贝了父类的实例属性和方法
this.like = like;
}
let boy1 = new Child('小刚','apple');
let boy2 = new Child('小明','orange');

//优点1:可向父类构造函数传参
console.log(boy1.name,boy2.name); //小刚,小明
//优点2:不共享父类构造函数的引用属性
boy1.arr.push(2);
console.log(boy1.arr,boy2.arr); //[1,2],[1]

//缺点1:方法不能复用
console.log(boy1.say === boy2.say); //false (说明boy1和boy2的say方法独立,不是共享的)

//缺点2:不能继承父类原型上的方法
Parent.prototype.walk = function(){
console.log('我会走路');
}
boy1.walk; //undefined(说明实例不能获得父类原型上的方法)

组合继承

  • 核心:通过调用父类构造函数,继承父类属性并保留传参的优点;然后通过将父类实例作为子类原型,实现函数复用。
  • 优点:
    • 保留方法1的优点:父类的方法定义在原型对象上,可以实现方法复用
    • 保留方法2的优点:创建子类实例,可以向父类构造函数传参;并且不共享父类的引用属性,如arr
  • 缺点:由于调用了2次父类的构造方法,会存在一份多余的父类实例属性
    原因:第一次Parent.call(this)从父类拷贝一份父类实例属性,作为子类的实例属性,第二次Child.prototype = new Parent()创建父类实例作为子类原型,(Child.prototype中的父类属性和方法会被第一次拷贝来的实例属性屏蔽掉,所以多余←这句话没理解)
    我的理解是,第二次new Parent的时候也执行了Parent构造函数,但是因为没有传参,导致子类实例对象的__proto__中,一部分属性为undefined

    注意name:undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Parent(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
}
Parent.prototype.say = function(){ //将需要复用、共享的方法定义在父类原型上
console.log('hello');
}
function Child(name,like){
Parent.call(this,name); //核心,第二次
this.like = like;
}
Child.prototype = new Parent(); //核心,第一次
Child.prototype.constructor = Child; //修正constructor指向

let boy1 = new Child('小刚','apple');
let boy2 = new Child('小明','orange');

//优点1:可以复用父类原型上的方法
console.log(boy1.say === boy2.say); true
//优点2:可以向父类构造函数传参数,且不共享父类引用属性
console.log(boy1.name,boy1.like); //小刚,apple

boy1.arr.push(2);
console.log(boy1.arr,boy2.arr); //[1,2],[1]

//缺点:由于调用了2次父类的构造方法,会存在一份多余的父类实例属性

组合继承优化

  • 核心:通过这种方式,砍掉父类的实例属性,这样在调用父类的构造函数的时候,就不会初始化两次实例,避免组合继承的缺点

  • 优点:

    • 只调用一次父类构造函数
    • 保留组合继承的优点
  • 缺点:修正构造函数的指向之后,父类实例的构造函数指向,同时也发生变化(这是我们不希望的)

具体原因:因为是通过原型来实现继承的,Child.prototype上面没有constructor属性,就会往上找,这样就找到了Parent.prototype上面的constructor属性;当修改了子类实例的constructor属性,所有的constructor的指向都会发生变化。(我觉得这个原因说得不对,constructor属性指向自身,Child上有constructor属性,真正原因可能是因为constructor是引用数据类型,所以修改一方才会影响另一方)


之前的name:undefined 消失了,改进成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Parent(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
}
Parent.prototype.say = function(){ //将需要复用、共享的方法定义在父类原型上
console.log('hello');
}
function Child(name,like){
Parent.call(this,name); //核心
this.like = like;
}
Child.prototype = Parent.prototype //核心,子类原型和父类原型,实际上是同一个
Child.prototype.constructor = Child;//修复代码

let boy1 = new Child('小刚','apple');
let boy2 = new Child('小明','orange');
let p1 = new Parent('小爸爸');

//优点不演示
//缺点1:当修复子类构造函数的指向后,父类实例的构造函数指向也会跟着变了

console.log(boy1.constructor);//没修复之前:Parent
console.log(boy1.constructor,p1.constructor); //修复之后:Child,Child 这就是问题所在

寄生组合继承

完美的继承方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent(name){
this.name = name; //实例基本属性(该属性,强调私有,不共享)
this.arr = [1]; //实例引用属性(该属性,强调私用,不共享)
}
Parent.prototype.say = function(){ //将需要复用、共享的方法定义在父类原型上
console.log('hello');
}
function Child(name,like){
Parent.call(this,name); //核心
this.like = like;
}
//核心 通过创建中间对象,子类原型和父类原型就会隔离开,不再是同一个,有效避免了方式4的缺点
Child.prototype = Object.create(Parent.prototype);

Child.prototype.constructor = Child;//修复代码

let boy1 = new Child('小刚','apple');
let boy2 = new Child('小明','orange');
let p1 = new Parent('小爸爸');

console.log(boy1.constructor,p1.constructor); //修复之后:Child,Parent

其中,Object.create()函数等价为:

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

于是中间那段核心代码可改为:

1
2
3
4
5
6
7
8
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
Child.prototype = object(Parent);

Child.prototype.constructor = Child;