详解JavaScript中命名函数表达式所存在的bug
英文原文:http://kangax.github.com/nfe/
前言
函数表达式和函数声明
函数声明:
function 函数名称 (参数:可选){ 函数体 }
函数表达式:
function 函数名称(可选)(参数:可选){ 函数体 }
function foo(){} // 声明,因为它是程序的一部分
var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
new function bar(){}; // 表达式,因为它是new表达式
(function(){
function bar(){} // 声明,因为它是函数体的一部分
})();
function foo(){} // 函数声明
(function foo(){}); // 函数表达式:包含在分组操作符内
try {
(var x = 5); // 分组操作符,只能包含表达式而不能包含语句:这里的var就是语句
} catch(err) {
// SyntaxError
}
try { { "x": 5 }; // "{" 和 "}" 做解析成代码块 } catch(err) { // SyntaxError } ({ "x": 5 }); // 分组操作符强制将"{" 和 "}"作为对象字面量来解析
alert(fn());
function fn() {
return 'Hello world!';
}
// 千万别这样做!
// 因为有的浏览器会返回first的这个function,而有的浏览器返回的却是第二个
if (true) {
function foo() {
return 'first';
}
}
else {
function foo() {
return 'second';
}
}
foo();
// 相反,这样情况,我们要用函数表达式
var foo;
if (true) {
foo = function() {
return 'first';
};
}
else {
foo = function() {
return 'second';
};
}
foo();
函数语句
if (true) {
function f(){ }
}
else {
function f(){ }
}
if (true) {
function foo(){ return 1; }
}
else {
function foo(){ return 2; }
}
foo(); // 1
// 注:其它客户端会将foo解析成函数声明
// 因此,第二个foo会覆盖第一个,结果返回2,而不是1
// 此刻,foo还没用声明
typeof foo; // "undefined"
if (true) {
// 进入这里以后,foo就被声明在整个作用域内了
function foo(){ return 1; }
}
else {
// 从来不会走到这里,所以这里的foo也不会被声明
function foo(){ return 2; }
}
typeof foo; // "function"
var foo;
if (true) {
foo = function foo(){ return 1; };
}
else {
foo = function foo() { return 2; };
}
if (true) {
function foo(){ return 1; }
}
String(foo); // function foo() { return 1; }
// 函数声明
function foo(){ return 1; }
if (true) {
// 用函数语句重写
function foo(){ return 2; }
}
foo(); // FF3以下返回1,FF3.5以上返回2
// 不过,如果前面是函数表达式,则没用问题
var foo = function(){ return 1; };
if (true) {
function foo(){ return 2; }
}
foo(); // 所有版本都返回2
命名函数表达式
// 该代码来自Garrett Smith的APE Javascript library库(http://dhtmlkitchen.com/ape/)
var contains = (function() {
var docEl = document.documentElement;
if (typeof docEl.compareDocumentPosition != 'undefined') {
return function(el, b) {
return (el.compareDocumentPosition(b) & 16) !== 0;
};
}
else if (typeof docEl.contains != 'undefined') {
return function(el, b) {
return el !== b && el.contains(b);
};
}
return function(el, b) {
if (el === b) return false;
while (el != b && (b = b.parentNode) != null);
return el === b;
};
})();
var f = function foo(){
return typeof foo; // foo是在内部作用域内有效
};
// foo在外部用于是不可见的
typeof foo; // "undefined"
f(); // "function"
调试器中的函数名
function foo(){
return bar();
}
function bar(){
return baz();
}
function baz(){
debugger;
}
foo();
// 这里我们使用了3个带名字的函数声明
// 所以当调试器走到debugger语句的时候,Firebug的调用栈上看起来非常清晰明了
// 因为很明白地显示了名称
baz
bar
foo
expr_test.html()
function foo(){
return bar();
}
var bar = function(){
return baz();
}
function baz(){
debugger;
}
foo();
// Call stack baz
bar() //看到了么?
foo
expr_test.html()
function foo(){
return bar();
}
var bar = (function(){
if (window.addEventListener) {
return function(){
return baz();
};
}
else if (window.attachEvent) {
return function() {
return baz();
};
}
})();
function baz(){
debugger;
}
foo();
// Call stack baz
(?)() // 这里可是问号哦
foo
expr_test.html()
function foo(){
return baz();
}
var bar = function(){
debugger;
};
var baz = bar;
bar = function() {
alert('spoofed');
};
foo();
// Call stack: bar()
foo
expr_test.html()
function foo(){
return bar();
}
var bar = (function(){
if (window.addEventListener) {
return function bar(){
return baz();
};
}
else if (window.attachEvent) {
return function bar() {
return baz();
};
}
})();
function baz(){
debugger;
}
foo();
// 又再次看到了清晰的调用栈信息了耶! baz
bar
foo
expr_test.html()
JScript的Bug
下面我们就来看看IE在实现中究竟犯了那些错误,俗话说知已知彼,才能百战不殆。我们来看看如下几个例子:
例1:函数表达式的标示符泄露到外部作用域
var f = function g(){};
typeof g; // "function"
例2:将命名函数表达式同时当作函数声明和函数表达式
typeof g; // "function"
var f = function g(){};
这个例子引出了下一个例子。
例3:命名函数表达式会创建两个截然不同的函数对象!
var f = function g(){};
f === g; // false
f.expando = 'foo';
g.expando; // undefined
再来看一个稍微复杂的例子:
var f = function g() {
return 1;
};
if (false) {
f = function g(){
return 2;
};
}
g(); // 2
var f = function g(){
return [
arguments.callee == f,
arguments.callee == g
];
};
f(); // [true, false] g(); // [false, true]
(function(){
f = function f(){};
})();
JScript的内存管理
var f = (function(){
if (true) {
return function g(){};
}
return function g(){};
})();
var f = (function(){
var f, g;
if (true) {
f = function g(){};
}
else {
f = function g(){};
}
// 设置g为null以后它就不会再占内存了 g = null;
return f;
})();
function createFn(){
return (function(){
var f;
if (true) {
f = function F(){
return 'standard';
};
}
else if (false) {
f = function F(){
return 'alternative';
};
}
else {
f = function F(){
return 'fallback';
};
}
// var F = null;
return f;
})();
}
var arr = [ ];
for (var i=0; i<10000; i++) {
arr[i] = createFn();
}
IE6:
without `null`: 7.6K -> 20.3K
with `null`: 7.6K -> 18K
IE7:
without `null`: 14K -> 29.7K
with `null`: 14K -> 27K
SpiderMonkey的怪癖
Object.prototype.x = 'outer';
(function(){
var x = 'inner';
/*
函数foo的作用域链中有一个特殊的对象——用于保存函数的标识符。这个特殊的对象实际上就是{ foo:
}。 当通过作用域链解析x时,首先解析的是foo的局部环境。如果没有找到x,则继续搜索作用域链中的下一个对象。下一个对象
就是保存函数标识符的那个对象——{ foo:
},由于该对象继承自Object.prototype,所以在此可以找到x。 而这个x的值也就是Object.prototype.x的值(outer)。结果,外部函数的作用域(包含x = 'inner'的作用域)就不会被解析了。
*/
(function foo(){
alert(x); // 提示框中显示:outer
})();
})();
另一个把内部对象实现为全局Object对象的是黑莓(Blackberry)浏览器。目前,它的活动对象(Activation Object)仍然继承Object.prototype。可是,ECMA-262并没有说活动对象也要“像调用new Object()表达式那样”来创建(或者说像创建保存NFE标识符的对象一样创建)。人家规范只说了活动对象是规范中的一种机制。
Object.prototype.x = 'outer';
(function(){
var x = 'inner';
(function(){
/*
在沿着作用域链解析x的过程中,首先会搜索局部函数的活动对象。当然,在该对象中找不到x。
可是,由于活动对象继承自Object.prototype,因此搜索x的下一个目标就是Object.prototype;而
Object.prototype中又确实有x的定义。结果,x的值就被解析为——outer。跟前面的例子差不多,
包含x = 'inner'的外部函数的作用域(活动对象)就不会被解析了。
*/
alert(x); // 显示:outer
})();
})();
(function(){
var constructor = function(){ return 1; };
(function(){
constructor(); // 求值结果是{}(即相当于调用了Object.prototype.constructor())而不是1
constructor === Object.prototype.constructor; // true
toString === Object.prototype.toString; // true
// ……
})();
})();
var fn = (function(){
// 声明要引用函数的变量
var f;
// 有条件地创建命名函数
// 并将其引用赋值给f
if (true) {
f = function F(){ }
}
else if (false) {
f = function F(){ }
}
else {
f = function F(){ }
}
// 声明一个与函数名(标识符)对应的变量,并赋值为null
// 这实际上是给相应标识符引用的函数对象作了一个标记,
// 以便垃圾回收器知道可以回收它了
var F = null;
// 返回根据条件定义的函数
return f;
})();
// 1) 使用独立的作用域包含声明
var addEvent = (function(){
var docEl = document.documentElement;
// 2) 声明要引用函数的变量
var fn;
if (docEl.addEventListener) {
// 3) 有意给函数一个描述性的标识符
fn = function addEvent(element, eventName, callback) {
element.addEventListener(eventName, callback, false);
}
}
else if (docEl.attachEvent) {
fn = function addEvent(element, eventName, callback) {
element.attachEvent('on' + eventName, callback);
}
}
else {
fn = function addEvent(element, eventName, callback) {
element['on' + eventName] = callback;
}
}
// 4) 清除由JScript创建的addEvent函数
// 一定要保证在赋值前使用var关键字
// 除非函数顶部已经声明了addEvent
var addEvent = null;
// 5) 最后返回由fn引用的函数
return fn;
})();
替代方案
var hasClassName = (function(){
// 定义私有变量
var cache = { };
// 使用函数声明
function hasClassName(element, className) {
var _className = '(?:^|\\s+)' + className + '(?:\\s+|$)';
var re = cache[_className] || (cache[_className] = new RegExp(_className));
return re.test(element.className);
}
// 返回函数
return hasClassName;
})();
var addEvent = (function(){
var docEl = document.documentElement;
function addEventListener(){
/* ... */
}
function attachEvent(){
/* ... */
}
function addEventAsProperty(){
/* ... */
}
if (typeof docEl.addEventListener != 'undefined') {
return addEventListener;
}
elseif (typeof docEl.attachEvent != 'undefined') {
return attachEvent;
}
return addEventAsProperty;
})();
'addEvent', 'altAddEvent', 'fallbackAddEvent'
// 或者
'addEvent', 'addEvent2', 'addEvent3'
// 或者
'addEvent_addEventListener', 'addEvent_attachEvent', 'addEvent_asProperty'
WebKit的displayName
未来考虑
// 此前,你可能会使用arguments.callee
(function(x) {
if (x <= 1) return 1;
return x * arguments.callee(x - 1);
})(10);
// 但在严格模式下,有可能就要使用命名函数表达式
(function factorial(x) {
if (x <= 1) return 1;
return x * factorial(x - 1);
})(10);
// 要么就退一步,使用没有那么灵活的函数声明
function factorial(x) {
if (x <= 1) return 1;
return x * factorial(x - 1);
}
factorial(10);
评论