javascript中的类式继承

来源:互联网 发布:淘宝云客服报名关闭 编辑:程序博客网 时间:2024/05/18 02:45
作者:Douglas Crockford

原文链接:http://www.crockford.com/javascript/inheritance.html

JavaJavaScriptStrongly-typedLoosely-typedStaticDynamicClassicalPrototypalClassesFunctionsConstructorsFunctionsMethodsFunctionsJavaScript是一种无类别的、面向对象的语言,而且它用原型继承而不是用类继承。对于一直被训练于使用传统的面向对象语言(比如java、c++)的程序员来说对此可能会感到疑惑。JavaScript的原型继承比传统的面向对象继承具有更强大的表达力,下面我们将会看到。
首先,我们为什么要关心继承的问题?这里主要有两个原因。首先是为了类型便利。我们希望语言系统能自动的对类似的类投射引用。从一个需要显式铸造常规引用对象的类型系统里,往往能获取较少的类型安全。在强类型语言中这是至关重要的,但是在对象引用不需要铸造的松弛语言类型比如JavaScript中这是不相干的。
另一个原因是代码重用。对于拥有相同方法实现的一些对象来说这是很常见的。类使得从单一的一套定义中来生成这些对象成为了可能。拥有一些和其他对象类似的对象也是很常见的,不同之处仅在于添加或修改一小部分的方法。类式继承对于此很有用处,但是原型继承会更有用处。
为了演示这一点,我们引入了一点sugar,它让我们可以用类似于传统的类语言的方式来写代码。我们等下回展示在类语言中获取不到的有用的模式。最后,我们会解释这些sugar。

类式继承

首先我们写一个Parenizor类,对于它的value它有set和get方法。另外还有一个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方法有一个name和一个function参数,把它们增加给类作为公共方法。
因此我们可以这样写:
myParenizor = new Parenizor(0);
myString = myParenizor.toString();
正如你所期望的,myString 的值为"(0)"。
现在我们来写继承自Parenizor的另一个类,除了该类的toString方法在value是0的情况下产生"-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。
假定有一个数值类,它有一个setValue方法来检查值是否是一个一定范围内的数值,有必要的话扔出一个异常。对于我们的ZParenizor类我们仅需要它的setValue和setRange方法。我们当然不需要它的toString方法。因此我们这样写:
ZParenizor.swiss(NumberValue, 'setValue', 'setRange');
对于我们的类上面的方法就仅仅增加我们需要的方法。

寄生式继承

这有另一种方式来写ZParenizor。不用从Parenizor继承,我们写一个构造函数来调用Parenizor的构造函数,把结果作为它的返回值。并且不用增加公共方法,构造函数增加了私有方法(privileged methods)。

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这个天生的超类方法在是有方法中仍然是可以获取的到的。


类扩展

JavaScript的动态性允许我们来增加或者替换一个已经存在的类的方法。我们可以在任何时候来调用method方法,而且这个类的现在和将来的所有实例都会拥有着那个方法。我们可以在任何时候来扩展一个类。继承会追溯的工作。我们调用这种类扩展来避免和Java的extends混淆,它意味着其他的一些东西。


对象扩展

在静态面向对象语言中,如果你想拥有一个和另一个对象略微不同的对象,你需要定义一个新类。在JavaScript中,你可以向个体对象增加方法而不需要额外的其他类。这会有巨大的能量,因为你可以写很少的类并且你写的类也可能会非常简单。回想一下,JavaScript对象就像哈希表。你可以在任何时间为其增加新的值。如果你增加的值是一个函数,那么它就会变为一个方法。


因此在上面的例子中,我完全不需要一个ZParenizor类。我可以简单的修改我的实例来达到相同的目的。

myParenizor = new Parenizor(0);myParenizor.toString = function () {    if (this.getValue()) {        return this.uber('toString');    }    return "-0-";};myString = myParenizor.toString();

我们增加了一个toString方法到我们的myParenizor实例中却没用任何形式的继承。我们可以演变个实例,因为语言是无类型的。

Sugar

为了使上面的例子能工作,我写了四个sugar方法。首先是method方法,它用来为类增加实例方法:
Function.prototype.method = function (name, func) {    this.prototype[name] = func;    return this;};

它增加了一个公共方法到Function.prototype,因此所有的函数可以通过类的扩充都可以得到该方法。它有一个name和一个函数参数,并把他们添加到函数的原型对象中。
它返回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。我们生成一个父类的实例并把它作为新的原型。我们也改正了constructor指向,并给prototype增加了uber方法。
uber方法在它的原型中寻找命名了的方法,这是在寄生式继承或者对象扩展中来调用的函数。如果我们正在做类式继承,我们需要在父亲的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,它从父类的成员中复制一份给新类的prototype。

结论

JavaScript可以像类语言那样被应用,但是它也有一定级别的丰富的表现力,这是很独特的。我们已经看了类式继承,Swiss继承,寄生式继承,类扩展和对象扩展。代码重用模式的这个很大的集合就来自于一种被认为比Java更简单更小型的语言。
类对象是很硬的。给一个硬对象添加一个新成员的唯一方式就是去创建一个新类。在Javascript里面,对象是软的。通过简单的分配就能把一个新成员增加到一个软的对象上。
因为JavaScript中的对象是如此的灵活,你可能想以不同的角度来思考类层次结构。深层次的结构是不恰当的。浅层次结构才有效并富有表现力。
到现在我已经写JavaScript 8年了,我从来没有发现要用uber方法的需求。super的想法在类模式中是相当重要的,但是它在原型式和函数式模式中似乎是不必要的。我现在看到,我早期在JavaScript中尝试支持类式模型的做法是错误的。

0 0
原创粉丝点击