内容来自《JavaScript高级程序设计》第三版第6章第3节
原型链
ECMAScript中描述了 原型链的概念,并将原型链作为实现继承的主要方法。基本思想是利用原型让一个引用类型继承另一个引用了类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(proto.constructor)。那么,加入我们让原型对象等于另一个对象的实例,结果会怎么样呢?显然,此时的原型对象将宝航一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个原型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
实现原型链有一种基本模式:
function SuperType () {
this.prototype = true;
}
SuperType.prototype.getSuperValue = function () {
return this.subprototype;
}
function SubType () {
this.subprototype = false;
}
//继承了SuperType的实例
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.getSubValue = function(){
return this.subprototype;
};
var instance = new SubType();
console.log(instance.getSuperValue);//true
以上代码定义了一个类型:SuperType和SubType。每个类型分别有一个属性和方法。它们的主要区别是SubType继承了SuperType。而继承是用过创建SuperType的实例,并将该实例赋值给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了,在确立了继承关系之后,我们给SubType.prototype添加了一个方法,这样在继承了SuperType的属性和方法的基础上又添加了一个新方法。
这里我们没有使用SubType默认提供的原型,而是给它换了一个新的原型;这个新原型就是SuperType的实例。于是,新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType的原型。最终结果是这样的:instance的原型指向SubType的原型,SubType的原型又指向SuperType的原型。getSuperValue()方法仍然还在SuperType.prototype中, 但prototype则位于SubType.prototype中。这是因为prototype是一个实例属性,而getSuperVale()则是一个原型方法。
注意此时instance.constructor指向SuperType。
- 别忘记默认原型
前面的例子展示的原型链还少一环。所有引用类型默认继承了Object,而这个继承也是通过原型链实现的。所有好事的more呢原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。
一句话,SubType继承了SuperType,二SuperType继承了Object。
- 确定原型和实例的关系
可以通过两种方式确定原型和实例之前的关系。
第一种方式是使用instanceof
操作符,只要这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true
。
console.log(instance instanceof Object);//true
console.log(instance instanceof SuperType);//true
console.log(instance instanceof SubType);//true
第二种方式是使用isprototypeOf()
方法。同样,只要原型链中出现过的原型,都可以说是该原型所派生的实例的原型,因此isprototypeOf()
方法也会返回true
。
console.log(Object.prototype.isPrototypeOf(instance));
console.log(SuperType.prototype.isPrototypeOf(instance));
console.log(SubType.prototype.isPrototypeOf(instance));
- 谨慎地定义方法
子类型有时候需要重写超类型中的某个方法,或者添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这杨会重写原型链。
- 原型链的问题
最主要的问题来自包含引用类型值的原型。包含引用类型值的原型属性会被所有实例共享,在通过原型来实现继承时,原型会变成另一个对象的实例,于是原先实例的属性也就成了现在的原型属性了。这样所有实例都会共享同一个引用类型的原型属性,修改其中一个的值,其他所有实例的值都会随之修改。
原型链的第二个问题是在创建子类型的实例时,不能向超类型的构造函数中传递参数,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
借用构造函数
在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种交错借用构造函数的技术(有时候也叫做伪造对象或经典继承),即在子类型构造函数的内部调用超类型构造函数,通过使用apply()和call()方法可以在新创建的对象上执行构造函数。
function SuperType () {
this.color = ['red','blue','green'];
}
function SubType () {
// 继承了SuperType
SuperType.call(this)
}
var instance1 = new SubType();
instance1.color.push('blcak');
console.log(instance1.color); //["red", "blue", "green", "blcak"]
var instance2 = new SubType();
console.log(instance2.color); //["red", "blue", "green"]
通过使用call()
方法(或apply()
方法也可以),我们实际上是在新创建的SubType
实例中调用了SuperType
构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就会都具有自己的colors属性的副本了。
- 传递参数
相对于原型链而言,借用构造函数有一个很大的有时,即可用在子类型构造函数中向超类型构造函数中传递参数。
function SuperType (name) {
this.name=name;
}
function SubType () {
//继承了SuperType
SuperType.call(this,'Nicholas');
//实例属性
this.age=19;
}
var instance = new SubType();
console.log(instance.name);
console.log(instance.age);
- 借用构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题————方法都在构造函数中定义,因此函数服用就无从谈起了。而且,在超类型的原型中定义的方法,在子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
组合继承
组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,二通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。
function SuperType (name) {
this.name=name;
this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
};
function SubType (name,age) {
//继承属性
SuperType.call(this,name);
//实例属性
this.age=age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.SayAge = function () {
console.log(this.age);
}
var instance1 = new SubType("nicholas",29);
instance1.colors.push('black');
console.log(instance1.colors);
instance1.sayName();
instance1.SayAge();
var instance2 = new SubType('Greg',27);
console.log(instance2.colors);
instance2.sayName();
instance2.SayAge();
在这个例子中,SuperType构造函数定义了两个属性:name和colors。SuperType的原型定义了一个方法sayName()。SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了它自己的属性age。然后将SuperType的实例赋值给了SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的SubType实例既分别拥有自己的属性————包括colors属性,又可以使用相同的方法了。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象
原型式继承
道格拉斯·克罗克福德在2006年鞋了一篇文章,题为Prototypal Inheritance in JavaScript (JavaScript中的原型继承)。在这片文章中,他介绍了一种实现继承的方法,这种方法没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同事还不必因此创建自己定义类型,为了达到这个目的,他给出了如下函数。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
从本质上讲,object()对传入的对象执行了一次浅复制。
这种原型式继承,要求你笔记有一个对象可以作为另一个对象的原型。ECMAScript5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象,一个为新对象定义额外属性的对象(可选)。在传入一个参数的情况下,Object.create()与object()方法的行为相同。
支持Object.create()方法的浏览器有IE9+、Firefox 4+、Opera 12+ 和Chrome。
寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再想真的是它做了所有工作一样返回对象。
function createAnother(original){
var clone = object(Original);
clone.sayHi=function(){
console.log('hi');
}
return clone;
}
在这个例子中,createAnother()函数接收了一个参数,也就是将要作为新对象基础的对象。然后,吧这个对象(original)传递给object()函数,将返回的结果赋值给clone。再为clone对象添加一个新方法sayHi(),最后返回clone对象。可以向厦门这样来使用createAnother()函数。
var person = {
name = "Nicholas",
friends = ['aaa','bbb,'ccc']
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //'hi'
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种游泳的模式。前面示范继承模式时使用的object()函数不是必须的,任何能够返回新对象的函数都适用于此模式。
寄生组合式继承
组合式继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次实在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
function SuperType(name){
this.name = name;
this.color = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name); //第二次调用SuperType()
this.age =age;
}
SubType.prototype = new SuperType() //第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
在第一次调用SuperType构造函数,这一次又在新对象上创建了实例属性name和colors。于是,这两个属性就拼比了原型中的两个同名属性。 构造函数时,SubType.prototype会得到两个属性:name和colors;他们都是SuperType的实例属性,只不过现在位于SubType的原型中。当调用SubType构造函数时,又会调用一次SuperType构造函数,这一次又在新对象上创建了实例属性name和colors。于是,这两个属性就拼比了原型中的两个同名属性。
所谓寄生组合式继承,即通过借用构造函数还继承属性,通过原型链的混成形式来继承方法。其基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无非是超类型原型的一个副本而已,本质上,就是使用寄生式来继承超类型的原型,然后再将结结果指定给子类型的原型。寄生组合式继承的基本模式如下
functiong inheritPrototype(SubType,SuperType){
var prototype = object(SuperType.prototype);
prototype.constructor = SubType;
SubType.prototype=prototype;
}
这个示例中 inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了。函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本;第二部是为创建的副本添加constructor属性,从而弥补因重写尔失去的默认constructor尚需经;最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以调用inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了。
function SuperType(name){
this.name = name;
this.color = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name); //第二次调用SuperType()
this.age =age;
}
inheritPrototype(SubType,SuperType);
Subtype.prototype.sayAge = function(){
console.log(this.age);
}
这个例子的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变,因此,还能正常使用instanceof和isPrororypeOf().