一.原型链继承
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
原型链的问题
原型链虽然很强大,可以用来实现继承,但它也存在一些问题。其中最主要的问题来自包含引用类型值的原型。想必大家还记得,我们前面介绍过包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。下列代码可以用来说明这个问题。
function SuperType(){
this.colors = ["red","blue","green"];
}
function SubType(){
}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors) //[ 'red', 'blue', 'green', 'black' ]
var instance2 = new SubType();
console.log(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反映出来,就已经充分证实了这一点。
原型的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少单独使用原型链。
二、借用构造函数
(在子类型的构造函数内部调用超类型构造函数)
function SuperType(){
this.colors = ["red","blue","green"];
}
function SubType(){
// 继承了SuperType
SuperType.call(this)
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors) //[ 'red', 'blue', 'green', 'black' ]
var instance2 = new SubType();
console.log(instance2.colors)//[ 'red', 'blue', 'green']
我们实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数。这样一来就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例都会具有自己的colors属性的副本了。
1.传递参数
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
function SuperType(name){
this.name = name;
}
function SubType(){
//继承了SuperType,同时还传递了参数
SuperType.call(this,"Nicholas");
// 实例属性
this.age = 29;
}
var instance = new SubType()
console.log(instance.name) //Nicholas
console.log(instance.age) //29
以上代码中的SuperType只接受一个参数name,该参数会直接赋值给一个属性。在Subtype构造函数内调用SuperType构造函数时,实际上为SubType的实例设置了name属性。为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
2.借用构造函数的问题
如果仅仅是借用构造函数,那么将无法避免构造函数模式存在的问题——方法都在构造函数中定义。因此函数复用无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。
三、组合继承
组合继承指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性与方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。
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("balck");
console.log(instance1.colors)//[ 'red', 'blue', 'green', 'balck' ]
instance1.sayName();//Nicholas
instance1.sayAge();//29
var instance2 = new SubType("Greg",27);
console.log(instance2.colors) //[ 'red', 'blue', 'green' ]
instance2.sayName(); //Greg
instance2.sayAge();//27
在这个例子中,SuperType构造函数定义了两个属性:name和colors。SuperType的原型定义了一个方法sayName()。SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了它自己的属性age。然后,将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的SubType实例分别拥有自己的属性——包括colors属性,又可以使用相同的方法了。
组合继承避免了使用原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof 和isPrototypeof()也能够用于识别基于组合继承创建的对象。