继承
许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在JS中无法实现接口继承,JS只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
原型链
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。
JS将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
实现原型链有一种基本模式,如下:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SuperType() {
this.subProperty = false;
}
// 继承了SuperType
SuperType.prototype = new SuperType();
SubType.prototype.getSuperValue = function() {
return this.subProperty;
};
var instance = new SuperType();
alert(instance.getSuperValue()); // true
当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。在找不到属性或方法的情况下,搜索过程总是一环一环地前行到原型链末端才会停下。
默认的原型
所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。
确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。第一种方式就是使用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
谨慎地定义方法
子类有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。
原型链的问题
最主要的问题来自包含引用类型值的原型。包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章的变成了现在的原型属性了。
第二个问题是在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
借用构造函数
在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用借用构造函数的技术(也叫做伪造对象或经典继承)。这种技术的基本思想是在自类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。例如:
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
// 继承了SuperType
SuperType.call(this);
}
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'
传递参数
相对于原型链而言,借用构造函数可以在自类型构造函数中向超类型构造函数传递参数。
借用构造函数的问题
如果仅仅是借用构造函数,那么也将无从避免构造函数模式存在的问题–方法都在构造函数中定义,因此函数复用就无从谈起。而且,在超类型的原型中定义的方法,对自类型而言也是不可见的。结果所有类型都只能使用构造函数模式。
组合继承
组合继承(combination inheritance),有时候也叫伪经典继承,指的是将原型链和借用构造函数肚饿技术组合到一块,从而发挥二者之长的一种继承模式。思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,即通过在原型上定义方法显示了复用,又保证每个实例都有自己的属性。例如:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Supertype.prototype.sayName = function() {
alert(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() {
alert(this.age);
}
var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
alert(instance1.colors); // 'red, blue, green, black'
instance1.sayName(); // 'Nicholas'
instance1.sayAge(); // 29
var isntance2 = new SubType('Greg', 27);
alert(instance2.colors); // 'red, blue, green'
instance2.sayName(); // 'Greg'
instance2.sayAge(); // 27
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,称为JS中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合创建的对象。
原型式继承
原型式继承的的实现方法与普通继承的实现方法不同,原型式继承并没有使用严格意义上的构造函数,而是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
Object.create()这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与Object()方法的行为相同。
var person = {
name : 'Nicholas',
friends : ['Shelby', 'Court', 'Van']
}
var anotherPerson = object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Bob');
var anotherPerson2 = object.create(person);
anotherPerson2.name = 'Linda';
anotherPerson2.friends.push('Rose');
alert(person.friends); // Shelby, Court, Van, Bob, Linda
第二个参数与Object.defineProperties()方法的第二个参数格式相同;每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:
var person = {
name : 'Nicholas',
friends : ['Shelby', 'Court', 'Van']
}
var anotherPerson = Object.create(person, {
name: {
value: 'Greg'
}
});
alert(anotherPerson.name); // Greg
寄生式继承
寄生式(parasitic)继承式与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地式它做了所有工作一样返回对象。例如:
function createAnother(original) {
var clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式来增强这个对象
alert('hi');
}
return clone; // 返回这个对象
}
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。任何能够放那hi新对象的函数都适用于此模式。
寄生组合式继承
继承组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数。本质上就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。如下:
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);//实现继承
SubType.prototype.sayAge = function(){
alert(this.age);
}
开发人员普遍认为寄生组合式继承式引用类型最理想的继承范式。