2007年3月14日

在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类拥有setValuesetRange方法,所以我们这样写:

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中的对象是如此的灵活,你将可以考虑不同的关于类的继承方式,不要太深的层次,浅浅的是有效率的和富于表现力的。

特别声明,文章翻译于 Classical Inheritance in JavaScript
版权由原作者所有。

没有评论: