详解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的调用栈上看起来非常清晰明了// 因为很明白地显示了名称bazbarfooexpr_test.html()
function foo(){return bar();}var bar = function(){return baz();}function baz(){debugger;}foo();// Call stack bazbar() //看到了么?fooexpr_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(?)() // 这里可是问号哦fooexpr_test.html()
function foo(){return baz();}var bar = function(){debugger;};var baz = bar;bar = function() {alert('spoofed');};foo();// Call stack: bar()fooexpr_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();// 又再次看到了清晰的调用栈信息了耶! bazbarfooexpr_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; // falsef.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.3Kwith `null`: 7.6K -> 18KIE7:without `null`: 14K -> 29.7Kwith `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())而不是1constructor === Object.prototype.constructor; // truetoString === Object.prototype.toString; // true// ……})();})();
var fn = (function(){// 声明要引用函数的变量var f;// 有条件地创建命名函数// 并将其引用赋值给fif (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关键字// 除非函数顶部已经声明了addEventvar 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);

评论
