寫在前面
這是一篇譯文,原文:Immediately-Invoked Function Expression (IIFE)
原文是一篇很經典的講解IIFE的文章,很適合收藏。本文雖然是譯文,但是直譯的很少,而且添加了不少自己的理解。
ps:下文中提到的“立即執行函數”其實就是“立即執行函數表達式”
我們要說的到底是什么?
在javascript中,每一個函數在被調用的時候都會創建一個執行上下文,在該函數內部定義的變量和函數只能在該函數內部被使用,而正是因為這個上下文,使得我們在調用函數的時候能創建一些私有變量。
// 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(){}),函數名后加上一對小括號即可完成對該函數的調用,比如下面的代碼:
var foo = function(){ /* code */ }; foo();
接着我們來看下面的代碼:
function(){ /* code */ }(); // SyntaxError: Unexpected token (
報錯了,這是為何?這是因為在javascript代碼解釋時,當遇到function關鍵字時,會默認把它當做是一個函數聲明,而不是函數表達式,如果沒有把它顯視地表達成函數表達式,就報錯了,因為函數聲明需要一個函數名,而上面的代碼中函數沒有函數名。(以上代碼,也正是在執行到第一個左括號(時報錯,因為(前理論上是應該有個函數名的。)
- 一波未平一波又起
有意思的是,如果我們給它函數名,然后加上()立即調用,同樣也會報錯,而這次報錯原因卻不相同:
function foo(){ /* code */ }(); // SyntaxError: Unexpected token )
為什么會這樣?在一個表達式后面加上括號,表示該表達式立即執行;而如果是在一個語句后面加上括號,該括號完全和之前的語句不搭嘎,而只是一個分組操作符,用來控制運算中的優先級(小括號里的先運算)。所以以上代碼等價於:
function foo(){ /* code */ } (); // SyntaxError: Unexpected token )
相當於先聲明了一個叫foo的函數,之后進行()內的表達式運算,但是()(分組操作符)內的表達式不能為空,所以報錯。(以上代碼,也就是執行到右括號時,發現表達式為空,所以報錯)。
如果想要了解更多,可以參考ECMA-262-3 in detail. Chapter 5. Functions.
立即執行函數(IIFE)
看到這里,相信你一定迫不及待地想知道究竟如何做了吧,其實很簡單,只需要用括號全部括起來即可,比如下面這樣:
(function(){ /* code */ }());
為什么這樣就能立即執行並且不報錯呢?因為在javascript里,括號內部不能包含語句,當解析器對代碼進行解釋的時候,先碰到了(),然后碰到function關鍵字就會自動將()里面的代碼識別為函數表達式而不是函數聲明。
而立即執行函數並非只有上面的一種寫法,寫法真是五花八門:
// 最常用的兩種寫法 (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 */ }() // 帶參數
- 無論何時,給立即執行函數加上括號是個好習慣
通過以上的介紹,我們大概了解通過()可以使得一個函數表達式立即執行。
有的時候,我們實際上不需要使用()使之變成一個函數表達式,啥意思?比如下面這行代碼,其實不加上()也不會保錯:
var i = function(){ return 10; }();
但是我們依然推薦加上():
var i = (function(){ return 10; }());
為什么?因為我們在閱讀代碼的時候,如果function內部代碼量龐大,我們不得不滾動到最后去查看function(){}后是否帶有()來確定i值是個function還是function內部的返回值。所以為了代碼的可讀性,請盡量加上()無論是否已經是表達式。
- 立即執行函數與閉包的曖昧關系
立即執行函數能配合閉包保存狀態。
像普通的函數傳參一樣,立即執行函數也能傳參數。如果在函數內部再定義一個函數,而里面的那個函數能引用外部的變量和參數(閉包),利用這一點,我們能使用立即執行函數鎖住變量保存狀態。
// 並不會像你想象那樣的執行,因為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),接下去作者開始為了說明立即執行函數的稱呼好於自執行函數的稱呼開始據理力爭,有點咬文嚼字,不過也蠻有意思的,我們來看看作者說了些什么。
// 這是一個自執行函數,函數內部執行的是自己,遞歸調用 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好了。
- 最后的旁白:模塊模式
立即執行函數在模塊化中也大有用處。用立即執行函數處理模塊化可以減少全局變量造成的空間污染,構造更多的私有變量。
// 創建一個立即執行的匿名函數 // 該函數返回一個對象,包含你要暴露的屬性 // 如下代碼如果不使用立即執行函數,就會多一個屬性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
某坑
在實際開發中,關於 IIFE 遇到了一個坑,即 IIFE 中使用 JSONP,很顯然 JSONP 中的函數調用是獲取不到匿名函數中定義的函數的:
!function() { // 回調函數定義在匿名函數內,JSONP回調找不到該函數 function callback() { //... } T.getScript('..'); // 獲取JSONP接口 }();
不僅僅是在 IIFE 中,如果是這樣,也會出錯:
// 為了封裝,把一系列調用寫在一個函數內 function fn() { // 設置回調 function callback() { //... } T.getScript('..'); // JSONP } fn();
很顯然是一樣的道理,所以在用 JSONP 的時候,特別要注意它的回調函數定義必須是全局的,可以將回調函數手動設置為 window.callback() 的形式。