该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2008-10-04
最后修改:2009-01-16
岁月如歌,欢迎讨论交流。
原文同步发表在先来看 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这个陷阱导致的。 小结
后续 上面的分析花了一个晚上的时间,今天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中支持类模型的尝试是一个错误。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2008-10-04
原来John Hax就是本论坛的hax^o^
|
|
返回顶楼 | |
发表时间:2008-10-05
John的方案(John Resig)也是不错的,还有Hedger的,可以看看:
John http://ejohn.org/blog/simple-javascript-inheritance/ Hedger: http://www.hedgerwow.com/360/dhtml/js-simple-instantiation.html |
|
返回顶楼 | |
发表时间:2008-10-05
KKFC 写道 John的方案(John Resig)也是不错的,还有Hedger的,可以看看:
John http://ejohn.org/blog/simple-javascript-inheritance/ Hedger: http://www.hedgerwow.com/360/dhtml/js-simple-instantiation.html 嗯,john resig和hedger wang的方案不错,我会在接下来的学习笔记里继续分享我的理解 |
|
返回顶楼 | |
发表时间:2008-10-05
lifesinger 写道 原来John Hax就是本论坛的hax^o^
呵呵。偶从csdn搬到blogspot用了一段时间,不过因为被墙了,就很少去更新了。BTW,dc同志拿了我的补丁也不打招呼,偶心里很不爽的说,所以偶要坚定自己坚决不用yahoo和YUI的决心…… 搞笑完毕,说说正题。 dc的继承方案也只是万马奔腾中的一种。实际上有许多其他非常有意思的oo方案。譬如dean edwards的base。这个方案的特点是继承链是基于一个精心构造的base基类,相当于搞了一个Object2,而不会影响传统的Object的继承模式。另一个例子是prototype 1.6开始的Class继承,其特点是通过对源代码的分析来重写方法使之支持super调用(参见:http://hax.iteye.com/blog/167131),缺点是函数被包装了一层,可能存在潜在的问题。其他的方式也有,譬如爱民的qomo框架是编写了一个通用的base方法,然后再其中通过Function.caller回溯调用链来找到应该调用哪个的父类方法,缺点是依赖caller这个非ECMA标准特性。 总而言之,偶并不完全赞同dc的看法。存在的就是合理的。各个框架都搞oo方案,说明人民群众有这个需求。。。 |
|
返回顶楼 | |
发表时间:2008-10-06
还是extjs的继承方案比较实用,constructor的问题是js继承实现中较初级的问题吧,看样子这位js大仙也是玩玩概念,并没有实践他的“继承方法”。
|
|
返回顶楼 | |
发表时间:2008-10-06
jianfeng008cn 写道 还是extjs的继承方案比较实用,constructor的问题是js继承实现中较初级的问题吧,看样子这位js大仙也是玩玩概念,并没有实践他的“继承方法”。
这里的constructor陷阱,并不是简单的constructor未重置问题,而是 sp.prototype 的连续引用导致的 prototype是一个很普通的对象,在下面的注释里,就是一系列指针,最后都指向顶层的D1,这导致原本只想回溯一级,结果却直接回溯到了最顶层 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; } |
|
返回顶楼 | |
发表时间:2008-10-06
hax 写道 ... 总而言之,偶并不完全赞同dc的看法。存在的就是合理的。各个框架都搞oo方案,说明人民群众有这个需求。。。 oo没错,但用传统的类模型套在javascript的原型模式上,总感觉有点别扭。为什么一定要子类、父类这些概念呢?this.super真的必须吗? 归根结底,我们要解决的问题是提高代码的可复用、可维护和可扩展。只要能达到目标就行,管它面向过程还是面向对象呢。传统的类模式的确能非常好的解决这些问题,因此在针对原型模式的设计模式总结出来之前,模拟类模型是最简单的一条路 但原型模式、动态语言,毕竟不同于传统的C++, Java, 更多的是一种编程思想的转变。怎样适应这种转变?在动态编程思想下怎样体现传统oo里的设计模式甚至创造出新的更好的模式?我觉得这些是很值得研究的。 存在即合理,但存在的未必是好的,人民群众有这个需求,也许是因为人民群众还未找到新的路,只好依着老路子走罢了…… |
|
返回顶楼 | |
发表时间:2008-10-06
lifesinger 写道 jianfeng008cn 写道 还是extjs的继承方案比较实用,constructor的问题是js继承实现中较初级的问题吧,看样子这位js大仙也是玩玩概念,并没有实践他的“继承方法”。
这里的constructor陷阱,并不是简单的constructor未重置问题,而是 sp.prototype 的连续引用导致的 prototype是一个很普通的对象,在下面的注释里,就是一系列指针,最后都指向顶层的D1,这导致原本只想回溯一级,结果却直接回溯到了最顶层 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; } 什么意思啊? 不就是连续引用导致constructor语义失真了嘛,D5.constructor应该是D5,但是这里成了D1了。 说白了还是constructor未重置的问题啊,因为这里修改了函数的prototype ,自然会导致constructor和原始语义不符合,需要重置也是清理之中。 |
|
返回顶楼 | |
发表时间:2008-10-06
jianfeng008cn 写道 什么意思啊? 不就是连续引用导致constructor语义失真了嘛,D5.constructor应该是D5,但是这里成了D1了。 说白了还是constructor未重置的问题啊,因为这里修改了函数的prototype ,自然会导致constructor和原始语义不符合,需要重置也是清理之中。 这样理解也行,呵呵 |
|
返回顶楼 | |