首先回顾一下构造函数、原型和实例的关系?
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。如下图所示
现在我们引入原型链的概念?
假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。
看个例子
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 的实例,并将该实例赋给SubType.prototype 实现的。
这个例子中的实例以及构造函数和原型之间的关系如图所示
最终结果就是这样的:
1)instance 指向 SubType的 原 型 , SubType 的 原 型 又 指 向 SuperType 的 原 型 。
2)getSuperValue() 方 法 仍 然 还 在SuperType.prototype 中,但 property 则位于 SubType.prototype 中。这是因为 property 是一个实例属性,而 getSuperValue() 则是一个原型方法。既然 SubType.prototype 现在是 SuperType 的实例,那么 property 当然就位于该实例中了。
3)instance.constructor 现在指向的是 SuperType ,这是因为原来 SubType.prototype 中的 constructor 被重写了的缘故。
总结:调用instance.getSuperValue() 会经历三个搜索步骤:
1)搜索实例;
2)搜索 SubType.prototype ;
3)搜索 SuperType.prototype ,
最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。
1. 别忘记默认的原型
所有引用类型默认都继承了 Object。所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype 。上面例子中完整的原型链应该如下所示。
一句话, SubType 继承了 SuperType ,而 SuperType 继承了 Object 。当调用 instance.toString()时,实际上调用的是保存在 Object.prototype 中的那个方法。
2. 确定原型和实例的关系
instanceof操作符。测试实例与原型链中出现过的构造函数,结果就会返回 true 。
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
isPrototypeOf() 方法。只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此 isPrototypeOf() 方法也会返回 true ,
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
3. 谨慎地定义方法
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。给原型添加方法的代码一定要放在替换原型的语句之后。来看下面的例子。
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
另外在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链。
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!
4. 原型链的问题
问题一:最主要的问题来自包含引用类型值的原型。引用类型值的原型属性会被所有实例共享。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {}
//继承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
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 反映出来。
问题二:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。