感谢所有老前辈们,感谢所有同类代码。因为有了你们,世界才丰富多采。
关于类继承,Douglas 有一篇经典文章:
这篇文章里,老道分析了为什么要在 JavaScript 里模拟类继承:主要目的是复用。老道的实现方式是给
Function.prototype
增加 method
和 inherits
两个方法,并提供了 uber
语法糖。
悲催的是,大神实现的 inherits
和 uber
存在不少缺陷,国内和国外都有不少人剖析过,可以参考
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__
, prototype
和 new
等关键点有清晰的认识。通过 inherits
等方法,可以简化部分细节。但用户在使用时,依旧需要面对
prototype
等属性,并且很容易写出有隐患的代码,比如:
function Animal() {}
function Dog() {}
util.inherits(Dog, Animal);
Dog.prototype = {
talk: function() {},
run: function() {}
};
上面的代码,你知道问题在哪吗?请继续阅读。
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 是前端界的一位老前辈。老前辈做过一个当时很著名的 JavaScript 类库: Base.js, 其中有一套非常不错的 OO 实现:
这个方案开辟了一条阳光大道:通过精心构造的 Base
基类来实现类继承。同一时期,JavaScript
界 OO 模拟蔚然成风,万马奔腾。让我们继续考考古。
作为一名前端,如果没用过 Prototype, 那么恭喜你,说明你还年轻,潜力无限。来看一名老前端的吐槽:
Prototype 目前已经 v1.7 了。从官方文档来看,Class 继承已经很成熟:
Class.create
的写法已经比较优美。然而悲催的是,$super
的约定真让人无语。super
虽然很难实现,但也不要这样实现呀:代码一压缩就都浮云了。
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 的全称是 My OO Tools, 有一套口碑很不错的 Class 机制:
new Class
的方式很优美,Extends
和 Implements
的首字母大写,看习惯了也觉得挺好。
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
风格,部分细节考虑如下:
new Class
, 而用 Class.create
和 SomeClass.extend
。Implements
接收的参数就是普通对象,与 implement
方法保持一致。MooTools 中 Implements
属性需要是类。this.parent()
语法糖,需要调用时,和 Backbone 类似,推荐直接使用 SuperClass.prototype.methodName
来调用。superclass
语法糖,与 YUI 类似。