Crockford uber方法中的陷阱
先来看Douglas Crockford的经典文章:Classical Inheritance in JavaScript. 此文的关键技巧是给Function.prototype增加inherits方法,代码如下(注释是我的理解):
Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
};
Function.method('inherits', function (parent) {
var d = {}, // 递归调用时的计数器
// 下面这行已经完成了最简单的原型继承:将子类的prototype设为父类的实例
p = (this.prototype = new parent());
// 下面给子类增加uber方法(类似Java中的super方法),以调用上层继承链中的方法
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];
}
}
// 因为f函数中,可能存在uber调用上层的f
// 不设置d[name]的话,将导致获取的f始终为最近父类的f(陷入死循环)
d[name] += 1;
// slice.apply的作用是将第2个及其之后的参数转换为数组
// 第一个参数就是f的名字,无需传递
// 这样,通过uber调用上层方法时可以传递参数:
// sb.uber(methodName, arg1, arg2, ...);
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
// 还原计数器
d[name] -= 1;
return r;
});
// 返回this, 方便chain操作
return this;
});
上面d[name]不好理解,我们来创建一些测试代码:
function println(msg) {
document.write(msg + '<br />');
}
// 例1
function A() { }
A.prototype.getName = function () { return 'A'; }; // @1
function B() { }
B.inherits(A);
B.prototype.getName = function () { return this.uber('getName') + ',B'; }; // @2
function C() { }
C.inherits(B);
C.prototype.getName = function () { return this.uber('getName') + ',C'; }; // @3
var c = new C();
println(c.getName()); // => A,B,C
println(c.uber('getName')); // => A,B
c.getName()调用的是@3, @3中的uber调用了@2. 在@2中,又有this.uber(‘getName’), 这时下面这段代码发挥作用:
while (t) {
// 往上追溯一级
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
可以看出,d[name]表示的是递归调用时的层级。如果不设此值,@2中的this.uber将指向@2本身,这将导致死循环。Crockford借助d[name]实现了uber对同名方法的递归调用。
uber只是一个小甜点。类继承中最核心最关键的是下面这一句:
p = (this.prototype = new parent());
将子类的原型设为父类的一个实例,这样子类就拥有了父类的成员,从而实现了一种最简单的类继承机制。注意JavaScript中,获取obj.propName时,会自动沿着prototype链往上寻找。这就让问题变得有意思起来了:
// 例2
function D1() {}
D1.prototype.getName = function() { return 'D1' }; // @4
function D2() {}
D2.inherits(D1);
D2.prototype.getName = function () { return this.uber('getName') + ',D2'; }; // @5
function D3() {}
D3.inherits(D2);
function D4() {}
D4.inherits(D3);
function D5() {}
D5.inherits(D4);
D5.prototype.getName = function () { return this.uber('getName') + ',D5'; }; // @6
function D6() {}
D6.inherits(D5);
var d6 = new D6();
println(d6.getName()); // => ?
println(d6.uber('getName')); // => ?
猜猜最后两行输出什么?按照uber方法设计的原意,上面两行都应该输出D1,D2,D5, 然而实际结果是:
println(d6.getName()); // => D1,D5,D5
println(d6.uber('getName')); // => D1,D5
这是因为Crockford的inherits方法中,考虑的是一种理想情况(如例1),对于例2这种有“断层”的多层继承,d[name]的设计就不妥了。我们来分析下调用链:
d6.getName()首先在d6对象中寻找是否有getName方法,发现没有,于是到D6.prototype(一个d5对象)中继续寻找,结果d5中也没有,于是到D5.protoype中寻找,这次找到了getName方法。找到后,立刻执行,注意this指向的是d6. this.uber(‘getName’)此时表示的是d6.uber(‘getName’). 获取f的代码可以简化为:
// 对于d6来说, parent == D5
var f, v = parent.prototype;
f = p[name];
// 对于d6来说,p[name] == this[name]
if (f == this[name]) {
// 因此f = D5.prototype[name]
f = v[name];
}
// 计数器加1
d[name] += 1;
// 等价为 D5.prototype.getName.apply(d6);
f.apply(this);
至此,一级调用d6.getName()跳转进入二级递归调用D5.prototype.getName.apply(d6). 二级调用的代码可以简化为:
var f, t = 1, v = D5.prototype;
while (t) {
// 这里有个陷阱,v.constructor == D1
// 因为 this.prototype = new parent(), 形成了下面的指针链:
// D5.prototype = d4
// D4.prototype = d3
// D3.prototype = d2
// D2.prototype = d1
// 因此v.constructor == d1.constructor
// 而d1.constructor == D1.prototype.constructor
// D1.prototype.constructor指向D1本身,因此最后v.constructor = D1
v = v.constructor.prototype;
t -= 1;
}
// 这时f = D1.prototype.getName
f = v[name];
d[name] += 1;
// 等价为 D1.prototype.getName.apply(d6)
f.apply(this);
上面的代码产生最后一层调用:
return 'D1';
因此d6.getName()的输出是D1,D5,D5.
同理分析,可以得到d6.uber(‘getName’)的输出是D1,D5.
上面分析了“断层”时uber方法中的错误。注意上面提到的v.constructor.prototype产生的陷阱,这个陷阱在“非断层”的理想继承链中也会产生错误:
// 例3
function F1() { }
F1.prototype.getName = function() { return 'F1'; };
function F2() { }
F2.inherits(F1);
F2.prototype.getName = function() { return this.uber('getName') + ',F2'; };
function F3() { }
F3.inherits(F2);
F3.prototype.getName = function() { return this.uber('getName') + ',F3'; };
function F4() { }
F4.inherits(F3);
F4.prototype.getName = function() { return this.uber('getName') + ',F4'; };
var f3 = new F3();
println(f3.getName()); // => F1,F2,F3
var f4 = new F4();
println(f4.getName()); // => F1,F3,F4
很完美的一个类继承链,但f4.getName()没有产生预料中的输出,这就是v.constructor.prototype这个陷阱导致的。
小结
- 在JavaScript中,模拟传统OO模型来实现类继承不是一个很好的选择(上面想实现一个uber方法都困难重重)。
- 在JavaScript中,考虑多重继承时,要非常小心。尽可能避免多重继承,保持简单性。
- 理解JavaScript中的普通对象,Function对象,Function对象的prototype和constructor, 以及获取属性时的原型追溯路径非常重要。(比如上面提到的constructor陷阱)
- Crockford是JavaScript界的大仙级人物,但其代码中依旧有陷阱和错误。刚开始我总怀疑是不是自己理解错了,费了牛劲剖析了一把,才敢肯定是Crockford考虑不周,代码中的错误是的的确确存在的。学习时保持怀疑的态度非常重要。
后续
上面的分析花了一个晚上的时间,今天google了一把,发现对Crockford的uber方法中的错误能搜到些零星文章,还有人给出了修正方案(忍不住八卦一把:从链接上看,是CSDN上的一位兄弟第一次指出了Crockford uber方法中的这个bug,然后John Hax(估计也是个华人)给出了修正方案。更有趣的是,Crockford不知从那里得知了这个bug, 如今Classical Inheritance in JavaScript这篇文章中已经是修正后的版本^o^)。
这里发现的uber方法中的constructor陷阱,尚无人提及。导致constructor陷阱的原因是:
p = (this.prototype = new parent());
上面这句导致while语句中v.constructor始终指向继承链最顶层的constructor. 分析出了原因,patch就简单了:
// patched by lifesinger@gmail.com 2008/10/4
Function.method('inherits', function (parent) {
var d = { },
p = (this.prototype = new parent());
// 还原constructor
p.constructor = this;
// 添加superclass属性
p.superclass = 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) {
// 利用superclass来上溯,避免contructor陷阱
v = v.superclass.prototype;
// 跳过“断层”的继承点
if(v.hasOwnProperty(name)) {
t -= 1;
}
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d[name] += 1;
if(f == this[name]) { // this[name]在父类中的情景
r = this.uber.apply(this, Array.prototype.slice.apply(arguments));
} else {
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
}
d[name] -= 1;
return r;
});
return this;
});
测试页面:crockford_classic_inheritance_test.html
最后以Douglas Crockford的总结结尾:
我编写JavaScript已经8个年头了,从来没有一次觉得需要使用uber方法。在类模式中,super的概念相当重要;但是在原型和函数式模式中,super的概念看起来是不必要的。现在回顾起来,我早期在JavaScript中支持类模型的尝试是一个错误。

October 6th, 2008 on 7:47
from hax@je:
呵呵。偶从csdn搬到blogspot用了一段时间,不过因为被墙了,就很少去更新了。BTW,dc同志拿了我的补丁也不打招呼,偶心里很不爽的说,所以偶要坚定自己坚决不用yahoo和YUI的决心……
搞笑完毕,说说正题。
dc的继承方案也只是万马奔腾中的一种。实际上有许多其他非常有意思的oo方案。譬如dean edwards的base。这个方案的特点是继承链是基于一个精心构造的base基类,相当于搞了一个Object2,而不会影响传统的Object的继承模式。另一个例子是prototype 1.6开始的Class继承,其特点是通过对源代码的分析来重写方法使之支持super调用(参见:http://hax.javaeye.com/blog/167131 ),缺点是函数被包装了一层,可能存在潜在的问题。其他的方式也有,譬如爱民的qomo框架是编写了一个通用的base方法,然后再其中通过Function.caller回溯调用链来找到应该调用哪个的父类方法,缺点是依赖caller这个非ECMA标准特性。
总而言之,偶并不完全赞同dc的看法。存在的就是合理的。各个框架都搞oo方案,说明人民群众有这个需求。。。
October 6th, 2008 on 10:07
to hax:
oo没错,但用传统的类模型套在javascript的原型模式上,总感觉有点别扭。为什么一定要子类、父类这些概念呢?this.super真的必须吗?
归根结底,我们要解决的问题是提高代码的可复用、可维护和可扩展。只要能达到目标就行,管它面向过程还是面向对象呢。传统的类模式的确能非常好的解决这些问题,因此在针对原型模式的设计模式总结出来之前,模拟类模型是最简单的一条路
但原型模式、动态语言,毕竟不同于传统的C++, Java, 更多的是一种编程思想的转变。怎样适应这种转变?在动态编程思想下怎样体现传统oo里的设计模式甚至创造出新的更好的模式?我觉得这些是很值得研究的。
存在即合理,但存在的未必是好的,人民群众有这个需求,也许是因为人民群众还未找到新的路,只好依着老路子走罢了……
October 10th, 2008 on 21:04
关于OO的产生和流行,可能是因为它迎合了人类原始的思考方式吧。
在js中完全模拟oo中的概念,让人闻到过度设计的味道,让某些人感觉别扭也就没什么奇怪的了。
关于大众需求,可与说,群众掌握了大部分真理。
新的真理只能被少数人掌握。而且在长时间里,这些都不能被大众接受。
这些问题,我们五千年的历史就是厚厚的证词。
January 8th, 2009 on 1:24
发现了又一个陷阱
http://blog.12km.com/index.php/archives/35/
April 15th, 2009 on 16:02
看来不能盲目崇拜权威
December 3rd, 2009 on 22:59
p = (this.prototype = new parent());
不明的上面为什么会加上括号
是为了代码简洁还是有什么其它 的原因
December 8th, 2009 on 10:10
让我来把这整个故事整理一下,如果有问题,请大家指正和补充。
跟这个故事有关的人物:
1. Douglas Crockford (DC)
2. chensheng
3. John Hax 贺师俊
4. lifesinger 射雕
首先是DC在他的网站上发表了一篇有关Javascript继承文章: Classical Inheritance in JavaScript,其中提出了inherits方法,这个方法实现很复杂,其中有包含有uber方法;
结果chensheng发现了其中关于uber方法的一个错误,发表在CSDN上,时间是2006年12月28日;
John Hax 与2006年12月31日在CSDN上连续发表评论,并且给出修正方法,同时还在在英文网站上给出修正;
DC不知何时(估计是2007年)看到了John Hax的文章,并且修改了最初的文章,完全采用了Hax的修正(但是没有任何感谢Hax的表示,这让Hax很不高兴),DC同时在他的文章最后补充说:他写了8年的Javascript,从来没有用过uber方法,(这是否意味着uber方法没什么实际用途,大家都在瞎忙活?我不太懂);
在这篇文章中,lifesinger似乎又发现了uber方法的另一个漏洞或陷阱(这一点我不是很确定,麻烦lifesinger再确认一下,应为感觉uber方法没什么用途,所以本人还没有认真阅读)。
后话:
DC在2008年出版了他的大作Javascript: The Good Parts,有中文版,译者之一(小马)应该是lifesinger的同事。在书中的48页,提到了inherits方法,这一回的inherits实现很简单,完全没有提到uber方法,我不清楚DC为什么这么做?是不是因为DC发现复杂的uber方法根本没有用,而且很容易出错,干脆拿掉算了,这一点希望高手指点迷津。
象DC这样的大师或大牛,我们当然要多学习他们的文章或者观点,但是我们可能也还要保持独立的观点,大师也是人,也会犯错,而大师一犯错,错误也会是影响深远的。比如这个uber方法,Javascript的另一个大人物John Resig就在他的大作中(Pro JavaScript Techniques,也有中文版了)完全引用了uber方法(书中第3章),而且引用的是DC的错误的老版本(可惜了,又一个错误被大人物传开了,而且这个错误的东西似乎没有多大的用处)。
December 8th, 2009 on 10:51
@约翰:感谢你的整理。uber 的 bug, chensheng 发现了一个,hax 给出了解决办法。我这里是 uber 的另一个隐晦 bug, 也给出了解决办法。
这个方法,是有传统 OO 思想的程序员情节,对于 JS 本身来说,uber 没啥用。这也就是为何 DC 说 8 年没用一次的原因。
JS 有更简洁方便继承体系,《JavaScript: The Good Parts》中的 inherits 是一种。
December 8th, 2009 on 12:17
谢谢lifesinger的确认,不知道你是否把你的发现和解决方法告知DC?
December 8th, 2009 on 15:41
@约翰:发过邮件给 DC, 不过没有任何回复。可能太忙了。
leave a reply