本文深入解析 JavaScript 基于原型的继承机制,对比传统类继承。详细讲解如何使用 method、inherits 和 swiss 等辅助函数实现类继承、瑞士继承及寄生继承,并通过 Parenizor 示例演示代码复用与对象扩展技巧,揭示 JavaScript 动态对象的灵活优势。
JavaScript 继承
Douglas Crockford
And you think you're so clever and classless and free --John Lennon
JavaScript 原型继承概述
JavaScript 是一种没有传统类的面向对象语言,它采用原型继承机制来替代类继承。对于习惯 C++ 和 Java 等传统面向对象语言训练的开发者而言,这可能令人困惑。然而,JavaScript 的原型继承比类继承具有更强的表现力,下面我们将具体探讨。
|
Java |
JavaScript |
|---|---|
|
强类型 |
弱类型 |
|
静态 |
动态 |
|
基于类 |
基于原型 |
|
类 |
函数 |
|
构造器 |
函数 |
|
方法 |
函数 |
首先,我们探讨为何需要继承。主要有两个原因:一是类型安全。我们希望语言系统能自动处理类似类型的引用转换(cast)。通过要求程序显式转换对象引用类型,可以获得一定程度的类型安全,这是强类型语言的关键特征。但对于 JavaScript 这种弱类型语言而言,类引用无需强制转换,因此这一点对 JavaScript 并不适用。
第二个原因是代码复用。程序中常发现许多对象实现相同方法。类允许在单一位置集中定义对象,这非常常见。对象中包含其他对象也很普遍,区别仅在于添加或修改部分方法。类继承对此有用,但原型继承更为有效。
为了展示这一点,我们将介绍一些“甜点”代码,使开发者能像编写传统类语言一样写代码。随后展示类语言中没有的有用模式,最后解释这些“甜点”。 类继承 首先,我们建立一个 Parenizor 类,它包含 value 属性的 getter 和 setter 方法,以及一个将 value 包装在括号内的 toString 方法。
代码如下:
function Parenizor(value) {
this.setValue(value);
}
Parenizor.method('setValue', function (value) {
this.value = value;
return this;
});
Parenizor.method('getValue', function () {
return this.value;
});
Parenizor.method('toString', function () {
return '(' + this.getValue() + ')';
}); 这种语法可能看似无用,但它清晰地展示了类的形式。method 方法接受一个方法名和一个函数,并将它们作为公共方法放入类中。
现在我们可以这样写:
代码如下:
myParenizor = new Parenizor(0);
myString = myParenizor.toString(); 正如预期,myString 的值为 "(0)"。
接下来,我们建立另一个继承自 Parenizor 的类,基本相同,但 toString 方法在 value 为零或空时返回 "-0-"。
代码如下:
function ZParenizor(value) {
this.setValue(value);
}
ZParenizor.inherits(Parenizor);
ZParenizor.method("e;toString"e;, function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
}); inherits 方法类似于 Java 的 extends。uber 方法类似于 Java 的 super,它允许方法调用父类方法(更改名称以避免与保留字冲突)。
我们可以这样编写:
代码如下:
myZParenizor = new ZParenizor(0);
myString = myZParenizor.toString(); 这次,myString 的值为 "-0-"。
类继承实现
JavaScript 虽然没有类,但我们可以编程实现类似类的行为。
多继承
通过操作函数的 prototype 对象,我们可以实现多继承。混合多继承难以实现且可能面临名称冲突风险。我们可以在 JavaScript 中实现混合多继承,但本例使用一种较规范的形式,称为瑞士继承(Swiss Inheritance)。
假设有一个 NumberValue 类,其 setValue 方法用于检查 value 是否在指定范围内,并在适当时抛出异常。我们只需要 ZParenizor 的 setValue 和 setRange 方法,而不需要它的 toString 方法。因此,我们编写:
代码如下:
ZParenizor.swiss(NumberValue, 'setValue', 'setRange'); 瑞士继承模式
这将仅添加所需的方法。
寄生继承
这是编写 ZParenizor 类的另一种方法。不从 Parenizor 继承,而是编写一个调用 Parenizor 构造器的构造器,并对结果进行修改,最后返回该结果。该构造器添加的是特权方法而非公共方法。
代码如下:
function ZParenizor2(value) {
var self = new Parenizor(value);
self.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-"
};
return self;
} 寄生继承与对象扩展
类继承是一种“是……”的关系,而寄生继承是一个关于“原是……而现在是……”的关系。构造器在对象构造中扮演大量角色。注意 uber(代替 super 关键字)对特权方法仍然有效。
类扩展
JavaScript 的动态性允许我们对已有类添加或替换方法。我们可以在任何时候调用方法,随时扩展一个类。继承并非如此。因此,我们将这种情况称为“类扩展”,以避免与 Java 的 extends(也称扩展,但含义不同)混淆。
对象扩展
在静态面向对象语言中,若希望一个对象与另一个对象有所区别,必须新建一个类。但在 JavaScript 中,你可以向单独的对象添加方法而无需新建类。这具有巨大能量,因为你只需书写尽可能少的类,类也可以写得更简单。将 JavaScript 的对象视为哈希表。你可以在任何时候添加新值。如果该值是一个函数,它就会成为一个方法。
因此,在上面的例子中,我完全不需要 ZParenizor 类。我只需简单修改我的实例即可。
代码如下:
myParenizor = new Parenizor(0);
myParenizor.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
};
myString = myParenizor.toString(); 我们给 myParenizor 实例添加了一个 toString 方法,而未使用任何继承。由于语言是无类型的,我们可以演化单独的实例。
小甜点
辅助方法实现细节
要使上述示例运行,我编写了四个“甜点”方法。首先是 method 方法,它可以将一个实例方法添加到类中。
代码如下:
Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
}; 这将在 Function.prototype 中添加一个公共方法,从而通过类扩展所有函数均可使用。它接受名称和函数作为参数。
它返回 this。当我编写没有返回值的方法时,通常让其返回 this,以形成链式语句。
以下是 inherits 方法,它指示一个类继承自另一个类。它必须在两个类都定义完成后调用,但在方法继承之前调用。
代码如下:
Function.method('inherits', function (parent) {
var d = 0, p = (this.prototype = new parent());
this.method('uber', function uber(name) {
var f, r, t = d, v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d -= 1;
return r;
});
return this;
}); 再次,我们扩展 Function 类。我们加入一个 parent 类的实例并将其作为新的 prototype。我们还必须修正 constructor 字段,同时加入 uber 方法。
uber 方法将在自己的 prototype 中查找某个方法。这是寄生继承或类扩展的一种情况。如果是类继承,我们要找到 parent 的 prototype 中的函数。return 语句调用函数的 apply 方法来调用该函数,同时显式设置 this 并传递参数。参数(如果有的话)可以从 arguments 数组中获得。不幸的是,arguments 数组并非真正的数组,因此我们再次用到 apply 来调用数组中的 slice 方法。
最后是 swiss 方法
代码如下:
Function.method('swiss', function (parent) {
for (var i = 1; i < arguments.length; i += 1) {
var name = arguments[i];
this.prototype[name] = parent.prototype[name];
}
return this;
}); swiss 方法对每个参数进行循环。对于每个名称,它将 parent 原型中的成员复制到新类的 prototype 中。
总结
JavaScript 可以像类语言那样使用,但它也有独特的表现层次。我们已经看过类继承、瑞士继承、寄生继承、类扩展和对象扩展。这一系列代码复用模式都源自这个一直被认为很小、很简单的 JavaScript 语言。
类对象属于“硬的”。给“硬的”对象添加成员的唯一方法是建立一个新的类。在 JavaScript 中,对象是“软的”。要给“软”对象添加成员,只需简单赋值即可。
由于 JavaScript 中的类如此灵活,你可能会想到更复杂的类继承。但深度继承并不合适。浅继承则较有效且更易表达。