javascript中的类式继承

作者:Douglas Crockford

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

Java JavaScript
Strongly-typed Loosely-typed
Static Dynamic
Classical Prototypal
Classes Functions
Constructors Functions
Methods Functions
JavaScript是一种无类别的、面向对象的语言,而且它用原型继承而不是用类继承。对于一直被训练于使用传统的面向对象语言(比如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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值