讓我們開始
在 JavaScript 中,每一個方法被調用時都會創建一個新的 execution context(2)。因爲在一個方法中定義的變量和方法只能從內部訪問,從外部則不能,而這個調用着方法的 context 就讓我們擁有了一個非常簡單的途徑來實現私有化。
//這個方法將返回一個對‘私有’變量 i 有訪問權的方法,這個被返回的方法,形象的說是個‘有特權’的方法。
function makeCounter() {
//‘i’ 只的作用域只在 ‘makeCounter’ 方法內。
var i = 0;
return function() {
console.log( ++i );
};
}
//注意變量 ‘counter’ 和 ‘counter2’ 有各自的變量 ‘i’。
var counter = makeCounter();
counter(); // 輸出: 1
counter(); // 輸出: 2
var counter2 = makeCounter();
counter2(); // 輸出: 1
counter2(); // 輸出: 2
i; // ReferenceError: i is not defined (i 只在 makeCounter 方法中存在)
在大部分情況下,你不需要擁有這裏的 makeCounter 的多個實例,一個對你已經夠用了,在其它的一些情況下,你甚至都不會明確的返回一個值。
核心問題
無論你是通過 foo(){} 或 var foo = function(){} 來定義一個方法,都可以通過在其後放一對括號來調用它,就像這樣 foo() 。
// 像這樣定義的一個方法可以通過在其名字後面加一對()來調用它,如 foo()
// 在此 foo 只是對 function(){/* 一些代碼 */} 這個方法的 function expression 的引用
var foo = function(){ /* 一些代碼 */ }
// 那麼有沒有可能直接通過在一個方法表達式後加()來調用它自身呢?
function(){ /* 一些代碼 */ }(); // SyntaxError: Unexpected token (
就像你看到的,報錯了。當解析器遇到一個 function 關鍵字時,不管是在全局範圍或是在另一個方法裏,它都會默認的被當作一個 function declaration(3) 而非一個 function expression(4) 來對待。如果你不明確的告訴解析器它正在處理的是一個 expression,它就只會將其當作一個缺少標識符的 function declaration 來對待並因此拋出一個語法錯誤,因爲一個 function declaration 需要一個標識符。
方法,括號和語法錯誤
如果你給方法指定一個名稱,解析器依舊會拋出語法錯誤,不過這次是全然不同的原因。當()被放置在一個 function declaration 之後時,僅僅會被當作一個分組操作符而已。
// 從語法上來說,這個 function declaration 是有效的,但在其後用()調用是無效的
// 因爲這裏()僅僅被當作了一個分組操作符,而一個分組操作符中需要包含表達式。
function foo(){ /* 一些代碼 */ }(); // SyntaxError: Unexpected token )
// 現在,如果你在()中放一個 expression,錯誤沒有了……
// 不過這個方法依舊不會被執行,因爲:
function foo(){ /* 一些代碼 */ }( 1 );
// 它還會被視作一個 function declaration 後跟着一個與之全然無關的 expression,等價於這樣:
function foo(){ /* 一些代碼 */ }
( 1 );
如果你對此想了解更多,請參考 Dmitry A. Soshnikov 的 ECMA-262-3 in detail. Chapter 5. Functions.,裏面有更詳細的相關內容。
Immediately-Invoked Function Expression (IIFE)
幸運的是,‘修復’語法錯誤很容易。讓解析器‘明確’它正在操作的是一個 function expression 只需要用一個()將它們包裹起來,因爲在 JavaScript 裏,括號中是不能包含指令的。這樣,當解析器遇到 function 關鍵字時,就會知道將它作爲一個 function expression解析,而不是一個 function declaration。
// 以下兩種方式都可以實現立即調用一個方法 function expression,並利用方法的 execution context
// 實現‘私有化’
(function(){ /* 一些代碼 */ }()); // Crockford 推薦此種方法
(function(){ /* 一些代碼 */ })(); // 但是這種方法一樣可行
// 因爲用括號進行強制操作的目的是消除 function expressions 和方法 function declarations 的歧義,所以
// 如果當解析器已經在之前遇到了一個表達式時也可以省略括號(但請務必往下看)
var i = function(){ return 10; }();
true && function(){ /* 一些代碼 */ }();
0, function(){ /* 一些代碼 */ }();
// 如果你不需要返回值,或者想儘可能的讓你的代碼難讀些,你也可以
// 通過一個一元運算符來節省你寶貴的字節
!function(){ /* 一些代碼 */ }();
~function(){ /* 一些代碼 */ }();
-function(){ /* 一些代碼 */ }();
+function(){ /* 一些代碼 */ }();
// 這裏還有另一種花樣,來自 [url=http://twitter.com/kuvos/status/18209252090847232]@kuvos[/url] ,雖然我搞不懂它爲什麼行,但它的確行。
new function(){ /* 一些代碼 */ }
new function(){ /* 一些代碼 */ }() // 只有當需要傳遞參數時才需要括號
關於()的必要性
有時候這些的額外的用來‘消除歧義’的()是多餘的(比如解析器在它之前已經遇到了另一個表達式),但作爲一個良好慣例我們還是加上它爲好。
加上()可以讓人知道這個方法會被立即調用,變量指向的是方法的返回結果而非方法本身。這可以方便其他人閱讀你的代碼,當你的方法很長時,如果不加上(),別人就需要一直滾屏到方法末尾來檢查方法是否已經被調用。
明晰的代碼不但有必要防止編譯器拋出語法錯誤,也同樣有必要防止別人向你拋出“WTFError”(5)
通過閉包保存狀態
當我們通過一個方法的標識符調用方法時我們可以對其傳遞參數,同樣的,在使用 IIFE 時我們也可以傳遞參數。因爲這裏傳入的參數在方法(也就是閉包)中是被‘鎖定’的,所以一個 Immediately-Invoked Function Expression 可以有效的被用來保存狀態。
如果你想了解閉包的更多知識,請閱讀閉包在 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 );
}, 'false' );
}
// 以下的代碼可以達到我們的目的,在這個 IIFE 閉包中,變量‘i’像一個
// ‘鎖定索引’被鎖在其中。當循環結束執行時,儘管變量‘i’的值是元素的總數
// 但在 IIFE 閉包中,‘鎖定索引’的值總是當時方法被調用時傳入的‘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( '我是鏈接 #' + lockedInIndex );
}, 'false' );
})( i );
}
// 你也可以像這樣使用一個 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( '我是鏈接 #' + lockedInIndex );
};
})( i ), 'false' );
}
注意以上兩個實例,雖然這裏的‘鎖定索引’可以寫作 i,沒有任何執行問題,但用一個類似方法參數的標識符來代替 i 顯然能使代碼更具有解釋性。
使用 Immediately-Invoked Function Expressions 帶來的另一個好處是不會污染當前作用域,因爲它是匿名的,沒有使用標識符。
“Self-executing anonymous function”有什麼問題?
你可能早就聽說過這個叫法了,但事實上它並不怎麼準確。我認爲它應該稱作“Immediately-Invoked Function Expression”,或者“IIFE”————如果你喜歡首字母縮寫。有人建議將它發音成“iffy”,我挺喜歡,就這麼念好了。
什麼是 Immediately-Invoked Function Expression?就是一個被立即調用的方法 expression,就像它的名字告訴我們的。
我希望看到 JavaScript 社區裏更多人使用“Immediately-Invoked Function Expression”和“IIFE”的稱呼,我覺得這個名稱可以更好的詮釋這種模式的概念,而“self-executing anonymous function”確實不夠準確。
// 這是一個 self-executing function,它執行它自身:
function foo() { foo(); }
// 這是一個 self-executing anonymous function,因爲它沒有
// 標識符,需要用“arguments.callee”來調用它自身
var foo = function() { arguments.callee(); };
// 這可能是一個 self-executing anonymous function,但只有當“foo”標識符
// 指向它時纔是。
var foo = function() { foo(); };
// 一些人把這稱作“self-executing anonymous function”哪怕它並沒有執行它自身
// 事實上,它是一個立即調用。
(function(){ /* 一些代碼 */ }());
// IIFE 可以執行它自身,但大多數情況下這種模式我們用不着。
(function(){ arguments.callee(); }());
(function foo(){ foo(); }());
// 最後一個我們要注意的事情:這樣的寫法會在 BlackBerry 5(6) 中導致一個錯誤,因爲
// 在一個命名方法中再次出現這個方法的標識符時會被認定爲未定義值。
(function foo(){ foo(); }());
希望這些例子能幫助大家理解爲什麼“self-executing”是一種有誤導性的說法,因爲它並不是方法調用方法自身。同樣,匿名也是一個不必要的說詞,因爲一個立即調用的方法表達式既可以是匿名的也可以是命名的。之所以我用了“調用”而不是“執行”,是因爲我認爲“IIFE”比“IEFE”看起來更順眼些,當然讀起來也是。
好了,這就是我偉大的想法。
對了,因爲在 ECMAScript 5 嚴格模式下 arguments.callee 已經被棄用,所以技術上來說,在 ECMAScript 5 嚴謹模式下創造一個“self-executing anonymous function”是不可能的。
關於模塊模式
既然都說到了這,那麼附帶提提模塊模式也就成了順利成章的事。如果你對 JavaScript 的模塊模式不熟悉,請回看我的第一個代碼示例,只不過用返回一個 Object 來代替返回一個 function(通常像這個例子一樣它都是以單例的方式實現的)
// 創建一個立即調用的匿名方法,將它的返回值賦予一個變量。
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 is not defined (它只在閉包中存在)
模塊模式不但強大而且簡單。極短的代碼就可以讓你的方法和屬性具有命名空間,最大限度的避免污染全局域並創建私有對象。
延伸閱讀
這些文章可以幫助你瞭解更多更全面的關於方法和模塊模式的相關知識。
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
名詞註解
(1) 直譯的話就是“自身調用自身的方法”或括號中的“自身執行自身的方法”,這個說法與其實質內容有所偏差,故筆者提出了更貼切的 IIFE 的說法。
(2) 通常翻譯成執行環境或執行上下文,每一段有不同作用域的 JavaScript 代碼都會在一個不同的執行環境中執行。具體中文詳解可參見此博客
(3) 可以翻譯爲方法指令或方法申明,它和 function expression(可譯爲方法表達式)是 JavaScript 中一對比較容易讓人困惑的概念。二者的區別非一言所能詳盡,英文好的同學可閱讀這片文章
(4) 見註解(3)
(5) 遇到讓自己看起來惱火的代碼時通常我們會忍不住蹦出“What The Fu...”。
(6) 黑莓5.0系統