JavaScript的对象继承
参考书籍:《JavaScript高级程序设计》(第3版)
继承是OO语言(面对对象语言)中的概念。许多OO语言都支持两种继承方式:接口继承(继承方法签名)和实现继承(继承实际方法)。但由于JavaScript
中没有签名,因此在ECMAScript
中只能实现方法继承。
1. 原型链
原型链( $prototype\ \ chaining$ )是实现继承的主要方法。基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
function SuperType() {
this.property = true;
}
SuperType.property.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
//继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
上述代码中定义了两种类型—— $SuperType$ 和 $SubType$ ,其中 $SubType$ 继承了 $SuperType$ 。实现的本质是重写 $SubType$ 的原型对象,即将一个 $SuperType$ 对象赋给 $SubType.property$ ,从而使得 $SuperType$ 中所有的属性和方法均存在于 $SubType$ 中。在实例中,我们也给 $SubType$ 添加了一个新方法。此外,还要注意,以此种方式实现的继承中, $SubType.constructor$ 指向的是 $SuperType$ 。
原型链能够实现继承,但也存在问题。由于是直接赋值给原型,因此其中的属性会被所有实例共享。如以下代码:
function SuperType() {
this.colors = ["red"];
}
function SubType() {}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("blue");
alert(instance1.colors); //"red,blue"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue"
还有,不能通过子类调用超类的有参构造函数等。
由于种种原因,实践中很少单独使用原型链。
2. 借用构造函数
借用构造函数( $constructor\ \ stealing$ )用于解决原型中包含引用类型值带来的问题。本质是在子类构造函数中调用超类构造函数。可以通过 $call(\ )$ 或者 $apply(\ )$ 方法实现。
function SuperType(name) {
this.name = name;
function sayHello() {
alert("Hello World!");
}
}
function SubType(name) {
SuperType.call(this, name);
}
显然,通过“借调”超类构造函数,子类的每个实例就都具有超类中属性和方法的副本了。
虽然该种方法避免了引用原型的问题,但对于超类中包含的方法(如上例中所示的 $sayHello(\ )$ 方法)也被复制了一份,没有实现函数复用。因此,借用构造函数的方法也很少单独使用。
3. 组合继承
组合继承( $combination\ \ inheritance$ ),也叫伪经典继承。顾名思义,即组合原型链和借用构造函数的技术。本质是利用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。
function SuperType(name) {
this.name = name;
}
SuperType.prototype.sayName = function() {
alert(this.name);
};
function SubType(name) {
//继承属性
SuperType.call(this, name);
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
可以看到,组合继承不仅让子类中的每个实例都拥有自己的属性,还能调用相同的方法。
组合继承避免了原型链和借用构造函数的缺点,融合了他们的优点,因此成为了JavaScript
中最常用的继承方式。而且,通过组合继承, $instanceof$ 和 $isPrototypeOf(\ )$ 也能用于识别其创建的对象。
组合继承也有其不足,即在任何时候都会调用两次超类构造函数。
4. 原型式继承
原型式继承( $prototypal\ \ inheritance$ )是一种特殊的继承方式,该方法并没有严格意义上的构造函数,本质是借助原型可以基于已有对象创建新对象,还不必创建自定义类型的特性。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
friends: ["Tom"];
};
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Van");
alert(person.friends); //"Tom,Rob,Van"
在 $object(\ )$ 函数中,创建了函数 $F(\ )$ 作为一个临时构造函数,将传入的对象作为其原型并返回一个新实例。在ECMAScript5
中新增了 $Object.create(\ )$ 方法规范化了原型式继承。该方法接收两个参数——一个用作新对象的原型,另一个为新对象定义额外属性(可选)。在传入一个参数时,行为与 $object(\ )$ 方法相同。
var person = {
friends: ["Tom"];
}
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");
alert(person.friends); //"Tom,Rob"
var yetAnotherPerson = Object.create(person, {
name: {
value: "Van"
}
});
alert(yetAnotherPerson.name); //"Van"
5. 寄生式继承
寄生式继承( $parasitic\ \ inheritance$ )与原型式继承紧密相关。其思路与寄生构造函数和工厂模式类似,创建一个用于封装继承过程的函数,在函数内部以某种方式增强对象。
function createAnother(original) {
var clone = object(original);
clone.sayHello() = function() {
alert("Hello World!");
};
return clone;
}
$createAnother(\ )$ 方法接受一个对象作为新对象的基础对象,再将其赋给 $clone$ ,通过 $clone$ 对象添加 $sayHello(\ )$ 方法,最后返回 $clone$ 对象。
与构造函数模式类似,寄生式继承会由于无法做到函数复用而降低效率。
6. 寄生组合式继承
寄生组合式继承( $parasitic\ \ combination\ \ inheritance$ )即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。在之前我们谈到了组合继承的不足,而解决办法就是寄生组合式继承。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
$inheritPrototype(\ )$ 方法接收两个参数:子类构造函数和超类构造函数。其高效性在于只调用了一次 $SuperType$ 构造函数,且避免了在 $SubType.prototype$ 上创建不必要的属性,保持原型链不变,还能使用 $instanceof$ 和 $isPrototypeOf(\ )$ ,开发人员普遍认为寄生组合式继承是引用类型的最理想的继承方式。
7. 小结
JavaScript
主要通过原型链实现继承。但原型链不适宜单独使用,解决其问题的技术是借用构造函数。使用最多的继承模式是组合继承。此外还可以选择原型式继承、寄生式继承和寄生组合式继承。