天天看點

深入了解JavaScript之揭秘命名函數表達式

深入了解JavaScript之揭秘命名函數表達式

英文原文 | http://kangax.github.com/nfe/

前言

本文将從原理和實踐兩個方面來探讨JavaScript關于命名函數表達式的優缺點。

簡單的說,命名函數表達式隻有一個使用者,那就是在Debug或者Profiler分析的時候來描述函數的名稱,也可以使用函數名實作遞歸,但很快你就會發現其實是不切實際的。

當然,如果你不關注調試,那就沒什麼可擔心的了,否則,如果你想了解相容性方面的東西的話,你還是應該繼續往下看看。

我們先開始看看,什麼叫函數表達式,然後再說一下現代調試器如何處理這些表達式,如果你已經對這方面很熟悉的話,請直接跳過此小節。

函數表達式和函數聲明

在ECMAScript中,建立函數的最常用的兩個方法是函數表達式和函數聲明,兩者期間的差別是有點暈,因為ECMA規範隻明确了一點:函數聲明必須帶有标示符(Identifier)(就是大家常說的函數名稱),而函數表達式則可以省略這個标示符:

函數聲明:

function 函數名稱 (參數:可選){ 函數體 }

函數表達式:

function 函數名稱(可選)(參數:可選){ 函數體 }

是以,可以看出,如果不聲明函數名稱,它肯定是表達式,可如果聲明了函數名稱的話,如何判斷是函數聲明還是函數表達式呢?ECMAScript是通過上下文來區分的,如果function foo(){}是作為指派表達式的一部分的話,那它就是一個函數表達式,如果function foo(){}被包含在一個函數體内,或者位于程式的最頂部的話,那它就是一個函數聲明。

function foo(){} // 聲明,因為它是程式的一部分
var bar = function foo(){}; // 表達式,因為它是指派表達式的一部分


new function bar(){}; // 表達式,因為它是new表達式


(function(){
function bar(){} // 聲明,因為它是函數體的一部分
})();      

還有一種函數表達式不太常見,就是被括号包覆的(function foo(){}),他是表達式的原因是因為括号 ()是一個分組操作符,它的内部隻能包含表達式,我們來看幾個例子:

function foo(){} // 函數聲明
(function foo(){}); // 函數表達式:包含在分組操作符内


try {
(var x = 5); // 分組操作符,隻能包含表達式而不能包含語句:這裡的var就是語句
} catch(err) {
// SyntaxError
}      

你可以會想到,在使用eval對JSON進行執行的時候,JSON字元串通常被包含在一個圓括号裡:eval('(' + json + ')'),這樣做的原因就是因為分組操作符,也就是這對括号,會讓解析器強制将JSON的花括号解析成表達式而不是代碼塊。

try {
{ "x": 5 }; // "{" 和 "}" 做解析成代碼塊
} catch(err) {
// SyntaxError
}


({ "x": 5 }); // 分組操作符強制将"{" 和 "}"作為對象字面量來解析      

表達式和聲明存在着十分微妙的差别,首先,函數聲明會在任何表達式被解析和求值之前先被解析和求值,即使你的聲明在代碼的最後一行,它也會在同作用域内第一個表達式之前被解析/求值,參考如下例子,函數fn是在alert之後聲明的,但是在alert執行的時候,fn已經有定義了:

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

函數聲明的實際規則如下:

_函數聲明_隻能出現在_程式_或_函數體_内。從句法上講,它們 不能出現在Block(塊)({ ... })中,例如不能出現在 if、while 或 for 語句中。

因為 Block(塊) 中隻能包含Statement語句, 而不能包含_函數聲明_這樣的源元素。另一方面,仔細看一看規則也會發現,唯一可能讓_表達式_出現在Block(塊)中情形,就是讓它作為_表達式語句_的一部分。

但是,規範明确規定了_表達式語句_不能以關鍵字function開頭。

而這實際上就是說,_函數表達式_同樣也不能出現在Statement語句或Block(塊)中(因為Block(塊)就是由Statement語句構成的)。

函數語句

在ECMAScript的文法擴充中,有一個是函數語句,目前隻有基于Gecko的浏覽器實作了該擴充,是以對于下面的例子,我們僅是抱着學習的目的來看,一般來說不推薦使用(除非你針對Gecko浏覽器進行開發)。

1.一般語句能用的地方,函數語句也能用,當然也包括Block塊中:

if (true) {
function f(){ }
}
else {
function f(){ }
}      

2、函數語句可以像其他語句一樣被解析,包含基于條件執行的情形

if (true) {
function foo(){ return 1; }
}
else {
function foo(){ return 2; }
}
foo(); // 1
// 注:其它用戶端會将foo解析成函數聲明 
// 是以,第二個foo會覆寫第一個,結果傳回2,而不是1      

3、函數語句不是在變量初始化期間聲明的,而是在運作時聲明的——與函數表達式一樣。不過,函數語句的辨別符一旦聲明能在函數的整個作用域生效了。辨別符有效性正是導緻函數語句與函數表達式不同的關鍵所在(下一小節我們将會展示命名函數表達式的具體行為)。

// 此刻,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; };
}      

4、函數語句和函數聲明(或命名函數表達式)的字元串表示類似,也包括辨別符:

if (true) {
function foo(){ return 1; }
}
String(foo); // function foo() { return 1; }      

5、另外一個,早期基于Gecko的實作(Firefox 3及以前版本)中存在一個bug,即函數語句覆寫函數聲明的方式不正确。在這些早期的實作中,函數語句不知何故不能覆寫函數聲明:

// 函數聲明
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      

再次強調一點,上面這些例子隻是在某些浏覽器支援,是以推薦大家不要使用這些,除非你就在特性的浏覽器上做開發。

命名函數表達式

函數表達式在實際應用中還是很常見的,在web開發中友個常用的模式是基于對某種特性的測試來僞裝函數定義,進而達到性能優化的目的,但由于這種方式都是在同一作用域内,是以基本上一定要用函數表達式:

// 該代碼來自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 bar = function foo(){};就是一個有效的命名函數表達式,但有一點需要記住:這個名字隻在新定義的函數作用域内有效,因為規範規定了标示符不能在外圍的作用域内有效:

var f = function foo(){
return typeof foo; // foo是在内部作用域内有效
};
// foo在外部用于是不可見的
typeof foo; // "undefined"
f(); // "function"      

既然,這麼要求,那命名函數表達式到底有啥用啊?為啥要取名?

正如我們開頭所說:給它一個名字就是可以讓調試過程更友善,因為在調試的時候,如果在調用棧中的每個項都有自己的名字來描述,那麼調試過程就太爽了,感受不一樣嘛。

調試器中的函數名

如果一個函數有名字,那調試器在調試的時候會将它的名字顯示在調用的棧上。有些調試器(Firebug)有時候還會為你們函數取名并顯示,讓他們和那些應用該函數的便利具有相同的角色,可是通常情況下,這些調試器隻安裝簡單的規則來取名,是以說沒有太大價格,我們來看一個例子:

function foo(){
return bar();
}
function bar(){
return baz();
}
function baz(){
debugger;
}
foo();


// 這裡我們使用了3個帶名字的函數聲明
// 是以當調試器走到debugger語句的時候,Firebug的調用棧上看起來非常清晰明了 
// 因為很明白地顯示了名稱
baz
bar
foo
expr_test.html()      

通過檢視調用棧的資訊,我們可以很明了地知道foo調用了bar, bar又調用了baz(而foo本身有在expr_test.html文檔的全局作用域内被調用),不過,還有一個比較爽地方,就是剛才說的Firebug為匿名表達式取名的功能:

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

這時候,調用棧顯示的是foo調用了bar,但實際上并非如此,之是以有這種問題,是因為baz和另外一個包含alert('spoofed')的函數做了引用交換所導緻的。

歸根結底,隻有給函數表達式取個名字,才是最委托的辦法,也就是使用命名函數表達式。

我們來使用帶名字的表達式來重寫上面的例子(注意立即調用的表達式塊裡傳回的2個函數的名字都是bar):

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

OK,又學了一招吧?不過在高興之前,我們再看看不同尋常的JScript吧。

JScript的Bug

比較惡的是,IE的ECMAScript實作JScript嚴重混淆了命名函數表達式,搞得現很多人都出來反對命名函數表達式,而且即便是最新的一版(IE8中使用的5.8版)仍然存在下列問題。

下面我們就來看看IE在實作中究竟犯了那些錯誤,俗話說知已知彼,才能百戰不殆。我們來看看如下幾個例子:

例1:函數表達式的标示符洩露到外部作用域

var f = function g(){};
typeof g; // "function"      

上面我們說過,命名函數表達式的标示符在外部作用域是無效的,但JScript明顯是違反了這一規範,上面例子中的标示符g被解析成函數對象,這就亂了套了,很多難以發現的bug都是因為這個原因導緻的。

注:IE9貌似已經修複了這個問題

例2:将命名函數表達式同時當作函數聲明和函數表達式

typeof g; // "function"
var f = function g(){};      

特性環境下,函數聲明會優先于任何表達式被解析,上面的例子展示的是JScript實際上是把命名函數表達式當成函數聲明了,因為它在實際聲明之前就解析了g。

這個例子引出了下一個例子。

例3:命名函數表達式會建立兩個截然不同的函數對象!

var f = function g(){};
f === g; // false


f.expando = 'foo';
g.expando; // undefined      

看到這裡,大家會覺得問題嚴重了,因為修改任何一個對象,另外一個沒有什麼改變,這太惡了。

通過這個例子可以發現,建立2個不同的對象,也就是說如果你想修改f的屬性中儲存某個資訊,然後想當然地通過引用相同對象的g的同名屬性來使用,那問題就大了,因為根本就不可能。

再來看一個稍微複雜的例子:

例4:僅僅順序解析函數聲明而忽略條件語句塊

var f = function g() {
  return 1;
};
if (false) {
  f = function g(){
    return 2;
  };
}
g(); // 2      

這個bug查找就難多了,但導緻bug的原因卻非常簡單。首先,g被當作函數聲明解析,由于JScript中的函數聲明不受條件代碼塊限制,是以在這個很惡的if分支中,g被當作另一個函數function g(){ return 2 },也就是又被聲明了一次。

然後,所有“正常的”表達式被求值,而此時f被賦予了另一個新建立的對象的引用。

由于在對表達式求值的時候,永遠不會進入“這個可惡if分支,是以f就會繼續引用第一個函數function g(){ return 1 }。

分析到這裡,問題就很清楚了:假如你不夠細心,在f中調用了g,那麼将會調用一個毫不相幹的g函數對象。

你可能會文,将不同的對象和arguments.callee相比較時,有什麼樣的差別呢?我們來看看:

var f = function g(){
return [
  arguments.callee == f,
  arguments.callee == g
];
};
f(); // [true, false]
g(); // [false, true]      

可以看到,arguments.callee的引用一直是被調用的函數,實際上這也是好事,稍後會解釋。

還有一個有趣的例子,那就是在不包含聲明的指派語句中使用命名函數表達式:

(function(){
f = function f(){};
})();      

按照代碼的分析,我們原本是想建立一個全局屬性f(注意不要和一般的匿名函數混淆了,裡面用的是帶名字的生命),JScript在這裡搗亂了一把,首先他把表達式當成函數聲明解析了,是以左邊的f被聲明為局部變量了(和一般的匿名函數裡的聲明一樣),然後在函數執行的時候,f已經是定義過的了,右邊的function f(){}則直接就指派給局部變量f了,是以f根本就不是全局屬性。

了解了JScript這麼變态以後,我們就要及時預防這些問題了,首先防範辨別符洩漏帶外部作用域,其次,應該永遠不引用被用作函數名稱的辨別符;還記得前面例子中那個讨人厭的辨別符g嗎?——如果我們能夠當g不存在,可以避免多少不必要的麻煩哪。

是以,關鍵就在于始終要通過f或者arguments.callee來引用函數。如果你使用了命名函數表達式,那麼應該隻在調試的時候利用那個名字。最後,還要記住一點,一定要把命名函數表達式聲明期間錯誤建立的函數清理幹淨。

對于,上面最後一點,我們還得再解釋一下。

JScript的記憶體管理

知道了這些不符合規範的代碼解析bug以後,我們如果用它的話,就會發現記憶體方面其實是有問題的,來看一個例子:

var f = (function(){
if (true) {
  return function g(){};
}
return function g(){};
})();      

我們知道,這個匿名函數調用傳回的函數(帶有辨別符g的函數),然後指派給了外部的f。

我們也知道,命名函數表達式會導緻産生多餘的函數對象,而該對象與傳回的函數對象不是一回事。

是以這個多餘的g函數就死在了傳回函數的閉包中了,是以記憶體問題就出現了。這是因為if語句内部的函數與g是在同一個作用域中被聲明的。

這種情況下 ,除非我們顯式斷開對g函數的引用,否則它一直占着記憶體不放。

var f = (function(){
var f, g;
if (true) {
  f = function g(){};
}
else {
  f = function g(){};
}
// 設定g為null以後它就不會再占記憶體了
g = null;
return f;
})();      

通過設定g為null,垃圾回收器就把g引用的那個隐式函數給回收掉了,為了驗證我們的代碼,我們來做一些測試,以確定我們的記憶體被回收了。

測試

測試很簡單,就是命名函數表達式建立10000個函數,然後把它們儲存在一個數組中。等一會兒以後再看這些函數到底占用了多少記憶體。然後,再斷開這些引用并重複這一過程。下面是測試代碼:

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

通過運作在Windows XP SP2中的任務管理器可以看到如下結果:

IE6:


    without `null`:   7.6K -> 20.3K
    with `null`:      7.6K -> 18K


  IE7:


    without `null`:   14K -> 29.7K
    with `null`:      14K -> 27K      

如我們所料,顯示斷開引用可以釋放記憶體,但是釋放的記憶體不是很多,10000個函數對象才釋放大約3M的記憶體,這對一些小型腳本不算什麼,但對于大型程式,或者長時間運作在低記憶體的裝置裡的時候,這是非常有必要的。

關于在Safari 2.x中JS的解析也有一些bug,但介于版本比較低,是以我們在這裡就不介紹了,大家如果想看的話,請仔細檢視英文資料。

SpiderMonkey的怪癖

大家都知道,命名函數表達式的辨別符隻在函數的局部作用域中有效。但包含這個辨別符的局部作用域又是什麼樣子的嗎?其實非常簡單。

在命名函數表達式被求值時,會建立一個特殊的對象,該對象的唯一目的就是儲存一個屬性,而這個屬性的名字對應着函數辨別符,屬性的值對應着那個函數。這個對象會被注入到目前作用域鍊的前端。然後,被“擴充”的作用域鍊又被用于初始化函數。

在這裡,有一點十分有意思,那就是ECMA-262定義這個(儲存函數辨別符的)“特殊”對象的方式。

标準說**“像調用new Object()表達式那樣”**建立這個對象。如果從字面上來了解這句話,那麼這個對象就應該是全局Object的一個執行個體。

然而,隻有一個實作是按照标準字面上的要求這麼做的,這個實作就是SpiderMonkey。是以,在SpiderMonkey中,擴充Object.prototype有可能會幹擾函數的局部作用域:

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


})();
})();      

不過,更高版本的SpiderMonkey改變了上述行為,原因可能是認為那是一個安全漏洞。也就是說,“特殊”對象不再繼承Object.prototype了。不過,如果你使用Firefox 3或者更低版本,還可以“重溫”這種行為。

另一個把内部對象實作為全局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


})();
})();      

不過神奇的還是,函數中的變量甚至會與已有的Object.prototype的成員發生沖突,來看看下面的代碼:

(function(){


var constructor = function(){ return 1; };


(function(){


  constructor(); // 求值結果是{}(即相當于調用了Object.prototype.constructor())而不是1


  constructor === Object.prototype.constructor; // true
  toString === Object.prototype.toString; // true


  // ……


})();
})();      

要避免這個問題,要避免使用Object.prototype裡的屬性名稱,如toString, valueOf, hasOwnProperty等等。

JScript解決方案

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

最後我們給出一個應用上述技術的應用執行個體,這是一個跨浏覽器的addEvent函數代碼:

// 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;
})();      

雖然這個方案很優雅,但也不是沒有缺點。第一,由于使用不同的辨別符,導緻喪失了命名的一緻性。且不說這樣好還是壞,最起碼它不夠清晰。

有人喜歡使用相同的名字,但也有人根本不在乎字眼上的差别。可畢竟,不同的名字會讓人聯想到所用的不同實作。

例如,在調試器中看到attachEvent,我們就知 道addEvent是基于attachEvent的實作。當 然,基于實作來命名的方式也不一定都行得通。假如我們要提供一個API,并按照這種方式把函數命名為inner。

那麼API使用者的很容易就會被相應實作的 細節搞得暈頭轉向。

要解決這個問題,當然就得想一套更合理的命名方案了。但關鍵是不要再額外制造麻煩。我現在能想起來的方案大概有如下幾個:

'addEvent', 'altAddEvent', 'fallbackAddEvent'
// 或者
'addEvent', 'addEvent2', 'addEvent3'
// 或者
'addEvent_addEventListener', 'addEvent_attachEvent', 'addEvent_asProperty'      

另外,這種模式還存在一個小問題,即增加記憶體占用。提前建立N個不同名字的函數,等于有N-1的函數是用不到的。

具體來講,如果document.documentElement 中包含attachEvent,那麼addEventListener 和addEventAsProperty則根本就用不着了。

可是,他們都占着記憶體哪;而且,這些記憶體将永遠都得不到釋放,原因跟JScript臭哄哄的命名表達式相同——這兩個函數都被“截留”在傳回的那個函數的閉包中了。

不過,增加記憶體占用這個問題确實沒什麼大不了的。如果某個庫——例如Prototype.js——采用了這種模式,無非也就是多建立一兩百個函數而已。隻要不是(在運作時)重複地建立這些函數,而是隻(在加載時)建立一次,那麼就沒有什麼好擔心的。

WebKit的displayName

WebKit團隊在這個問題采取了有點兒另類的政策。介于匿名和命名函數如此之差的表現力,WebKit引入了一個“特殊的”displayName屬性(本質上是一個字元串),如果開發人員為函數的這個屬性指派,則該屬性的值将在調試器或性能分析器中被顯示在函數“名稱”的位置上。Francisco Tolmasky詳細地解釋了這個政策的原理和實作。

未來考慮

将來的ECMAScript-262第5版(目前還是草案)會引入所謂的嚴格模式(strict mode)。

開啟嚴格模式的實作會禁用語言中的那些不穩定、不可靠和不安全的特性。據說出于安全方面的考慮,arguments.callee屬性将在嚴格模式下被“封殺”。是以,在處于嚴格模式時,通路arguments.callee會導緻TypeError(參見ECMA-262第5版的10.6節)。

而我之是以在此提到嚴格模式,是因為如果在基于第5版标準的實作中無法使用arguments.callee來執行遞歸操作,那麼使用命名函數表達式的可能性就會大大增加。從這個意義上來說,了解命名函數表達式的語義及其bug也就顯得更加重要了。

// 此前,你可能會使用arguments.callee
(function(x) {
if (x return 1;
return x * arguments.callee(x - 1);
})(10);


// 但在嚴格模式下,有可能就要使用命名函數表達式
(function factorial(x) {
if (x return 1;
return x * factorial(x - 1);
})(10);


// 要麼就退一步,使用沒有那麼靈活的函數聲明
function factorial(x) {
if (x return 1;
return x * factorial(x - 1);
}
factorial(10);      

緻謝

理查德· 康福德(Richard Cornford),是他率先解釋了JScript中命名函數表達式所存在的bug。理查德解釋了我在這篇文章中提及的大多數bug,是以我強烈建議大家去看看他的解釋。我還要感謝**Yann-Erwan Perio和道格拉斯·克勞克佛德(Douglas Crockford)**,他們早在2003年就在comp.lang.javascript論壇中提及并讨論NFE問題了。

約翰-戴維·道爾頓(John-David Dalton)**對“最終解決方案”提出了很好的建議。