在JavaScript中实现基于类模式的继承
JavaScript 是一个和类无关的(class-free),面向对象的语言,它使用了原型继承的方式代替了类的(classical,传统的)继承方式,这会使那些用惯了像C++和Java的程序员感到困惑。过一会我们会发现,JavaScript的原型继承方式比传统的继承方式更据表现力。
Java | JavaScript |
---|---|
强类型(Strongly-typed) | 弱类型(Loosely-typed) |
静态(Static) | 动态(Dynamic) |
基于类的(Classical) | 基于原型的(Prototypal) |
类(Classes) | 函数(Functions) |
构造器(Constructors) | 函数(Functions) |
方法(Methods) | 函数(Functions) |
首先,我们为什么要关心继承机制呢?有2个理由,首先,这有利于类型,我们想要Javascript自动建造相似类的引用关系,安全类型(type-safety)是从一个需要程序直接构造对象引用类型的系统获取,这是鉴别强类型语言一个关键点,但对于像JavaScript这样的弱类型语言是不相关的,因为它的对象的引用关系根本不需要建造(casting)。
第2个理由是代码的重用,让一系列对象实现一些相同的方法是相当平常的事情,传统的类通过一套单独的定义实现了这一点。让对象与一些其它对象类似也是很平常的,但是不同点是需要增加或者修改一小撮方法。类的继承方式对这些是非常有用,但是原型继承甚至是更有用的。
为了证明这点,我们将引入一个甜点(sugar),它将给我们提供一种别具风格的像传统的基于类的方式书写代码。我们稍后也会介绍一种基于类的语言不可做的,非常有用的模式。最后我们将解释这甜点(sugar)。
类模式的继承
首先,我们创建一个Parenizor类,它有设置和获取属性value值的方法,toString方法将用括号包裹住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方法生成一个方法名和一个函数,并把他们增加到类的公有方法中。
现在我们可以写了。
myParenizor = new Parenizor(0);
myString = myParenizor.toString();
正像你所期待的一样,myString的值是"(0)"。
现在我们将创建另一个类,它继承于Parenizor,功能上相同除了当value为0或空的时候,方法toString将返回"-0-"。
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 Inheritance。
设想有一个叫做NumberValue的类,它有一个setValue来检查value属性在一个确切的范围内是否是数字,如果有必要就抛出异常。我们仅仅想要ZParenizor类拥有setValue和setRange方法,所以我们这样写:
ZParenizor.swiss(NumberValue, 'setValue', 'setRange');
这在类上只增加了我们需要的方法。
寄生继承(Parasitic Inheritance)
有另一种方式写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方法),仍然可以在特权方法里访问。
类的扩充(Class Augmentation)
JavaScript的动态的特性允许我们对已存在的类增加或替换他们的方法。我们可以在任何时候调用method 方法,不管是现在还是将来的类的实例都将拥有新增加的方法,我们差不多可以在任何时候扩充类,继承方式则需要倒转过来,我们称这种方式为类的扩充(Class Augmentation)以避免与Java的意味着别的含义的extends相混淆。
对象扩充(Object Augmentation)
在静态的面向对象的语言中,如果你想要一个对象与另一个对象有着细微的不同,你需要定义一个新类;在JavaScript中,你可以为实例增加方法而不是类。这是一个极强的能力,因为你可以写更好的类了,并且类也可以写的更简单了。回想起JavaScript对象就像哈希表一样,你可以在任何时候增加新值,如果这个值是一个函数,那么它就成为了一个方法。
所以在上面的实例中,我根本就不需要ZParenizor这个类了,我只要简单地修改一下我的实例就行了。
myParenizor = new Parenizor(0); myParenizor.toString = function () { if (this.getValue()) { return this.uber('toString'); } return "-0-"; }; myString = myParenizor.toString();
我们在myParenizor实例上直接地增加了toString,而没有使用任何形式的继承。我们可以发展个别的实例,因为JavaScript是与类型无关的(class-free)。
甜点(Sugar)
为了使上面的实例可以运行,我写了4个sugar方法,首先method是为类增加一个实例方法的方法。
Function.prototype.method = function (name, func) { this.prototype[name] = func; return this; };
这为Function.prototype增加一个公有方法。所有由类的扩充的函数都可以使用它。它有一个名称和一个函数,并把它们增加到了函数的prototype上。(译者注:就是为所有function语句的作为类的原型提供了一个公有方法。)
它返回this,当我写一个不需要返回值的方法时,我通常让它返回this,这顾虑到了串联样式( cascade-style)的程序。
接下来是inherits方法,它的作用是让一个类从另一个上继承。它应当在那两个类都以声明完之后调用,在继承之前,类的方法已经增加了。
Function.method('inherits', function (parent) { var d = {}, p = (this.prototype = new 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,我们创建了一个父类的实例,并把它作为新的原型。我们也纠正了constructor字段,并且我们在这个原型增加了uber方法。
uber方法在他自己的原型里查找指定的方法,这就是在寄生继承或多项扩充的时候为了调用的函数。如果我们使用传统的继承方式,那么我们需要找到在其父类里的原型中找到这个函数。return语句使用了函数的apply方法来调用这个函数。参数(如果有的话)被获从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参数,对于每一个名称,它都复制出一个成员从父类的原型上到新类的原型上。
结束语
JavaScript可以用传统语言的方式使用,但是它有个层次上表现就是它非常的独特。我们已经观察过了类模式的继承、Swiss Inheritance、寄生继承、类的扩充和对象的扩充,这一套代码重用模式来自于被人为要比Java渺小的和简陋的多的JavaScript。
传统语言是硬梆梆的,为它增加一个新的成员的方式只能是就是创建一个新类。在JavaScript中,对象是软绵绵的,一个新的成员可以被增加到这个对象上使用简单的赋值即可。
因为在JavaScript中的对象是如此的灵活,你将可以考虑不同的关于类的继承方式,不要太深的层次,浅浅的是有效率的和富于表现力的。
没有评论:
发表评论