《JavaScript 高级程序设计》学习总结六(3)
引言:继承是面向对象语言中的一个最为人津津乐道的概念。许多面向对象都支持两种继承方式:接口继承与实现继承,接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于JavaScript 中函数没有签名,所以在ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而实现继承主要依靠原型链来实现的。
原型链:
概念:每个构造函数都要一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的指针,那么假如我们让原型对象等于另一个类型的实例结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。
说的太抽象了,我们直接看案例:实现原型链有一种基本模式:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } //继承了SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function (){ return this.subproperty; }; var instance = new SubType(); alert(instance.getSuperValue()); //true
如图:
我们没有使用 SubType 默认提供的原型,而是给它换了一个新原型;这个新原型 就是 SuperType 的实例。于是,新原型不仅具有作为一个 SuperType 的实例所拥有的全部属性和方法, 而且其内部还有一个指针,指向了 SuperType 的原型。最终结果就是这样的:instance 指向 SubType 的原型, SubType 的原型又指向 SuperType 的原型。 getSuperValue() 方法仍然还在 SuperType.prototype 中,但 property 则位于 SubType.prototype 中。这是因为 property 是一 个实例属性,而 getSuperValue()则是一个原型方法。既然 SubType.prototype 现在是 SuperType的实例,那么 property 当然就位于该实例中了。此外,要注意 instance.constructor 现在指向的 是 SuperType,这是因为原来 SubType.prototype 中的 constructor 被重写了的缘故(① 实际上,不是 SubType 的原型的 constructor 属性被重写了,而是 SubType 的原型指向了另一个对象—— SuperType 的原型( SubType.prototype = new SuperType()),而这个原型对象的 constructor 属性指向的是 SuperType。)。
别忘记默认的原型:
我们知道,所有引用类型默认都继承了 Object,而 这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。这也正是所有自定义类型都会继承 toString()、 valueOf()等默认方法的根本原因。所以,我们说上面例子展示的原型链中还应该包括另外一个继承层 次。
如图:
一句话,SubType 继承了 SuperType,而 SuperType 继承了 Object。当调用 instance.toString() 时,实际上调用的是保存在 Object.prototype 中的那个方法。
确定原型与实例的关系:
确定原型和实例的关系 可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用 instanceof 操作符。
第二种:是使用 isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该 原型链所派生的实例的原型,因此 isPrototypeOf()方法也会返回 true
举个例子:
alert(Object.prototype.isPrototypeOf(instance)); //true
谨慎地定义方法:
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎 样,给原型添加方法的代码一定要放在替换原型的语句之后。来看下面的例子
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } //继承了 SuperType 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
加粗的部分是两个方法的定义。第一个方法 getSubValue()被添加到了 SubType 中。第二个方法 getSuperValue()是原型链中已经存在的一个方法,但重写这个方法将会屏蔽原来的 那个方法。换句话说,当通过 SubType 的实例调用 getSuperValue()时,调用的就是这个重新定义 的方法;但通过 SuperType 的实例调用 getSuperValue()时,还会继续调用原来的那个方法。这里 要格外注意的是,必须在用 SuperType 的实例替换原型之后,再定义这两个方法(也就是上面的://继承了 SuperType SubType.prototype = new SuperType(); )
PS :值得注意的是:通过原型链实现继承时2,不能用对象字面量创建原型方法,因为这样会重写原型链。比如我们将上面的代码改成
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } //继承了 SuperType SubType.prototype = new SuperType(); //使用字面量添加新方法,会导致上一行代码无效 SubType.prototype = { getSubValue : function (){ return this.subproperty; }, someOtherMethod : function (){ return false; } }; var instance = new SubType(); alert(instance.getSuperValue()); //error!
以上代码展示了刚刚把 SuperType 的实例赋值给原型,紧接着又将原型替换成一个对象字面量而 导致的问题。由于现在的原型包含的是一个 Object 的实例,而非 SuperType 的实例,因此我们设想 中的原型链已经被切断——SubType 和 SuperType 之间已经没有关系了。
原型链的问题
上一章节我们提到,原型模式的自身问题,那就是原型可以实现属性共享,而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。举个例子:
1 function SuperType(){ this.colors = ["red", "blue", "green"]; 2 3 } 4 5 function SubType(){ } 6 7 //继承了 8 9 SuperType SubType.prototype = new SuperType(); 10 11 var instance1 = new SubType(); 12 13 instance1.colors.push("black"); 14 15 alert(instance1.colors); //"red,blue,green,black" 16 17 var instance2 = new SubType(); alert(instance2.colors); //"red,blue,green,black"
这个例子中的 SuperType 构造函数定义了一个 colors 属性,该属性包含一个数组(引用类型值)。 SuperType 的每个实例都会有各自包含自己数组的 colors 属性。当 SubType 通过原型链继承了 SuperType 之后,SubType.prototype 就变成了 SuperType 的一个实例,因此它也拥有了一个它自 己的 colors 属性——就跟专门创建了一个 SubType.prototype.colors 属性一样。但结果是什么 呢?结果是 SubType 的所有实例都会共享这一个 colors 属性。而我们对 instance1.colors 的修改 能够通过 instance2.colors 反映出来,就已经充分证实了这一点。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上, 应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上 前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。
----------------------------------------------------------------------本章节完------------------------------------------------------------
下一章节预告:本章节我们总结学习了继承,同时也了解到原型链继承机制碰到的问题,那么这个问题我们会在下一章节进行学习总结。
(JavaScript的继承是这个语言的重点部分,我在写这篇博文时一直在想怎么总结才好,要不要自己用自己的理解与语言重写,纠结了好一会后自己写了一篇,但是不如人意,决定还是使用书中的文字,以后随着深入,再重开一篇单独讲继承。还有一个比较尴尬,我现在才发现博客的“插入代码”功能可以帮助更好的排版,所以我又得重新将前面的博文重新排版了)