OO 模拟那些事儿

感谢所有老前辈们,感谢所有同类代码。因为有了你们,世界才丰富多采。

Douglas Crockford 的尝试与悟道

关于类继承,Douglas 有一篇经典文章:

这篇文章里,老道分析了为什么要在 JavaScript 里模拟类继承:主要目的是复用。老道的实现方式是给 Function.prototype 增加 methodinherits 两个方法,并提供了 uber 语法糖。

悲催的是,大神实现的 inheritsuber 存在不少缺陷,国内和国外都有不少人剖析过,可以参考 2008 年时的一篇讨论帖子:

老道最后悟出了一段经常被引用的话:

我编写 JavaScript 已经 8 个年头了,从来没有一次觉得需要使用 uber 方法。在类模式中,super 的概念相当重要;但是在原型和函数式模式中,super 的概念看起来是不必要的。现在回顾起来,我早期在 JavaScript 中支持类模型的尝试是一个错误。

由此,老道又写了一篇经典文章,推崇在 JavaScript 里,直接使用原型继承:

继续悲催的是,老道的实现,依旧有不足之处。可以参考两篇博文:

老道的代码虽然不尽完美,但老道的尝试和对原型继承的呼吁依旧非常值得我们尊敬和思考。

研究到此,如果大家都能理解原型继承,问题其实已经终结,特别是在 webkit 等支持 __proto__ 的运行环境下。比如:

function Animal() {}
function Dog() {}

// 要让 Dog 继承 Animal, 只需:
Dog.prototype.__proto__ == Animal.prototype;

// 实例化后
var dog = new Dog();
// dog.__proto__ 指向 Dog.prototype
// dog.__proto__.__proto__ 指向 Animal.prototype
// 原型链已成功建立起来,而且很清晰

老道的 inherits 和 NCZ 的 inherit 本质上都是设置好 __proto__ 属性。看清楚这一点,一切都很简单。

原型继承的确已经够用,但这需要大家都能深入理解原型继承,对 __proto__, prototypenew 等关键点有清晰的认识。通过 inherits 等方法,可以简化部分细节。但用户在使用时,依旧需要面对 prototype 等属性,并且很容易写出有隐患的代码,比如:

function Animal() {}
function Dog() {}

util.inherits(Dog, Animal);

Dog.prototype = {
  talk: function() {},
  run: function() {}
};

上面的代码,你知道问题在哪吗?请继续阅读。

YUI 之路

YUI 团队是 Douglas 的铁杆粉丝团。从 YUI2 到 YUI3, 都高度贯彻了 Douglas 的精神。在 YUI 里,提供了 extend 方法:

function Animal() {}
function Dog() {}

Y.extend(Dog, Animal, {
    talk: fn,
    run: fn
});

YUI 还提供了 augment, mix 等方法来混入原型和静态方法。理论上足够用了,但对普通使用者来说,依旧存在陷阱:

function Animal() {}
function Dog() {}

Y.extend(Dog, Animal);

Dog.prototype = {
    talk: fn,
    run: fn
};

var dog = new Dog();
alert(dog instanceof Dog); // false

上面的写法,破坏了 Dog.prototype.constructor, 导致 instanceof 不能正常工作。正确的写法是:

Dog.prototype = {
    constructor: Dog,
    talk: fn,
    run: fn
};

Dog.prototype.talk = fn;
Dog.prototype.run = fn;

通过 extend 等方式来实现原型继承,写法上很灵活。constructor 是个不小不大的问题, 但对于类库来说,任何小问题,都有可能成为大问题。

extend 的方式,仅仅是对 JavaScript 语言中原型继承的简单封装,需要有一定 JavaScript 编程经验后才能娴熟使用。(我个人其实蛮喜欢简简单单的 extend)。

此外,extend 的灵活性也是一种“伤害”。定义一个类时,我们更希望能有一种比较固定的书写模式, 什么东西写在什么地方,都能更简单,更一目了然。

JavaScript 是一门大众语言,在类继承模式当道的今天,直接让用户去面对灵活的原型继承,未必是最好的选择。

世界的进步在于人类的不满足。作为前端的我们,只是想用更简单更舒适的方式来书写代码。JavaScript 新的语言规范里,已经提出了 class 概念。但在规范确定和浏览器原生支持前,故事还得继续。

:YUI3 里,除了 extend 方式,也提供了 Base.create 来创建新类,但是该方法比较重量级了,用起来不轻便。

Dean Edwards 的 Base.js

Dean Edwards 是前端界的一位老前辈。老前辈做过一个当时很著名的 JavaScript 类库: Base.js, 其中有一套非常不错的 OO 实现:

这个方案开辟了一条阳光大道:通过精心构造的 Base 基类来实现类继承。同一时期,JavaScript 界 OO 模拟蔚然成风,万马奔腾。让我们继续考考古。

Prototype 的 Class

作为一名前端,如果没用过 Prototype, 那么恭喜你,说明你还年轻,潜力无限。来看一名老前端的吐槽:

Prototype 目前已经 v1.7 了。从官方文档来看,Class 继承已经很成熟:

Class.create 的写法已经比较优美。然而悲催的是,$super 的约定真让人无语。super 虽然很难实现,但也不要这样实现呀:代码一压缩就都浮云了。

John Resig 的实现

jQuery 专注于 DOM 操作,因此无论现在还是以后,应该都不会去模拟类继承。但在风云变幻的年代里,jQuery 作者 John Resig 也忍不住掺合一脚:

与 Base2 和 Prototype 相比,John Resig 的实现无疑更漂亮。_super 的实现方案也简单有效,不过在 JavaScript 实现原生的 class 之前,所有 super 方案都很难完美。比如:

var Animal = Class.extend({
    talk: function() {
        alert('I am talking.');
    },
    sleep: function() {
        alert('I am sleeping.')
    }
});

var Dog = Animal.extend({
    talk: function() {
        this._super();
    }
});

// 在另一个文件里,扩展 Dog 对象:
Dog.prototype.sleep = function() {
    this._super(); // 会报错
};

很明显,要使用 _super, 必须严格按照固定模式来写。面对灵活的 JavaScript, 所有 super 都是美丽的谎言。

MooTools Class

MooTools 的全称是 My OO Tools, 有一套口碑很不错的 Class 机制:

new Class 的方式很优美,ExtendsImplements 的首字母大写,看习惯了也觉得挺好。

Class 和所创建的类上,也都有 extend 方法,与 John Resig 版本相同。

super 语法糖,MooTools 采用了 this.parent() 的形式。原理与 John Resig 的差不多,都是采用 wrap 的方式,但 MooTools 利用了非标准属性 caller 来实现。

所有 wrap 的实现方式,都要求使用者彻底忘记 prototype. 以下代码在 MooTools 里也会出问题:

Dog.prototype.sleep = function() {
    this.parent(); // 会报错
};

在 MooTools 里,需要这样写:

Dog.implement({
    sleep: function() {
        this.parent();
    }
});

还有很多很多

JavaScript 的世界里,OO 的实现还有很多很多,比较有名气的还有:

还有一个很有意思、崇尚组合的:Traits.js

实现方式上都大同小异,有兴趣的可以逐一看看。

写文档比写代码还累呀,终于快接近尾声了 —— 最重要的尾声部分。如果你在家里的话,强烈建议去洗把冷水脸,清爽一下后再来看。

我们的选择

权衡考虑后,FNX选择 Class.create 风格,部分细节考虑如下:

  1. 主要 API 与 MooTools 保持一致,但不用 new Class, 而用 Class.createSomeClass.extend
  2. Implements 接收的参数就是普通对象,与 implement 方法保持一致。MooTools 中 Implements 属性需要是类。
  3. 去除 this.parent() 语法糖,需要调用时,和 Backbone 类似,推荐直接使用 SuperClass.prototype.methodName 来调用。
  4. 为了方便调用父类中的方法,提供 superclass 语法糖,与 YUI 类似。