JS继承学习笔记

很多的OO语言都支持:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMASript中只实现了实现继承,而且其实实现继承主要是依赖原型链来实现的。

1、原型链

ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方式。其主要思想是利用原型让一个引用类型继承另一个引用类型的属性和方法,简单回顾一下构造函数、原型和实例的关系:每一个构造函数都有一个原型对象,原型对象都包含一个指向一个构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如我们让原型对象等于另一个类型的实例,那么此时原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,这样就形成了实例域原型的链条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 实现原型链有一种基本模式
function SuperType(){
this.property = true;
}

SuperType.prototype.getSuperValue = function(){
return this.property;
};

function SubType(){
this.subproperty = false;
}

//继承了Supertype,实现的本质是重写原型对象
SubType.prototype = new SuberType();

SubType.prototype.getSubValue = function (){
return this.subproperty;
}

var instance = new SubType();
alert(instance.getSuperValue()); //true

现在instance.constructor现在指向的是SuperType,这是因为原来SubType.prototype中的constructor被重写的缘故(实际上是subType的原型指向了另一个对象SuperType的原型,而原型对象的构造函数指向的是SuperType)。

通过原型链,就扩展了原型搜索机制。当然所有的引用类型都继承自Object.

注意:需要谨慎地定义方法,子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。来看下面的例子

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 SuperType(){
this.property = true;
}

SuperType.prototype.getSuperValue = function(){
return this.prototype;
}

function SubType(){
this.subproperty = false;
}

//继承了SubperType
SubType.prototype = new SuperType();

//添加新方法
SubType.prototype.getSubValue = function (){
return this.subproperty;
}

//重写超类型中的方法
SubType.prototype.getSuperValue = function (){
return false;
}

var instance = new SubType();
alert(instance.getSuperValue()); //false

同时注意在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这个方法会重写原型链。

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 SuperType(){
this.property = true;
}

SuperType.prototype.getSuperValue = function(){
return this.prototype;
}

function SubType(){
this.subproperty = false;
}

//继承了SubperType
SubType.prototype = new SuperType();

//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
getSubValue : function (){
return this.subproperty;
},
someOtherMethod : function (){
reutrn false;
}
};

var instance = new SubType();
alert(insdtance.getSuperValue();) //error!

以上代码展示了刚刚把SuperType的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个Object的实例,而非SuperType的实例,因此我们设想中的原型链已经被切断——SuperType和SubType之间已经没有关系了。

1.借助构造函数实现继承

原理:通过call()函数修改 this 指向,从而实现将父类属性挂载到子类实例中。

复制代码

1
2
3
4
5
6
7
8
function parent1() {
this.name = 'parent1';
}
function child1() {
parent1.call(this);
this.type = 'child1';
}
console.log(new child1);

复制代码

打印结果:

img

当我们给父类 parent1 的 prototype 属性添加say方法后,但是在 child1 中是获取不到的。

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
function parent1() {
this.name = 'parent1';
}
parent1.prototype.say = function() {
console.log('hello');
};

function child1() {
parent1.call(this);
this.type = 'child1';
}
console.log(new child1, new child1().say());

复制代码

打印结果:

img

所以.借助构造函数实现继承,只能实现部分继承;如果父类属性都在构造函数中,则能够实现全部继承,如果父类原型对象上还有方法,则子类是继承不到的。

总结:

优点:
1.只调用一次父类的构造函数,避免了在子类原型上创建不必要的,多余的属性 。
2.原型链保持不变。
缺点:只能实现部分继承;如果父类属性都在构造函数中,则能够实现全部继承,如果父类原型对象上还有方法,则子类是继承不到的。

2.借助原型链实现继承(最通用的方式)

原理:将子类的prototype属性赋值为父类实例对象,则子类的_proto_属性继承父类。

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function parent2() {
this.name = 'parent2';
this.play = [1, 2, 3];
}
parent2.prototype.say = function() {
console.log('hello');
};

function child2() {
this.type = 'child2';
}
child2.prototype = new parent2();
console.log(new child2);
var p1 = new child2();
var p2 = new child2();
console.log(p1.say());
console.log(p1.play, p2.play);
p1.play.push(4);
console.log(p1, p2);
console.log(p1.play, p2.play);

复制代码

打印结果:

img

注意:

1.在第一种继承方式中,子类是继承不到父类 prototype 属性的内容的,但现在可以继承到了。

2.其实小颖只执行了 p1.play.push(4) ,然而 p2.play 的值也跟着变化了。

这其实都是因为 child2.prototype = new parent2(),他们的 proto 都继承了父类parent2 的所有属性。虽然表面上 p1.play.push(4) 看起来像是只改变了 p1 的 play 属性,但其实是改变了父类 parent2 的 play 属性,而p1,p2继承了 parent2 ,所以p1,p2同时发生变化。

总结:

优点:父类的方法(getName)得到了复用。
缺点:重写子类的原型 等于 父类的一个实例,(父类的实例属性变成子类的原型属性)如果父类包含引用类型的属性,那么子类所有实例都会共享该属性 (包含引用类型的原型属性会被实例共享)。

3.组合方式

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function parent3() {
this.name = 'parent3';
this.play = [1, 2, 3];
}

function child3() {
parent3.call(this);
this.type = 'child3';
}
child3.prototype = new parent3();
var p3 = new child3();
var p4 = new child3();
console.log(p3.play, p4.play);
p3.play.push(4);
console.log(p3,p4);
console.log(p3.play, p4.play);

复制代码

打印结果:

img

注意:

在上面的结果中,大家有没有发现,同样只给 p3.play.push(4) ,但是只有p3一个变了,但p4没有变,其实大家通过小颖用红框框起来的地方,大就会明白,为什么p3、p4的 proto 都继承了父类parent2 的属性,为什么修改p3,p4,这次p4却没有变化。

总结:

优点:继承了上述两种方式的优点,摒弃了缺点,复用了方法,子类又有各自的属性。
缺点:因为父类构造函数被执行了两次,子类的原型对象(Sub.prototype)中也有一份父类的实例属性,而且这些属性会被子类实例(sub1,sub2)的属性覆盖掉,也存在内存浪费。

4.组合继承的优化1

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function parent4() {
this.name = 'parent4';
this.play = [1, 2, 3];
}

function child4() {
parent4.call(this);
this.type = 'child4';
}
child4.prototype = parent4.prototype;
var p5 = new child4();
var p6 = new child4();
console.log(p5, p6);
console.log(p5 instanceof child4, p5 instanceof parent4);
console.log(p5.constructor);

复制代码

打印结果:

img

注意:

instanceof 和 constructor 都是用来判断一个实例对象是不是这个构造函数的实例的。
不同点是:用constructor 比instanceof 更严谨,例如如果 A 继承 B,B 继承 C,A 生成的实例对象,用 instanceof 判断与 A、B、C 的关系,都是 true。所以无法区分这个到底是 A、B、C 谁生成的实例。而constructor 是原型对象的一个属性,并且这个属性的值是指向创建当前实例的对象的。

console.log(p5 instanceof child4, p5 instanceof parent4); 执行结果一样,而且 p5.constructor 竟然不是 child4 而是 parent4。

5.组合继承的优化2 ——寄生组合式继承

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function parent5() {
this.name = 'parent5';
this.play = [1, 2, 3];
}

function child5() {
parent5.call(this);
this.type = 'child5';
}
child5.prototype = Object.create(parent5.prototype);
child5.prototype.constructor = child5;
var p7 = new child5();
var p8 = new child5();
console.log(p7, p8);
console.log(p7.constructor);

复制代码

打印结果:

img

总结:

组合继承的缺点就是在继承父类方法的时候调用了父类构造函数,从而造成内存浪费,并且找不到实例对象真正的 constructor 。

那在复用父类方法的时候,使用Object.create方法也可以达到目的,没有调用父类构造函数,并将子类的 prototype.constructor 属性赋值为自己本身,则问题完美解决。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!