现在的位置: 首页 > 综合 > 正文

JavaScript中的继承学习笔记(1):Crockford uber方法中的陷阱

2014年01月04日 ⁄ 综合 ⁄ 共 10845字 ⁄ 字号 评论关闭

先来看 Douglas Crockford 的经典文章:Classical Inheritance in JavaScript
. 此文的关键技巧是给Function.prototype增加inherits方法,代码如下(注释是我的理解):

Javascript代码
  1. Function.prototype.method = 
    function
     (name, func) {  
  2.     this
    .prototype[name] = func;  
  3.     return
     
    this
    ;  
  4. };  
  5. Function.method('inherits'

    function
     (parent) {  
  6.     var
     d = {}, 
    // 递归调用时的计数器
      
  7.         // 下面这行已经完成了最简单的原型继承:将子类的prototype设为父类的实例
      
  8.         p = (this
    .prototype = 
    new
     parent());  
  9.       
  10.     // 下面给子类增加uber方法(类似Java中的super方法),以调用上层继承链中的方法
      
  11.     this
    .method(
    'uber'

    function
     uber(name) {  
  12.         if
     (!(name 
    in
     d)) {  
  13.             d[name] = 0;  
  14.         }  
  15.         var
     f, r, t = d[name], v = parent.prototype;  
  16.         if
     (t) {  
  17.             while
     (t) {  
  18.                 // 往上追溯一级
      
  19.                 v = v.constructor.prototype;  
  20.                 t -= 1;  
  21.             }  
  22.             f = v[name];  
  23.         } else
     {  
  24.             f = p[name];  
  25.             if
     (f == 
    this
    [name]) {  
  26.                 f = v[name];  
  27.             }  
  28.         }  
  29.         // 因为f函数中,可能存在uber调用上层的f
      
  30.         // 不设置d[name]的话,将导致获取的f始终为最近父类的f(陷入死循环)
      
  31.         d[name] += 1;         
  32.         // slice.apply的作用是将第2个及其之后的参数转换为数组
      
  33.         // 第一个参数就是f的名字,无需传递
      
  34.         // 这样,通过uber调用上层方法时可以传递参数:
      
  35.         // sb.uber(methodName, arg1, arg2, ...);
      
  36.         r = f.apply(this
    , Array.prototype.slice.apply(arguments, [1]));       
  37.         // 还原计数器
      
  38.         d[name] -= 1;         
  39.         return
     r;  
  40.     });  
  41.     // 返回this, 方便chain操作
      
  42.     return
     
    this
    ;  
  43. });  


上面d[name]不好理解,我们来创建一些测试代码:

Javascript代码
  1. function
     println(msg) {  
  2.     document.write(msg + '<br />'
    );  
  3. }  
  4.   
  5. // 例1
      
  6. function
     A() { }  
  7. A.prototype.getName = function
     () { 
    return
     
    'A'
    ; }; 
    // @1
      
  8.   
  9. function
     B() { }  
  10. B.inherits(A);  
  11. B.prototype.getName = function
     () { 
    return
     
    this
    .uber(
    'getName'
    ) + 
    ',B'
    ; }; 
    // @2
      
  12.   
  13. function
     C() { }  
  14. C.inherits(B);  
  15. C.prototype.getName = function
     () { 
    return
     
    this
    .uber(
    'getName'
    ) + 
    ',C'
    ; }; 
    // @3
      
  16.   
  17. var
     c = 
    new
     C();  
  18. println(c.getName()); // => A,B,C
      
  19. println(c.uber('getName'
    )); 
    // => A,B
      


c.getName()调用的是@3, @3中的uber调用了@2. 在@2中,又有this.uber('getName'), 这时下面这段代码发挥作用:

Javascript代码
  1. while
     (t) {  
  2.     // 往上追溯一级
      
  3.     v = v.constructor.prototype;  
  4.     t -= 1;  
  5. }  
  6. f = v[name];  


可以看出,d[name]表示的是递归调用时的层级。如果不设此值,@2中的this.uber将指向@2本身,这将导致死循环。Crockford借助d[name]实现了uber对同名方法的递归调用。

uber只是一个小甜点。类继承中最核心最关键的是下面这一句:

Javascript代码
  1. p = (
    this
    .prototype = 
    new
     parent());  


将子类的原型设为父类的一个实例,这样子类就拥有了父类的成员,从而实现了一种最简单的类继承机制。
注意JavaScript中,获取obj.propName时,会自动沿着prototype链往上寻找。这就让问题变得有意思起来了:

Javascript代码
  1. // 例2
      
  2. function
     D1() {}  
  3. D1.prototype.getName = function
    () { 
    return
     
    'D1'
     }; 
    // @4
      
  4.   
  5. function
     D2() {}  
  6. D2.inherits(D1);  
  7. D2.prototype.getName = function
     () { 
    return
     
    this
    .uber(
    'getName'
    ) + 
    ',D2'
    ; }; 
    // @5
      
  8.   
  9. function
     D3() {}  
  10. D3.inherits(D2);  
  11.   
  12. function
     D4() {}  
  13. D4.inherits(D3);  
  14.   
  15. function
     D5() {}  
  16. D5.inherits(D4);  
  17. D5.prototype.getName = function
     () { 
    return
     
    this
    .uber(
    'getName'
    ) + 
    ',D5'
    ; }; 
    // @6
      
  18.   
  19. function
     D6() {}  
  20. D6.inherits(D5);  
  21.   
  22. var
     d6 = 
    new
     D6();  
  23. println(d6.getName()); // => ?
      
  24. println(d6.uber('getName'
    )); 
    // => ?
      


猜猜最后两行输出什么?按照uber方法设计的原意,上面两行都应该输出D1,D2,D5, 然而实际结果是:

Javascript代码
  1. println(d6.getName()); 
    // => D1,D5,D5
      
  2. 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的代码可以简化为:

Javascript代码
  1. // 对于d6来说, parent == D5
      
  2. var
     f, v = parent.prototype;  
  3. f = p[name];  
  4. // 对于d6来说,p[name] == this[name]
      
  5. if
     (f == 
    this
    [name]) {  
  6.     // 因此f = D5.prototype[name]
      
  7.     f = v[name];  
  8. }  
  9.   
  10. // 计数器加1
      
  11. d[name] += 1;  
  12.   
  13. // 等价为 D5.prototype.getName.apply(d6);
      
  14. f.apply(this
    );  


至此,一级调用d6.getName()跳转进入二级递归调用D5.prototype.getName.apply(d6). 二级调用的代码可以简化为:

Javascript代码
  1. var
     f, t = 1, v = D5.prototype;  
  2. while
     (t) {  
  3.     // 这里有个陷阱,v.constructor == D1
      
  4.     // 因为 this.prototype = new parent(), 形成了下面的指针链:
      
  5.     // D5.prototype = d4
      
  6.     // D4.prototype = d3
      
  7.     // D3.prototype = d2
      
  8.     // D2.prototype = d1
      
  9.     // 因此v.constructor == d1.constructor
      
  10.     // 而d1.constructor == D1.prototype.constructor
      
  11.     // D1.prototype.constructor指向D1本身,因此最后v.constructor = D1
      
  12.     v = v.constructor.prototype;  
  13.     t -= 1;  
  14. }  
  15. // 这时f = D1.prototype.getName
      
  16. f = v[name];  
  17.   
  18. d[name] += 1;  
  19. // 等价为 D1.prototype.getName.apply(d6)
      
  20. f.apply(this
    );  


上面的代码产生最后一层调用:

Javascript代码
  1. return
     
    'D1'
    ;  


因此d6.getName()的输出是D1,D5,D5.

同理分析,可以得到d6.uber('getName')的输出是D1,D5.

上面分析了“断层”时uber方法中的错误。注意上面提到的v.constructor.prototype产生的陷阱,这个陷阱在“非断层”的理想继承链中也会产生错误:

Javascript代码
  1. // 例3
      
  2. function
     F1() { }  
  3. F1.prototype.getName = function
    () { 
    return
     
    'F1'
    ; };  
  4.   
  5. function
     F2() { }  
  6. F2.inherits(F1);  
  7. F2.prototype.getName = function
    () { 
    return
     
    this
    .uber(
    'getName'
    ) + 
    ',F2'
    ; };  
  8.   
  9. function
     F3() { }  
  10. F3.inherits(F2);  
  11. F3.prototype.getName = function
    () { 
    return
     
    this
    .uber(
    'getName'
    ) + 
    ',F3'
    ; };  
  12.   
  13. function
     F4() { }  
  14. F4.inherits(F3);  
  15. F4.prototype.getName = function
    () { 
    return
     
    this
    .uber(
    'getName'
    ) + 
    ',F4'
    ; };  
  16.   
  17. var
     f3 = 
    new
     F3();  
  18. println(f3.getName()); // => F1,F2,F3
      
  19.   
  20. var
     f4 = 
    new
     F4();  
  21. 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陷阱的原因是:

Javascript代码
  1. p = (
    this
    .prototype = 
    new
     parent());  


上面这句导致while语句中v.constructor始终指向继承链最顶层的constructor. 分析出了原因,patch就简单了:

Javascript代码
  1. // patched by lifesinger@gmail.com 2008/10/4
      
  2. Function.method('inherits'

    function
     (parent) {  
  3.     var
     d = { },   
  4.         p = (this
    .prototype = 
    new
     parent());  
  5.         // 还原constructor
      
  6.         p.constructor = this
    ;  
  7.         // 添加superclass属性
      
  8.         p.superclass = parent;  
  9.                   
  10.     this
    .method(
    'uber'

    function
     uber(name) {  
  11.         if
     (!(name 
    in
     d)) {  
  12.             d[name] = 0;  
  13.         }  
  14.         var
     f, r, t = d[name], v = parent.prototype;  
  15.         if
     (t) {  
  16.             while
     (t) {  
  17.                 // 利用superclass来上溯,避免contructor陷阱
      
  18.                 v = v.superclass.prototype;  
  19.                 // 跳过“断层”的继承点
      
  20.                 if
    (v.hasOwnProperty(name)) {  
  21.                     t -= 1;  
  22.                 }  
  23.             }  
  24.             f = v[name];  
  25.         } else
     {  
  26.             f = p[name];  
  27.             if
     (f == 
    this
    [name]) {  
  28.                 f = v[name];  
  29.             }  
  30.         }  
  31.         d[name] += 1;          
  32.         if
    (f == 
    this
    [name]) { 
    // this[name]在父类中的情景
      
  33.             r = this
    .uber.apply(
    this
    , Array.prototype.slice.apply(arguments));  
  34.         } else
     {  
  35.             r = f.apply(this
    , Array.prototype.slice.apply(arguments, [1]));  
  36.         }  
  37.         d[name] -= 1;  
  38.         return
     r;  
  39.     });  
  40.     return
     
    this
    ;  
  41. });  


测试页面:crockford_classic_inheritance_test.html

最后以Douglas Crockford的总结结尾:

引用


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

抱歉!评论已关闭.