詳解javascript立即執行函數表達式(IIFE)

詳解javascript立即執行函數表達式(IIFE)

寫在前面
原文是一篇很經典的講解IIFE的文章,很適合收藏。本文雖然是譯文,但是直譯的很少,而且添加了不少自己的理解。
ps:下文中提到的“立即執行函數”其實就是“立即執行函數表達式”
我們要說的到底是什麼?
在javascript中,每一個函數在被調用的時候都會創建一個執行上下文,在該函數內部定義的變量和函數只能在該函數內部被使用,而正是因爲這個上下文,使得我們在調用函數的時候能創建一些私有變量。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// makeCounter函數返回的是一個新的函數,該函數對makeCounter裏的局部變量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; // 報錯,i沒有定義,它只是makeCounter內部的局部變量
很多情況下我們並不需要像以上代碼一樣初始化很多實例,甚至有時候並不需要返回值。
  • 問題的核心
現在我們定義了一個函數(function foo(){}或者var foo = function(){}),函數名後加上一對小括號即可完成對該函數的調用,比如下面的代碼:
JavaScript
1
2
var foo = function(){ /* code */ };
foo();
接着我們來看下面的代碼:
JavaScript
1 function(){ /* code */ }(); // SyntaxError: Unexpected token (
報錯了,這是爲何?這是因爲在javascript代碼解釋時,當遇到function關鍵字時,會默認把它當做是一個函數聲明,而不是函數表達式,如果沒有把它顯視地表達成函數表達式,就報錯了,因爲函數聲明需要一個函數名,而上面的代碼中函數沒有函數名。(以上代碼,也正是在執行到第一個左括號(時報錯,因爲(前理論上是應該有個函數名的。)
  • 一波未平一波又起
有意思的是,如果我們給它函數名,然後加上()立即調用,同樣也會報錯,而這次報錯原因卻不相同:
JavaScript
1 function foo(){ /* code */ }(); // SyntaxError: Unexpected token )
爲什麼會這樣?在一個表達式後面加上括號,表示該表達式立即執行;而如果是在一個語句後面加上括號,該括號完全和之前的語句不搭嘎,而只是一個分組操作符,用來控制運算中的優先級(小括號裏的先運算)。所以以上代碼等價於:
JavaScript
1
2
function foo(){ /* code */ }
(); // SyntaxError: Unexpected token )
相當於先聲明瞭一個叫foo的函數,之後進行()內的表達式運算,但是()(分組操作符)內的表達式不能爲空,所以報錯。(以上代碼,也就是執行到右括號時,發現表達式爲空,所以報錯)。
如果想要了解更多,可以參考ECMA-262-3 in detail. Chapter 5. Functions.
立即執行函數(IIFE)
看到這裏,相信你一定迫不及待地想知道究竟如何做了吧,其實很簡單,只需要用括號全部括起來即可,比如下面這樣:
JavaScript
1 (function(){ /* code */ }());
爲什麼這樣就能立即執行並且不報錯呢?因爲在javascript裏,括號內部不能包含語句,當解析器對代碼進行解釋的時候,先碰到了(),然後碰到function關鍵字就會自動將()裏面的代碼識別爲函數表達式而不是函數聲明。
而立即執行函數並非只有上面的一種寫法,寫法真是五花八門:
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 最常用的兩種寫法
(function(){ /* code */ }()); // 老道推薦寫法
(function(){ /* code */ })(); // 當然這種也可以

// 括號和JS的一些操作符(如 = && || ,等)可以在函數表達式和函數聲明上消除歧義
// 如下代碼中,解析器已經知道一個是表達式了,於是也會把另一個默認爲表達式
// 但是兩者交換則會報錯
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();

// 如果你不怕代碼晦澀難讀,也可以選擇一元運算符
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();

// 你也可以這樣
new function(){ /* code */ }
new function(){ /* code */ }() // 帶參數
  • 無論何時,給立即執行函數加上括號是個好習慣
通過以上的介紹,我們大概瞭解通過()可以使得一個函數表達式立即執行。
有的時候,我們實際上不需要使用()使之變成一個函數表達式,啥意思?比如下面這行代碼,其實不加上()也不會保錯:
JavaScript
1 var i = function(){ return 10; }();
但是我們依然推薦加上():
JavaScript
1 var i = (function(){ return 10; }());
爲什麼?因爲我們在閱讀代碼的時候,如果function內部代碼量龐大,我們不得不滾動到最後去查看function(){}後是否帶有()來確定i值是個function還是function內部的返回值。所以爲了代碼的可讀性,請儘量加上()無論是否已經是表達式。
  • 立即執行函數與閉包的曖昧關係
立即執行函數能配合閉包保存狀態。
像普通的函數傳參一樣,立即執行函數也能傳參數。如果在函數內部再定義一個函數,而裏面的那個函數能引用外部的變量和參數(閉包),利用這一點,我們能使用立即執行函數鎖住變量保存狀態。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 並不會像你想象那樣的執行,因爲i的值沒有被鎖住
// 當我們點擊鏈接的時候,其實for循環已經執行完了
// 於是在點擊的時候i的值其實已經是elems.length了
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' );

}

// 這次我們得到了想要的結果
// 因爲在立即執行函數內部,i的值傳給了lockedIndex,並且被鎖在內存中
// 儘管for循環結束後i的值已經改變,但是立即執行函數內部lockedIndex的值並不會改變
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 );

}

// 你也可以這樣,但是毫無疑問上面的代碼更具有可讀性
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' );

}
其實上面代碼的lockedIndex也可以換成i,因爲兩個i是在不同的作用域裏,所以不會互相干擾,但是寫成不同的名字更好解釋。以上便是立即執行函數+閉包的作用。
  • 我爲什麼更願意稱它是“立即執行函數”而不是“自執行函數”
IIFE的稱謂在現在似乎已經得到了廣泛推廣(不知道是不是原文作者的功勞?),而原文寫於10年,似乎當時流行的稱呼是自執行函數(Self-executing anonymous function),接下去作者開始爲了說明立即執行函數的稱呼好於自執行函數的稱呼開始據理力爭,有點咬文嚼字,不過也蠻有意思的,我們來看看作者說了些什麼。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 這是一個自執行函數,函數內部執行的是自己,遞歸調用
function foo() { foo(); }

// 這是一個自執行匿名函數,因爲它沒有函數名
// 所以如果要遞歸調用自己的話必須用arguments.callee
var foo = function() { arguments.callee(); };

// 這可能也算是個自執行匿名函數,但僅僅是foo標誌引用它自身
// 如果你將foo改變成其它的,你將得到一個used-to-self-execute匿名函數
var foo = function() { foo(); };

// 有些人叫它自執行匿名函數,儘管它沒有執行自己,只是立即執行而已
(function(){ /* code */ }());

// 給函數表達式添加了標誌名稱,可以方便debug
// 但是一旦添加了標誌名稱,這個函數就不再是匿名的了
(function foo(){ /* code */ }());

// 立即執行函數也可以自執行,不過不常用罷了
(function(){ arguments.callee(); }());
(function foo(){ foo(); }());
我的理解是作者認爲自執行函數是函數內部調用自己(遞歸調用),而立即執行函數就如字面意思,該函數立即執行即可。其實現在也不用去管它了,就叫IIFE好了。
  • 最後的旁白:模塊模式
立即執行函數在模塊化中也大有用處。用立即執行函數處理模塊化可以減少全局變量造成的空間污染,構造更多的私有變量。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 創建一個立即執行的匿名函數
// 該函數返回一個對象,包含你要暴露的屬性
// 如下代碼如果不使用立即執行函數,就會多一個屬性i
// 如果有了屬性i,我們就能調用counter.i改變i的值
// 對我們來說這種不確定的因素越少越好

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並不是counter的屬性
i; // ReferenceError: i is not defined (函數內部的是局部變量)
擴展閱讀
如果你願意瞭解更多內容,特別是關於函數和模塊模式的內容,建議閱讀下列文章。
  1. ECMA-262-3 in detail. Chapter 5. Functions. – Dmitry A. Soshnikov
  2. Functions and function scope – Mozilla Developer Network
  3. Named function expressions – Juriy “kangax” Zaytsev
  4. JavaScript Module Pattern: In-Depth – Ben Cherry
  5. Closures explained with JavaScript – Nick Morgan
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章