JavaScript中的类继承
JavaScript是一个无class的面向对象语言,它使用原型继承而非类继承。这会让那些使用传统面向对象语言如C++和Java的程序员们感到困惑。正如我们所看到的,JavaScript的原型继承比类继承具有更强的表现力。
但首先,要搞清楚我们为什么如此关注继承?主要有两个原因。首先是方便类型的转换。我们希望语言系统能够对那些相似类的引用进行自动转换。而对于一个要求对引用对象进行显示转换的类型系统来说只能获得很少的类型安全性。这对于强类型语言来说很重要,但是在像JavaScript这样的松散型语言中,永远不需要对对象引用进行强制转换。
第二个原因是代码的复用。代码中存在大量拥有相同方法的对象是十分常见的。类可以通过一组定义来创建它们。另外存在很多相似的对象也很普遍,这些对象中只有少数有关添加和修改的方法存在区别。类的继承可以很有效地解决这些问题,但原型继承更有效。
为了说明这一点,我们将介绍一点语法糖,它允许我们以类似于传统的class的语言来编写代码。然后我们将介绍一些有用的模式,这些模式不适用于传统的class语言。最后,我们将对语法糖进行解释。
类继承
首先,我们添加了一个Parenizor类,包含set和get两个方法,分别用来设置和获取value,以及一个toString方法,用来对parens中的value进行包装。
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接受方法的名称和一个function,并将这个function作为公共方法添加到类中。
然后我们可以这样写:
myParenizor = new Parenizor(0); myString = myParenizor.toString();
正如你所期望的,myString的值为”(0)”.
现在我们创建另一个类继承Parenizor,除了toString方法中对于value为空或0的情况会输出”-0-“外其余都和Parenizor相同。
function ZParenizor(value) { this.setValue(value); } ZParenizor.inherits(Parenizor); ZParenizor.method('toString', 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没有类,但是我们可以通过编程来实现它。
多重继承
通过操作一个函数的原型对象,我们可以实现多重继承,从而使我们可以用多个类的方法来构建一个类。混合多重继承可能难以实现,并可能存在方法名称的冲突。我们可以在JavaScript中实现混合多重继承,但是在本例中我们将使用一个更严格的被称之为Swiss继承的形式。
假设有一个NumberValue类,包含一个方法setValue,该方法检查value是否为某个特定范围内的数字,必要的时候会抛出异常。我们只需要ZParenizor的setValue和setRange方法,而不需要toString方法。那么我们可以这样写:
ZParenizor.swiss(NumberValue, 'setValue', 'setRange');
这样只会将我们需要的方法添加到类中。
寄生继承
ZParenizor还有另外一种写法。除了从Parenizor类继承,我们还可以在构造函数中调用Parenizor的构造函数,并传递返回的结果。通过这种方式,我们给构造函数添加特权方法,而不用再去为其添加公共方法。
function ZParenizor2(value) { var that = new Parenizor(value); that.toString = function () { if (this.getValue()) { return this.uber('toString'); } return "-0-" }; return that; }
类的继承是is-a关系(公有继承),而寄生继承是was-a-but-now’s-a关系(私有继承与公有继承)。构造函数在对象的构造中发挥了很大的作用。注意uber和super方法仍然可用于特权方法。
类的扩充
JavaScript的动态性允许我们添加或替换现有类的方法,method方法可以随时被调用,这样类的所有实例在现在和将来都会有这个方法。我们可以在任何时候对一个类进行扩展。继承具有追溯性,我们把这个叫做类的扩充(Class Augmentation),以避免与Java的extends产生混淆。
对象的扩充
在静态面向对象语言中,如果你想要一个对象与另一个对象略微不同,就需要定义一个新的类。在JavaScript中,你可以将方法添加到单个的对象中,而不需要在定义额外的类。这个非常强大,因为你只需要写很少的类,并且类都可以很简单。回想一下,JavaScript对象就像哈希表,你可以随时添加新的值,如果值是function,那么它就成了一个方法。
因此在上面的示例中,我根本不需要ZParenizor类。我可以简单地修改我的实例。
myParenizor = new Parenizor(0); myParenizor.toString = function () { if (this.getValue()) { return this.uber('toString'); } return "-0-"; }; myString = myParenizor.toString();
我将toString方法添加到我的myParenizor实例中,而没有使用任何形式的继承。我们可以修改单个的实例,因为语言是无class的。
Sugar(语法糖)
为了使上面的示例能正常工作,我写了四个sugar方法。首先是method方法,它将一个实例方法添加到类中。
Function.prototype.method = function (name, func) { this.prototype[name] = func; return this; };
它在Function.prototype上添加了一个公共方法,因此所有的函数都通过Class Augmentation(类的扩充)获得了该方法。它接受一个名称和一个函数,并将它们添加到函数的原型对象中。
它返回this. 当我编写一个不需要返回值的方法时,我通常都会返回this,这样就具有了一个级联式的编程风格。
接下来是inherits方法,它用来表示一个类从另一个类继承。应该在两个类都被定义之后再调用这个方法,并且在继承类的方法之前添加该方法。
Function.method('inherits', function (parent) { this.prototype = new parent(); var d = {}, p = this.prototype; this.prototype.constructor = parent; this.method('uber', function uber(name) { if (!(name in d)) { d[name] = 0; } var f, r, t = d[name], 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[name] += 1; r = f.apply(this, Array.prototype.slice.apply(arguments, [1])); d[name] -= 1; return r; }); return this; });
我们继续对Function进行扩充。我们创建了一个父类的实例,并将其作为新的原型。我们还修改了构造函数的字段,并将uber方法添加到原型中。
Uber方法在自己的原型中查找指定的方法。这是在寄生继承或对象扩充的情况下调用的函数。如果我们进行类的继承,那么我们就需要在父类的原型中找到这个函数。Return语句使用函数的apply方法来调用function,显示地设置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方法对arguments进行遍历。对每一个name,它都从父类的原型中复制一个成员到新类的原型中。
结论
JavaScript可以像class语言一样来使用,但它也具有相当独特的表现力。我们研究了类的继承,Swiss继承,寄生继承,类的扩充以及对象的扩充。这种大量代码的复用模式来自于一种被认为比Java更小,更简单的语言。
类的对象非常严格,要将一个新成员添加到对象中,唯一的方法就是创建一个新类。而在JavaScript中,对象是松散的,可以通过简单的赋值操作将一个新成员添加到对象中。
由于JavaScript中的对象非常灵活,所以你需要对类的层次结构进行不同的考虑。深层次的结构并不太适用,相反,浅层次的结构更高效,更具有表现力。
我从事编写JavaScript代码已经有14年了,而且我从来没有发现需要使用uber函数。Super在class模式中十分重要,但是在原型和函数式模式中不是必须的。现在看来我早期尝试在JavaScript中支持class模式是一个错误。 |