英文原文鏈接: Immediately-Invoked Function Expression (IIFE)
轉載: 立即執(zhí)行函數(shù)表達式(IIFE)
參考文章: 立即執(zhí)行函數(shù)表達式(IIFE)
可能你并沒有注意到,我對術語有點敏感,所以,當我聽到流行的但是還存在誤解的術語“自執(zhí)行匿名函數(shù)”[self-executing anonymous function](或“自我調(diào)用的匿名函數(shù)”[self-invoking anonymous function])之后,我終于決定將我的想法整理成一篇文章。
除了提供關于這種模式如何實際工作的一些非常全面的信息之外,更進一步的,我實際上已經(jīng)就我們應該稱之為什么提出了建議,另外,如果你想跳過這里,可以查看一些實際的立即調(diào)用函數(shù)表達式(Immediately-Invoked Function Expressions),但我建議閱讀整篇文章。
請理解,本文不打算成為“我是對的,你錯了”的一類東西,我真的有興趣幫助人們理解潛在復雜的概念,并認為使用一致和準確的術語是人們可以做的最容易理解的事情之一。
那么,這究竟是什么呢?
在 JavaScript 中,每個函數(shù)在被調(diào)用時都會創(chuàng)建一個新的執(zhí)行上下文,因為函數(shù)中定義的變量和函數(shù)只能在該上下文中訪問,而不能在該上下文之外訪問,當調(diào)用函數(shù)時,函數(shù)提供的上下文提供了一個非常簡單的方法創(chuàng)建私有變量。
// makeCounter 函數(shù)返回的是一個新的函數(shù),該函數(shù)對makeCounter里的局部變量i享有使用權
function makeCounter() {
// i 只是 makeCounter 函數(shù)內(nèi)的局部變量
var i = 0;
return function () {
console.log(++i);
}
}
// 注意:counter 和 counter2 是不同的實例,他們分別擁有自己范圍里的 i 變量。
var counter = makeCounter();
counter(); // expected output: 1
counter(); // expected output: 2
var counter2 = makeCounter();
counter2(); // expected output: 1
counter2(); // expected output: 2
console.log(i); // ReferenceError: i is not defined, 它只是 makeCounter 函數(shù)內(nèi)的局部變量
在許多情況下,你可能并不需要makeWhatever
(譯者注:原文是makeWhatever,但是上文未提到)這樣的函數(shù)返回多次累加值,并且可以只調(diào)用一次得到一個單一的值,在其他一些情況里,你甚至不需要明確的知道返回值。
核心
現(xiàn)在,無論您是定義函數(shù)function foo(){}
還是var foo = function(){}
,您最終得到的是一個函數(shù)的標識符,您可以在它后面加上 parens(括號,()),比如:foo();
//像下面這樣定義的函數(shù)可以通過在函數(shù)名后加一對括號進行調(diào)用,像這樣`foo()`,
//因為foo相對于函數(shù)表達式`function(){/* code */}`只是一個引用變量。
var foo = function () {
/* code */
};
// 它是否能說明函數(shù)表達式本身可以被調(diào)用呢?,僅僅通過在它之后放置()來調(diào)用它嗎?
function () { /* code */}(); // Uncaught SyntaxError: Unexpected token (
正如你所看到的,這里捕獲了一個錯誤。當圓括號為了調(diào)用函數(shù)出現(xiàn)在函數(shù)后面時,無論在全局環(huán)境或者局部環(huán)境里遇到了這樣的function
關鍵字,默認的,它會將它當作是一個函數(shù)聲明,而不是函數(shù)表達式, 如果您沒有明確告訴解析器期望表達式,它會看到它認為是沒有名稱的函數(shù)聲明,并拋出SyntaxError異常,因為函數(shù)聲明需要一個名稱。
另外:functions,parens,SyntaxErrors
有趣的是,如果你為一個函數(shù)指定一個名字并在它后面放一對圓括號,同樣的也會拋出錯誤,但這次是因為另外一個原因。當圓括號放在一個函數(shù)表達式后面指明了這是一個被調(diào)用的函數(shù),而圓括號放在一個聲明后面便意味著完全的和前面的函數(shù)聲明分開了,此時圓括號只是一個簡單的代表一個括號(用來控制運算優(yōu)先的括號)。
// 雖然這個函數(shù)聲明現(xiàn)在在語法上是有效的,但它仍然是一個聲明,而下面一組 parens 是無效的,
// 因為分組操作符需要包含一個表達式
function foo() { /* code */}(); // Uncaught SyntaxError: Unexpected token )
// 現(xiàn)在,如果你在 parens 中放置一個表達式,不會拋出異常...但是函數(shù)也不會執(zhí)行,因為:
function foo() {/* code */}(1);
// 實際上與此相當,一個函數(shù)聲明后跟一個完全不相關的表達式:
function foo() { /* code */ }
(1);
您可以在Dmitry A. Soshnikov的文章ECMA-262-3(第5章 函數(shù))中詳細了解這方面的內(nèi)容。
立即調(diào)用函數(shù)表達式(IIFE [Immediately-Invoked Function Expression])
幸運的是,SyntaxError “修復“很簡單,最流行的也最被接受的方法是將函數(shù)聲明包裹在圓括號里來告訴語法分析器去表達一個函數(shù)表達式,因為在 JavaScript 中,圓括號不能包含聲明語句。此時,當解析器遇到 function 關鍵字時,它知道將其解析為函數(shù)表達式而不是函數(shù)聲明。
/**
*
* 任何消除函數(shù)表達式和函數(shù)聲明之間歧義的方法,都可以創(chuàng)建出立即調(diào)用函數(shù)聲明。
*
*/
// 可以使用以下兩種模式中的任何一種立即調(diào)用函數(shù)表達式,利用函數(shù)的執(zhí)行上下文來創(chuàng)建私有變量。
(function () { /* code */}()); // Crockford 建議這一個
(function () { /* code */ })(); // 但是這個也同樣有效
(() => { /* code */})(); // 使用 ES6 箭頭函數(shù),雖然括號只允許在外面
// 因為 parens 或強制操作符的目的是消除函數(shù)表達式和函數(shù)聲明之間的歧義,
// 當解析器已經(jīng)獲得期望的表達式時,可以省略它們。但是請參考下面的"重要提示"
var i = function () { return 10; }();
true && function () { /* code */ }();
0, function () { /* code */ }();
// 如果你不關心返回值,或者讓你的代碼稍微難以閱讀,你可以通過在你的函數(shù)前面帶上一個一元操作符來存儲字節(jié)
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
void function () { /* code */ }();
// 這是另一種變體,來自@kuvos--我不確定使用`new`關鍵字的性能影響(如果有的話),但它有效。
new function () { /* code */ };
new function () { /* code */ }(); // 如果傳遞參數(shù),只需要 parens
將變量傳入到立即調(diào)用的函數(shù)表達式作用域中的步驟如下:
(function (a, b) { /* code */ })("hello", "world");
一個以括號開頭的 IIFE(Immediately-Invoked Function Expression 立即調(diào)用函數(shù)表達式)使得自動分號插入(automatic semicolon insertion ASI)會導致一些問題。該 IIFE 會被解析為對上一行的最后一個術語的調(diào)用,在某些可省略分號的情況中,分號放在括號前面,稱為防御分號。例如:
a = b + c
;(function () {
// code
})();
// 避免被解析為 c();
關于那些 parens 的重要說明
在一些情況下,當額外的帶著歧義的括號圍繞在函數(shù)表達式周圍是沒有必要的(因為這時候的括號已經(jīng)將其作為一個表達式去表達),但當括號用于調(diào)用函數(shù)表達式時,這仍然是一個好主意。
這樣的括號指明函數(shù)表達式將會被立即調(diào)用,并且變量將會儲存函數(shù)的結果,而不是函數(shù)本身。當這是一個非常長的函數(shù)表達式時,這可以節(jié)約別人閱讀你代碼的時間,不用滾動到頁面底部去看這個函數(shù)是否被調(diào)用。
根據(jù)經(jīng)驗,雖然編寫明確的代碼可能在技術上是必要的,以防止 JavaScript 解析器拋出 SyntaxError 異常,編寫明確的代碼也是非常必要的,以防止其他開發(fā)人員拋出 “WTFError” 異常!
用閉包保存狀態(tài)
就像當函數(shù)被它們的命名標識符調(diào)用時可以傳遞參數(shù)一樣,立即執(zhí)行函數(shù)表達式也能傳參數(shù)。由于在另一個函數(shù)中定義的任何函數(shù)都可以訪問外部函數(shù)的傳入?yún)?shù)和變量(這種關系稱為閉包),利用這一點,我們能使用立即執(zhí)行函數(shù)鎖住變量保存狀態(tài)。
如果您想了解更多關于閉包的知識,請閱讀用JavaScript解釋的閉包。
// 這不會像你想象的那樣工作,因為 `i` 的值永遠不會被鎖定,
// 相反每次點擊時(在循環(huán)完成后),都會提示元素的總數(shù),
// 因為這就是此時 `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`,
// 在循環(huán)完成執(zhí)行后,即使 `i` 的值是元素的總數(shù),在調(diào)用函數(shù)表達式時,
// 在 IIFE 內(nèi)部,`lockedInIndex` 的值是傳入函數(shù)表達式(`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 a link #' + lockedInIndex);
}, 'false');
})(i);
}
// 您也可以像這樣使用 IIFE,僅包含(并返回)click 處理函數(shù),而不是整個 `addEventListener` 賦值。
// 無論哪種方式,雖然兩個示例使用 IIFE 鎖定值,但我發(fā)現(xiàn)前面的示例更具可讀性。
var elems = document.getElementsByTagName("a");
for (var i = 0; i < elems.length; i++) {
elems[i].addEventListener("click", (function (lockInIndex) {
return function (e) {
e.preventDefault();
alert('I am a link #' + lockInIndex);
}
})(i), 'false');
}
請注意,在最后兩個實例中,lockedInIndex
可以沒有任何問題的訪問i
,但是作為函數(shù)的參數(shù)使用一個不同的命名標識符可以使概念更加容易的被解釋。
立即調(diào)用函數(shù)表達式的一個最有利的副作用是,因為這個未命名的或匿名的函數(shù)表達式被立即調(diào)用,而不使用標識符,所以可以使用閉包而不污染當前范圍。
自執(zhí)行匿名函數(shù)(“Self-executing anonymous function”)有什么問題呢?
您已經(jīng)見過它被多次提到,但如果不清楚,我建議使用術語“立即調(diào)用函數(shù)表達式”和“IIFE”(如果您喜歡首字母縮略詞)。“iffy”的發(fā)音是我提出來的,我很喜歡這個發(fā)音,我們繼續(xù)。
什么是立即調(diào)用函數(shù)表達式? 這是一個立即被調(diào)用的函數(shù)表達式。 就像這個名字會讓你相信。
我希望看到JavaScript社區(qū)成員在他們的文章和演示中采用“立即調(diào)用函數(shù)表達式”和“IIFE”這兩個術語,因為我覺得這樣理解這個概念更容易一些,而且因為“自執(zhí)行匿名函數(shù)”這個術語并不準確:
// 這是一個自我執(zhí)行的函數(shù)。 這是一個遞歸執(zhí)行(或調(diào)用)自身的函數(shù):
function foo() { foo(); }
// 這是一個自我執(zhí)行的匿名函數(shù),因為它沒有標識符,所以它必須使用 `arguments.callee` 屬性,
// (它指定當前正在執(zhí)行的函數(shù)) 來執(zhí)行他自己。
var foo = function () { arguments.callee(); };
// 這也許算是一個自執(zhí)行匿名函數(shù),但是僅僅當`foo`標識符作為它的引用時,
// 如果你將它換成用`foo`來調(diào)用同樣可行
var foo = function () { foo(); }
// 有些人稱之為“自我執(zhí)行的匿名函數(shù)”,即使它不是自動執(zhí)行的,
// 因為它不會自行調(diào)用。 然而,它立即被調(diào)用。
(function () { /* code */}());
// 為函數(shù)表達式增加標識符(也就是說創(chuàng)造一個命名函數(shù))對我們的調(diào)試會有很大幫助。
// 一旦命名,函數(shù)將不再匿名。
(function foo() { /* code */}());
// IIFE也可以自我執(zhí)行,盡管這也許不是最有用的模式。
(function () { arguments.callee(); }());
(function foo() { foo(); }());
//最后要注意的一件事:這會導致BlackBerry 5出錯,因為
//在命名函數(shù)表達式中,該名稱未定義。太棒了,對吧?
(function foo(){ foo(); }());
希望這些例子清楚的表明,“自我執(zhí)行” 這個術語有些誤導,因為它并不是執(zhí)行自己的函數(shù),盡管函數(shù)已經(jīng)被執(zhí)行,另外,“匿名” 是不必要的,因為立即調(diào)用的函數(shù)表達式可以是匿名的或者是命名的。至于我更喜歡“invoked”而非“executed”,這是一個簡單的頭韻問題; 我認為“IIFE”看起來和聽起來比“IEFE”好。
就是這樣了。 這是我的想法。
有趣的事實:因為arguments.callee
在 ECMAScript5 嚴格模式中被棄用,實際上在 ECMAScript5 嚴格模式下創(chuàng)建“自我執(zhí)行的匿名函數(shù)”在技術上是不可能的。
最后一點:模塊模式
在我調(diào)用函數(shù)表達式的時候,如果我沒有提到模塊模式,那我就會失職。如果您對JavaScript中的模塊模式不熟悉,它與我的第一個示例類似,但返回的是Object
而不是Function
(并且通常以單例實現(xiàn),如本例中所示)。
//創(chuàng)建一個立即調(diào)用的匿名函數(shù)表達式,并將其*返回值*賦給變量。這種方法“切斷了
//名為`makeWhatever`函數(shù)引用的“middleman”。
// 正如上面“重要說明”中所解釋的,盡管在這個函數(shù)表達式周圍不需要使用 parens,
// 但仍然應該將它們作為約定使用,以幫助闡明變量被設置為函數(shù)的*result*,而不是函數(shù)本身。
var counter = (function () {
var i = 0;
return {
get: function () {
return i;
},
set: function (val) {
i = val;
},
increment: function () {
return i++;
}
};
}());
// `counter` 是一個具有屬性的對象,在本例中正好屬性全是方法。
counter.get(); // expected output: 0
counter.set(3);
counter.increment(); // expected output: 4
counter.increment(); // expected output: 5
counter.i; // undefined(`i`不是返回對象的屬性)
i; // Uncaught ReferenceError: i is not defined(它只存在于閉包內(nèi))
模塊模式方法不僅非常強大,而且非常簡單。使用很少的代碼,您可以有效地命名相關方法和屬性的名稱空間,以一種最小化全局范圍污染和創(chuàng)建隱私的方式組織整個代碼模塊。