Javascript继承(原型链)
继承是OO语言中的一个最为人津津乐道的概念。许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于javascript函数没有签名,在ECMAScript中无法实现接口继承,ECMAScript只支持实现继承,而且其实现主要依靠原型链来实现的。
1、原型链
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。每个构造函数都有一个原型对象,原型对象中包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是所谓原型链的概念。
实现原型链有一种基本模式,其代码大致如下。
以上代码定义了两个类型:SuperType和SubType。每个类型分别有一个属性和方法。它们主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将实例赋给SuperType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType的实例中的所有方法和属性,现在也存在于SubType.prototype中了,在确立了继承关系之后,我们给SubType.prototype添加一个方法,这样就在继承了SuperType的属性和方法的基础上又添加了一个新方法。
在通过实现原型链实现继承的情况下,搜索过程就是沿着原型链继续向上,,例如上面例子,调用instance.getSuperValue()会经历三个搜素步骤:1)搜素实例;2)搜素SubType.prototype;3)搜素SuperType.prototype,最后一步才找到该方法。在找不到属性或方法的情况下,搜素过程总是一环一环地前行到原型链的末端才会停下来。
1.1别忘记默认的原型
事实上,前面例子中的原型链少了一环,我们知道,所有引用类型都默认继承了Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都继承toString()、valueOf()等默认方法的根本原因,所以我们上面例子展示的原型链中还应该包含另外一个继承层次。
Su币Type继承了SuperType,而SuperType继承了Object。当调用instance.toString()时,实际调用的是保存在Object.prototype中的那个方法。
1.2确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,就会返回true,一下几行代码就说明了这一点。
由于原型链的关系,我们可以说instance是Object、SuperType、SubType任何一个类型的实例。因此,测试者三个构造函数结果都返回了true。
第二种方式是使用isProttotypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例原型,因此isProttotypeOf()方法也会返回true,如下所示:
1.3谨慎地定义方法
子类型如果需要重写超类型中的某个方法,或者希望添加超类型中不存在的某个方法,不管怎样,给原型添加方法的代码一定要放在替换原型语句之后,如下图所示:
在以上代码中,方框2个方法的定义,第一个方法getSubValue()被添加到了SubType中。第二个方法getSuperValue()是原型链中已经存在的方法,但重写这个方法将会屏蔽原来的那个方法。换句话说,当通过SubType的实例调用getSuperValue()时,调用的就是重新定义的方法;但通过SuperType的实例调用getSuperValue()时,还会继续调用原来的那个方法。这里额外注意的是,必须在用SuperType的实例替换原型之后,再定义这2个方法。
还有一点需要注意的是,通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链,如下图所示:
以上代码刚刚把SuperType的实例赋给原型,紧接着又将原型替换成一个对象字面量而导致问题。由于现在的原型包含的是一个Object的实例,而非SuperType的实例,因此我们设想中的原型链已被切断——SubType和SuperType之间几经没有关系了。
1.4原型链的问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中最主要的问题来自包含引用类型的值原型。包含引用类型的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上回变成另一个类型的实例。于是,原先的实例属性也就变成了原型的属性,如以下代码:
这个例子中SuperType构造函数中定义了一个colors的属性,该属性包含一个数组(引用类型值)。SuperType的每个实例都会有包含自己数组的colors属性。当SubType通过原型链继承了自己的colors属性——就专门创建了一个SubType.prototype.colors属性一样,结果是SubType的所有实例都共享一个colors属性。我们通过对instance1.colors的修改能都通过instance2.colors反应出来,就已经证实了这一点。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。再加上刚刚讨论的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。