您的位置:首页 > Web前端 > JavaScript

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

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

Javascript代码

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
;

});

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代码

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

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代码

while
(t) {

// 往上追溯一级

v = v.constructor.prototype;

t -= 1;

}

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代码

p = (
this
.prototype =
new
parent());

p = (this.prototype = new parent());


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

Javascript代码

// 例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'
));
// => ?

// 例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代码

println(d6.getName());
// => D1,D5,D5

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代码

// 对于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来说, 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代码

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

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代码

return

'D1'
;

return 'D1';


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

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

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

Javascript代码

// 例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

// 例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代码

p = (
this
.prototype =
new
parent());

p = (this.prototype = new parent());


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

Javascript代码

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

});

// 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中支持类模型的尝试是一个错误。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: