带你看懂JavaScript的继承(三) 原型链的补充问题

####一,你看不到的默认原型
我们前面所说的原型链还少了最关键的一环,他是所有引用类型的开始,我们中二一点,把它称为“原式-零”。它就是Object。JS中所有的引用类型继承自Object,而且这个继承也是通过原型链实现的。我们所知的null也是一个引用类型,说到底,就是一个空Object的引用。
所以,默认原型都会有一个内部指针指向Object.prototype。现在你明白为什么我们所有的自定义类型都会继承toString(),valueOf()等默认方法了吧。因为他们有一个终极的祖先——原式-零。

####二,原型 && 实例
我们如何确定原型和实例的关系,也就是判断某个实例的原型是否是某个原型。有以下两个方法。

######使用instanceof操作符
用我们一二期中的代码来解释可以进行如下操作来判断instance的原型是SuperType、SubType还是Oject。

1
2
3
console.log(instance instanceof Object);        //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType); //true

由于原型链的关系,结果返回的都是true,这是没有问题的。

######使用isPropertyOf()方法
要记住下面这句话:

只要是原型链中出现的原型,都可以说是该原型链所派生的实例的原型

所以下面的结果毫无疑义:

1
2
3
Object.prototype.isPropertyOf(instance);          //true
SuperType.prototype.isPropertyOf(instance); //true
SubType.prototype.isPropertyOf(instance); //true

####三,定义方法需小心

######添加原型方法放在替换原型语句之后
我们在前两期的代码中加入稍微改动一点,

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

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

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

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

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

//替换超类型中的方法
SubType.prototype.getSuperValue = function(){
return 'Replacement';
}

var instance = new SubType();
console.log(instance.getSuperValue()) //'Replacement'

也就是说,当你替换完原型后,在子类型中重写超类型的方法,会覆盖掉超类型的方法,此时instance调用getSuperValue()这个方法是重写后的方法,所以会返回’Replacement’。这个点要切记。

######原型链继承切记不能通过字面量创建原型方法
我们首先要说一个知识点:

用字面量重写原型方法时,会重写原型链。

所以,当我们用如下写法时,会报错:

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

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

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

//替换SubType.prototype,实现了SubType继承SuperType
SubType.prototype = new SuperType();

SubType.prototype = {
getSubValue : function(){
return this.subproperty;
},
someOtherMethod : function(){
return false;
}
}

var instance = new SubType();
console.log(instance.getSuperValue()) //ERROR!

发生这种错误的原因很简单,当我们替换了SubType.prototype后,用字面量的方法给SubType.prototype添加新的方法,这时我们就重写了SubType.prototype,它已经和SuperType.prototype没有关系了,此时的SubType.prototype包含的是一个Object的实例,我们设计的原型链已经被切断

####四,原型链的原罪

######共享所带来的弊端
原型链是我们在JS中实现继承的基础,但是他也是带着原罪被设计出来的。主要的问题就是包含引用类型值得问题。超类型的方法和属性被子类型实例所共享,这也正是在上面代码中我们在构造函数中定义属性,而不在原型对象中定义属性的原因。在通过原型实现继承时,子类型就变成了超类型的实例,进而,超类型的方法和属性也就成了子类型的方法和属性。

我们假设有如下的情况:

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

function SubType(){
}

//替换SubType.prototype,实现了SubType继承SuperType
SubType.prototype = new SuperType();

var instance = new SubType();
instance.animals.push['dog'];
console.log(instance.animals) //['horse', 'cat', 'dog']

var instance2 = new SubType();
console.log(instance2.animals); //['horse', 'cat', 'dog']

SubType通过原型继承了SuperType之后,SubType.prototype就成了SuperType的实例,因此他有了自己的animals属性。但问题在于,所有SubType的所有实例都会共享这个animals属性,任何一个实例对这个属性做了修改,会直接修改SuperType中的animals属性,影响所有的实例,而且是实时的。这在使用中,要特别关注。

######传参不可能
在创建子类型实力的时候,不能向超类型的构造函数中传递参数。准确的说,是不能在不影响所有对象实例的情况下,给超类型的构造函数传递参数。大大降低灵活性。

####总结
这期,我们全面的阐释原型链的一些容易被忽略的知识和它的原罪。原型链是JS中特别重要,特别重要,特别重要,特别重要,特别重要的理论基础,这是它不同于其他编程语言很重要的一方面,但是很多人了解的并不是很清楚,深入的挖掘这里面的东西,还有很多很多的东西需要去理解,我们这里不作再深入的阐述,原型的问题写上十期说不尽。我们现在是为了介绍继承打下一个理论基础,下一期我们正式进入继承的部分,讨论一种继承理论——经典继承