寫在前面
這是一篇譯文,原文:Immediately-Invoked Function Expression (IIFE)
原文是一篇很經典的講解IIFE的文章,很適合收藏。本文雖然是譯文,但是直譯的很少,而且新增了不少自己的理解。
ps:下文中提到的“立即執行函式”其實就是“立即執行函式表示式”
我們要說的到底是什麼?
在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(){}),函式名後加上一對小括號即可完成對該函式的呼叫,比如下面的程式碼:
1 2 |
var foo = function(){ /* code */ }; foo(); |
接著我們來看下面的程式碼:
1 |
function(){ /* code */ }(); // SyntaxError: Unexpected token ( |
報錯了,這是為何?這是因為在javascript程式碼解釋時,當遇到function關鍵字時,會預設把它當做是一個函式宣告,而不是函式表示式,如果沒有把它顯視地表達成函式表示式,就報錯了,因為函式宣告需要一個函式名,而上面的程式碼中函式沒有函式名。(以上程式碼,也正是在執行到第一個左括號(時報錯,因為(前理論上是應該有個函式名的。)
- 一波未平一波又起
有意思的是,如果我們給它函式名,然後加上()立即呼叫,同樣也會報錯,而這次報錯原因卻不相同:
1 |
function foo(){ /* code */ }(); // SyntaxError: Unexpected token ) |
為什麼會這樣?在一個表示式後面加上括號,表示該表示式立即執行;而如果是在一個語句後面加上括號,該括號完全和之前的語句不搭嘎,而只是一個分組操作符,用來控制運算中的優先順序(小括號裡的先運算)。所以以上程式碼等價於:
1 2 |
function foo(){ /* code */ } (); // SyntaxError: Unexpected token ) |
相當於先宣告瞭一個叫foo的函式,之後進行()內的表示式運算,但是()(分組操作符)內的表示式不能為空,所以報錯。(以上程式碼,也就是執行到右括號時,發現表示式為空,所以報錯)。
如果想要了解更多,可以參考ECMA-262-3 in detail. Chapter 5. Functions.
立即執行函式(IIFE)
看到這裡,相信你一定迫不及待地想知道究竟如何做了吧,其實很簡單,只需要用括號全部括起來即可,比如下面這樣:
1 |
(function(){ /* code */ }()); |
為什麼這樣就能立即執行並且不報錯呢?因為在javascript裡,括號內部不能包含語句,當解析器對程式碼進行解釋的時候,先碰到了(),然後碰到function關鍵字就會自動將()裡面的程式碼識別為函式表示式而不是函式宣告。
而立即執行函式並非只有上面的一種寫法,寫法真是五花八門:
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 */ }() // 帶引數 |
- 無論何時,給立即執行函式加上括號是個好習慣
通過以上的介紹,我們大概瞭解通過()可以使得一個函式表示式立即執行。
有的時候,我們實際上不需要使用()使之變成一個函式表示式,啥意思?比如下面這行程式碼,其實不加上()也不會保錯:
1 |
var i = function(){ return 10; }(); |
但是我們依然推薦加上():
1 |
var i = (function(){ return 10; }()); |
為什麼?因為我們在閱讀程式碼的時候,如果function內部程式碼量龐大,我們不得不滾動到最後去檢視function(){}後是否帶有()來確定i值是個function還是function內部的返回值。所以為了程式碼的可讀性,請儘量加上()無論是否已經是表示式。
- 立即執行函式與閉包的曖昧關係
立即執行函式能配合閉包儲存狀態。
像普通的函式傳參一樣,立即執行函式也能傳引數。如果在函式內部再定義一個函式,而裡面的那個函式能引用外部的變數和引數(閉包),利用這一點,我們能使用立即執行函式鎖住變數儲存狀態。
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),接下去作者開始為了說明立即執行函式的稱呼好於自執行函式的稱呼開始據理力爭,有點咬文嚼字,不過也蠻有意思的,我們來看看作者說了些什麼。
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好了。
- 最後的旁白:模組模式
立即執行函式在模組化中也大有用處。用立即執行函式處理模組化可以減少全域性變數造成的空間汙染,構造更多的私有變數。
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 (函式內部的是區域性變數) |
擴充套件閱讀
如果你願意瞭解更多內容,特別是關於函式和模組模式的內容,建議閱讀下列文章。
- 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
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!