1. 程式人生 > >揭祕命名函式表示式

揭祕命名函式表示式

前言

網上還沒用發現有人對命名函式表示式進去重複深入的討論,正因為如此,網上出現了各種各樣的誤解,本文將從原理和實踐兩個方面來探討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: <function object> }。
當通過作用域鏈解析x時,首先解析的是foo的區域性環境。如果沒有找到x,則繼續搜尋作用域鏈中的下一個物件。下一個物件
就是儲存函式識別符號的那個物件——{ foo: <function object> },由於該物件繼承自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,那麼addEventListeneraddEventAsProperty則根本就用不著了。可是,他們都佔著記憶體哪;而且,這些記憶體將永遠都得不到釋放,原因跟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 <= 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);
複製程式碼