深刻理解JavaScript系列(2):揭祕命名函數表達式(轉)

前言

網上還沒用發現有人對命名函數表達式進去重複深刻的討論,正由於如此,網上出現了各類各樣的誤解,本文將從原理和實踐兩個方面來探討JavaScript關於命名函數表達式的優缺點。javascript

簡 單的說,命名函數表達式只有一個用戶,那就是在Debug或者Profiler分析的時候來描述函數的名稱,也可使用函數名實現遞歸,但很快你 就會發現實際上是不切實際的。固然,若是你不關注調試,那就沒什麼可擔憂的了,不然,若是你想了解兼容性方面的東西的話,你仍是應該繼續往下看看。html

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

函數表達式和函數聲明

在ECMAScript中,建立函數的最經常使用的兩個方法是函數表達式和函數聲明,二者期間的區別是有點暈,由於ECMA規範只明確了一點:函數聲明必須帶有標示符(Identifier)(就是你們常說的函數名稱),而函數表達式則能夠省略這個標示符:java

  函數聲明:git

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

  函數表達式:web

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

所 以,能夠看出,若是不聲明函數名稱,它確定是表達式,可若是聲明瞭函數名稱的話,如何判斷是函數聲明仍是函數表達式呢?ECMAScript是通 過上下文來區分的,若是function foo(){}是做爲賦值表達式的一部分的話,那它就是一個函數表達式,若是function foo(){}被包含在一個函數體內,或者位於程序的最頂部的話,那它就是一個函數聲明。json

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

致謝

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

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

託比·蘭吉的點子被我用在了「替代方案」中。

蓋瑞特·史密斯(Garrett Smith)德米特里·蘇斯尼科(Dmitry Soshnikov)對本文的多方面做出了補充和修正。


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

參考譯文:鏈接訪問 (SpiderMonkey的怪癖以後的章節參考該文)

相關文章
相關標籤/搜索