Javascript中的类式继承(Classical Inheritance)

来源:互联网 发布:电子琴软件 电脑版 编辑:程序博客网 时间:2024/05/21 09:01

译注:因为项目需要,最近学习了下Javscript。本来以为Javascript是个弱类型的脚本语言,大致看下就能搞定。到看到其面向对象部分时,发现与C++和Java的对象机制差别很大,有种耳目一新之感。遂找了些资料看,发现有个叫Douglas Crockford的大牛写得很深入,但都是英文的,便想把一些经典的翻译过来。此为其一。

Javascript 中的类式继承(Classical Inheritance)

英文原文

你聪明绝顶,又自由自在

——John Lennon


Javscript是种无
类型的面向对象语言,它采用一种与类式继承不同的原型式继承机制。虽然这让熟悉C++和Java等传统面向对象语言的程序员感到迷惑,但正如本文即将展示那样,Javascript的原型式继承方式比类式继承方式要强大得多。
但先有个问题——为什么我们关注继承方式呢?主要原因有二。其一是类型转化,强类型语言需要明确的机制以实现不同类型引用间的转化,但对Javascript这种弱类型语言来说,所有的对象引用都是同一类型,无需转化。其二是代码复用,可以定义一种类而建立大量具有相同方法的对象,也可以通过类的继承来创建许多具有相似方法的对象。类式继承方式在这方面做得很好,但原型式继承却能做到更好。
为说明这一点,我先引入几个基本函数,以便模仿传统类式语言的风格,然后给出一些类式语言中没有的模式,最后对这些基本函数作出解释。

类式继承

先来写个Parenizor类,它有个属性value及相应的set、get方法,和一个将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或非空时产生“-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没有类,但却可以写出“类式”代码。

多重继承

通过操作函数的prototype属性,可以实现多重继承,以建立继承多个类的方法的新类。任意的多重继承难于实现,并会造成命名冲突。虽然Javascript可以实现任意多重继承,但本例遵从一种称为Swiss继承的原则。

设存在NumberValue类,它有个setValue方法,检查属性value的值是否是某一范围内的数字,若不是则抛出异常。如果仅仅需要从这个类继承setValue和setRange给ZParenizor类话,只需这样写

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;}
类式继承是“是一个”的关系,寄生继承是“以前是但现在不是”的关系。构造函数在对象的构造过程中作用重大。需要注意的是,uber方法在特权函数中仍然可用。

类增强

Javscript允许增加或替换任意一个已经存在的类的方法,并且可以在任何时候在该类现有的或将来的任意实例中调用该方法。任意刻都可以直接扩展一个类。但继承却只能在父类改变后,才可以在新继承的子类实例中调用改变后的方法。我们将这称为类增强,以避免与Java中具有另外含义的扩展(extends)相冲突。

对象增强

在静态面向对象语言中,即使需要一个与其它对象稍微不同的新对象,也要定义一个新类。但在Javascript中,可以为单个对象添加方法,而无需定义额外的类。这一点威力无穷,因为它可以减少类的数量和编写代码的难度。回忆下,Javascrip的t对象就像个哈希表,可以随时添加新属性,如果属性值是函数,则添加的就是新方法。

因此,上述代码其实并不需要ZParenizor类,可以仅仅通过修改实例来实现。

myParenizor = new Parenizor(0);myParenizor.toString = function () {    if (this.getValue()) {        return this.uber('toString');    }    return "-0-";};myString = myParenizor.toString();
现在只是给myParenizor对象添加了个toString方法,而无需使用任何形式的继承。正是因为Javascript语言没有类的概念,我们才能演化任一单个对象实例。

基本函数

为使上述例子正常工作,我写了几个基本函数。首先是给类添加方法的method函数

Function.prototype.method = function (name, func) {    this.prototype[name] = func;    return this;};
它给Function.prototype添加公共方法,以使所有函数都能通过类增强来获得该方法。它采用一个名字和一个函数作为参数,将其作为函数添加到函数的prototype对象中。

method的返回值为this。我在写没有返回值的函数时,习惯上返回一个this,这样便于链式编程。

下一个出场的函数是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!我们创建了parent的新实例,并将其作为新的prototype,同时也修改了constructor属性,及为prototype添加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方法遍历arguments参数,对每个name属性,从parent的prototype复制一份给新类的prototype。

结论

虽然Javascript可以像类式继承那样使用,但它依旧有些非常独特的特性。我们阐述了类式继承、Swiss继承、寄生继承、类增强、对象增强。但这许多的复用模式却是来自一门比Java更小更简单的语言。

类式继承是很僵硬的,给“硬对象”添加新成员的唯一方式是创建一个新类。但Javascript中对象是“软”的,新成员可以简单地通过赋值来添加。

因为Javascript中的对象有如此强大的伸缩性,所以你会想对类式继承进行一些新的思考。深层次的继承结构通常并不合适,浅层次的继承才更有效、更有力。

我已经写了8年的Javascript程序,却从未发现需要用到uber函数之处。父类在类模式中固然非常重要,但在原型和函数模式中却可有可无。现在我认为在Javascript中模仿类模型是个错误。

原创粉丝点击