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

先来看 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. });  
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]不好理解,我们来创建一些测试代码:

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   
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'), 这时下面这段代码发挥作用:

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


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

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

Javascript代码
  1. p = ( this .prototype =  new  parent());  
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' ));  // => ?   
// 例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, 然而实际结果是:

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

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来说, 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). 二级调用的代码可以简化为:

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 );  
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);


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

Javascript代码
  1. return   'D1' ;  
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   
// 例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陷阱的原因是:

Javascript代码
  1. p = ( this .prototype =  new  parent());  
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. });  
// 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中支持类模型的尝试是一个错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值