譯者:nzbin
也許你還沒有注意到,我是一個對術語比較堅持的人。因此,在聽到很多次比較流行卻容易產生誤導的 JavaScript 術語“自執行匿名函數”之后,最終我決定把我的想法寫成一篇文章。
為了提供關於這一模式如何運作的透徹信息,我已經提出了我們應該如何稱呼它的建議,繼續向下看。當然,如果你想跳過開頭,你可以只看“自執行函數表達式”這一節,但是我建議你看完整篇文章。
請明白這篇文章並非要表達“我是對的,你是錯的”這一觀點。我真正感興趣的是幫助人們理解一些潛在的復雜概念,並且讓人們意識到使用一致的和准確的術語是人們能夠做到以方便理解的最簡單的事情之一。
那么,這到底是怎么回事呢?
在 JavaScript 中,每一個函數在執行時都會產生一個新的執行環境。由於在函數中定義的變量和函數只能在內部訪問而不能被外部訪問。這一執行環境調用的函數提供了一個非常簡單的方法來創建私有作用域。
// 因為返回的函數有權訪問私有變量 `i` function makeCounter() { // `i` 只能在 `makeCounter`內被訪問. var i = 0; return function() { console.log( ++i ); }; } // 注意 `counter` 和 `counter2` 都有私有的作用域 `i`. var counter = makeCounter(); counter(); // logs: 1 counter(); // logs: 2 var counter2 = makeCounter(); counter2(); // logs: 1 counter2(); // logs: 2 i; // ReferenceError: i 未定義 (只存在 makeCounter 內部)
很多情況下,你並不需要 makeWhatever 函數返回多個實例,可以用一個實例來做。在其他情況下,你甚至沒有明確的返回值。
這件事的核心
現在,無論你用 function foo(){} 還是 var foo = function(){} 的方式定義函數,最終都會以一個函數的標識符結尾,你可以通過圓括號 () 調用函數,像 foo() 。
// 像這樣定義的函數可以在函數名后放置 () 來執行 // 比如 foo(), 因為 foo 只是函數表達式 `function() { /* code */ }`的引用 var foo = function(){ /* code */ } // ...是不是只在函數表達式之后放置 () 就能執行? function(){ /* code */ }(); // SyntaxError: Unexpected token (
如你所見,有一個報錯。當解析器在全局范圍內或在函數中遇到 function 關鍵字時,默認情況下,它會認為這是函數聲明而不是函數表達式。如果你沒有明確告訴解析器這是一個表達式,它會認為這是一個匿名的函數聲明並拋出意外的語法錯誤,因為函數聲明需要名稱。
題外話:函數,括號,語法錯誤
有趣的是,如果你為一個函數指定了名稱並且在立刻在其后邊放置了括號,解析器也會拋出錯誤,但原因不同。雖然在表達式之后放置括號說明這是一個將被執行的函數,但在聲明之后放置括號會與前面的語句分離,成為一個分組操作符(可以作為優先提升的方法)。
// 現在這個函數聲明的語法是正確的,但還是有報錯 // 表達式后面的括號是非法的, 因為分組運算符必須包含表達式 function foo(){ /* code */ }(); // SyntaxError: Unexpected token ) // 如果你在括號內放置了表達式, 沒有錯誤拋出... // 但是函數也不會執行, 因為: function foo(){ /* code */ }( 1 ); // 它與一個函數聲明后面放一個完全無關的表達式是一樣的: function foo(){ /* code */ } ( 1 );
你可以閱讀 Dmitry A. Soshnikov 的文章來了解更多關於這方面的知識,ECMA-262-3 in detail. Chapter 5. Functions。
立即執行函數表達式(IIFE)
幸運的是,固定的語法錯誤很簡單。最普遍接受的方式告訴解析器這是一個被括號包裹的函數表達式。因為在 JavaScript 中,括號內不能包含函數聲明,在這一點上,當解析器遇到 function 關鍵字,它會以函數表達式而不是函數聲明去解析它。
// 以下的任何一種方式都可以立即執行函數表達式,利用函數的執行環境 // 創建私有作用域 (function(){ /* code */ }()); // Crockford 推薦這個 (function(){ /* code */ })(); // 這個同樣運行正常 // 因為括號和強制運算符的目的就是區分函數表達式和函數聲明 // 它們會在解析器解析表達式時被忽略(但是請看下面的“重要提示”) var i = function(){ return 10; }(); true && function(){ /* code */ }(); 0, function(){ /* code */ }(); // 如果你不關心函數返回值或者你的代碼變得難以閱讀 // 你可以在函數前面加一個一元運算符 !function(){ /* code */ }(); ~function(){ /* code */ }(); -function(){ /* code */ }(); +function(){ /* code */ }(); // 下面是另一種變體, from @kuvos // 我不確定使用 `new` 關鍵字是否有性能影響, 但是能夠正常運行 // http://twitter.com/kuvos/status/18209252090847232 new function(){ /* code */ } new function(){ /* code */ }() // 只需要使用括號傳遞參數
關於括號的注意事項
在函數表達式外面添加括號可以解除困惑,但這一情況並不是必須的,因為解析器已經預定義了一個函數表達式。作為約定,再做任務時使用括號仍然是一個好方法。
這一括號通常意味着函數表達式會被立即執行,變量將包含函數的結果而不是函數本身。這也會解決一些麻煩,否則如果你寫了一個很長的函數表達式,別人必須拉到最底部查看該函數有沒有被立即執行。
根據經驗來說,書寫明確的代碼不僅可以避免瀏覽器拋出語法錯誤,也可以避免其他開發者對你說“WTFError”(what the fuck error)!
閉包的存儲狀態
就像函數被函數名調用時參數會被傳遞一樣,立即執行函數表達式時參數同樣會被傳遞。因為在一個函數內部定義的函數可以訪問外部函數的變量(這種關系被稱為閉包)。一個立即執行函數表達式可以用於封鎖函數值並且有效的存儲狀態。
如果你想了解更多關於閉包的知識,請瀏覽Closures explained with JavaScript。
// 以下程序的運行結果和你想象的並不一樣, 因為 `i` 的值 // 不會被鎖定。相反,當點擊每個鏈接的時候 (循環已經 // 結束), 會顯示元素的總數, 因為那才是 // 點擊時 `i` 實際的值. var elems = document.getElementsByTagName( 'a' ); for ( var i = 0; i < elems.length; i++ ) { elems[ i ].addEventListener( 'click', function(e){ e.preventDefault(); alert( 'I am link #' + i ); }, 'false' ); } // 以下程序會按你想象的方式運行, 因為在 IIFE 中, `i` 的值 // 會作為 `lockedInIndex` 被鎖定。 循環結束之后, // 盡管 `i` 的值是元素總數, 但是在 IIFE 中 // `lockedInIndex` 的值是函數表達式調用時傳入的(`i`)的值 // 因此當點擊鏈接時, 顯示的值是正確的。 var elems = document.getElementsByTagName( 'a' ); for ( var i = 0; i < elems.length; i++ ) { (function( lockedInIndex ){ elems[ i ].addEventListener( 'click', function(e){ e.preventDefault(); alert( 'I am link #' + lockedInIndex ); }, 'false' ); })( i ); } // 你也許會這樣使用 IIFE , 只是包含 (返回) // 點擊處理函數, 並不是整個 `addEventListener` 聲明 // 無論哪種方式,兩個示例都使用 // IIFE, 我發現前面的例子更易讀懂 var elems = document.getElementsByTagName( 'a' ); for ( var i = 0; i < elems.length; i++ ) { elems[ i ].addEventListener( 'click', (function( lockedInIndex ){ return function(e){ e.preventDefault(); alert( 'I am link #' + lockedInIndex ); }; })( i ), 'false' ); }
注意最后兩個例子,雖然 lockedInIndex 可以獲得 i 的值,但是使用一個不同的名稱標識符作為函數參數可以使復雜的概念易於解釋。
立即執行函數表達式最好的一方面就是,因為這個匿名函數表達式被立即執行,沒有標識符,所以閉包的使用不會污染當前作用域。
“自執行匿名函數”有錯誤嗎?
你已經發現這一稱呼被提到了多次,但也許並不清晰,我已經提議“立即執行函數表達式”這一術語,如果你喜歡縮寫,也可以稱呼“IIFE”。“iffy”的發音提醒了我,我很喜歡,讓我們這樣稱呼它吧。
“立即執行函數表達式”是什么?它是一個被立即執行的函數表達式,就像這個名稱會讓你相信一樣。
我希望看到 JavaScript 社區成員在他們的文章和報告中采用“立即執行函數表達式”這個術語。因為我覺得這個術語使得理解這一概念變得簡單,而“自執行匿名函數”這一術語並不准確。
// 這是一個自執行函數。 這種函數會遞歸地 // 執行 (或調用) 自身: function foo() { foo(); } // 這是一個自執行匿名函數。因為它沒有 // 標識符, 必須使用 `arguments.callee` 屬性 (它 // 表示當前執行的函數) 來調用自身。 var foo = function() { arguments.callee(); }; // 這 *可能* 是一個自執行匿名函數, 但只有當 // `foo` 標識符實際引用它的時候。如果你把`foo` 換成 // 別的東西, 你可能會有一個 "用於自執行" 的匿名函數。 var foo = function() { foo(); }; // 有些人把這個稱為 "自執行匿名函數" ,其實它並 // 不是自執行, 因為它沒有調用自身。它只是 // 立即調用。 (function(){ /* code */ }()); // 給函數表達式添加一個標識符 (因此創建了一個命名 // 函數表達式) ,調試時會非常有用。一旦命名, // 函數不再是匿名的。 (function foo(){ /* code */ }()); // IIFE 也可以自執行, 盡管這並不是最 // 有用的方式。 (function(){ arguments.callee(); }()); (function foo(){ foo(); }()); // 最后需要注意的一點: 這在 BlackBerry 5 中會報錯, 因為 // 在一個命名函數表達式中, 函數名是 undefined。很奇怪,對吧? (function foo(){ foo(); }());
希望這些示例能夠說明“自執行”的術語容易被誤解,因為並不是函數執行自身,雖然函數被執行了。同樣“匿名”也不具體,因為“立即執行函數表達式”既可以匿名也可以命名。因為相比“executed”,我更喜歡“invoked”,一個簡單的原因是因為 頭韻。我認為“IIFE”聽上去比“IEFE”更好。
以上就是我的看法。
有趣的是:因為 arguments.callee 在ECMAScript 5 strict mode 嚴格模式下已經過時,所以無法在 ES5 的嚴格模式下創建“自執行匿名函數”。
最后的題外話:模塊化
既然提到了函數表達式,如果我不說一下模塊化就是我的疏忽。你不熟悉JavaScript的模塊化也沒關系,我的第一個示例非常簡單,只是最終返回的是一個對象而不是函數(通常作為單例模式運行,如以下示例)
// 創建一個立即執行的匿名函數表達式, 然后 // 將它的 *返回值* 賦給一個變量。這種方法無須再 // 創建一個 `makeWhatever` 函數的引用。 // // 如同上面 "關於括號的注意事項" 中提到的一樣, 盡管括號在函數 // 表達式中不是必須添加的, 但是按照習慣還是應該添加括號, // 因為這可以更清晰的表示出賦值給一個變量的是 // 函數的 *結果* 而不是函數自身 var counter = (function(){ var i = 0; return { get: function(){ return i; }, set: function( val ){ i = val; }, increment: function() { return ++i; } }; }()); // `counter` 是一個有屬性的對象, 它的屬性都是方法 counter.get(); // 0 counter.set( 3 ); counter.increment(); // 4 counter.increment(); // 5 counter.i; // undefined (`i` 不是返回對象的屬性) i; // ReferenceError: i 未定義 (它只存在於閉包內)
模塊化方法不僅強大而且簡單。你可以用更少的代碼有效地命名方法和屬性,用一種方式組織所有的代碼模塊,並且可以避免全局變量的污染以及創建私有作用域。
擴展閱讀
- ECMA-262-3 in detail. Chapter 5. Functions. - Dmitry A. Soshnikov
- Functions and function scope - Mozilla Developer Network
- Named function expressions - Juriy “kangax” Zaytsev
- JavaScript Module Pattern: In-Depth - Ben Cherry
- Closures explained with JavaScript - Nick Morgan
