## 前言
大家學JavaScript的時候,經常遇到自執行匿名函數的代碼,今天我們主要就來想想說一下自執行。
在詳細了解這個之前,我們來談了解一下“自執行”這個叫法,本文對這個功能的叫法也不一定完全對,主要是看個人如何理解,因為有的人說立即調用,有的人說自動執行,所以你完全可以按照你自己的理解來取一個名字,不過我聽很多人都叫它為“自執行”,但作者后面說了很多,來說服大家稱呼為“立即調用的函數表達式”。
本文英文原文地址:http://benalman.com/news/2010/11/immediately-invoked-function-expression/
## 什么是自執行?
在JavaScript里,任何function在執行的時候都會創建一個執行上下文,因為為function聲明的變量和function有可能只在該function內部,這個上下文,在調用function的時候,提供了一種簡單的方式來創建自由變量或私有子function。
~~~
// 由于該function里返回了另外一個function,其中這個function可以訪問自由變量i
// 所有說,這個內部的function實際上是有權限可以調用內部的對象。
function makeCounter() {
// 只能在makeCounter內部訪問i
var i = 0;
return function () {
console.log(++i);
};
}
// 注意,counter和counter2是不同的實例,分別有自己范圍內的i。
var counter = makeCounter();
counter(); // logs: 1
counter(); // logs: 2
var counter2 = makeCounter();
counter2(); // logs: 1
counter2(); // logs: 2
alert(i); // 引用錯誤:i沒有defind(因為i是存在于makeCounter內部)。
~~~
很多情況下,我們不需要makeCounter多個實例,甚至某些case下,我們也不需要顯示的返回值,OK,往下看。
## 問題的核心
當你聲明類似function foo(){}或var foo = function(){}函數的時候,通過在后面加個括弧就可以實現自執行,例如foo(),看代碼:
~~~
// 因為想下面第一個聲明的function可以在后面加一個括弧()就可以自己執行了,比如foo(),
// 因為foo僅僅是function() { /* code */ }這個表達式的一個引用
var foo = function(){ /* code */ }
// ...是不是意味著后面加個括弧都可以自動執行?
function(){ /* code */ }(); // SyntaxError: Unexpected token (
//
~~~
上述代碼,如果甚至運行,第2個代碼會出錯,因為在解析器解析全局的function或者function內部function關鍵字的時候,默認是認為function聲明,而不是function表達式,如果你不顯示告訴編譯器,它默認會聲明成一個缺少名字的function,并且拋出一個語法錯誤信息,因為function聲明需要一個名字。
## 旁白:函數(function),括弧(paren),語法錯誤(SyntaxError)
有趣的是,即便你為上面那個錯誤的代碼加上一個名字,他也會提示語法錯誤,只不過和上面的原因不一樣。在一個表達式后面加上括號(),該表達式會立即執行,但是在一個語句后面加上括號(),是完全不一樣的意思,他的只是分組操作符。
~~~
// 下面這個function在語法上是沒問題的,但是依然只是一個語句
// 加上括號()以后依然會報錯,因為分組操作符需要包含表達式
function foo(){ /* code */ }(); // SyntaxError: Unexpected token )
// 但是如果你在括弧()里傳入一個表達式,將不會有異常拋出
// 但是foo函數依然不會執行
function foo(){ /* code */ }( 1 );
// 因為它完全等價于下面這個代碼,一個function聲明后面,又聲明了一個毫無關系的表達式:
function foo(){ /* code */ }
( 1 );
~~~
你可以訪問[ECMA-262-3 in detail. Chapter 5\. Functions](http://dmitrysoshnikov.com/ecmascript/chapter-5-functions/#question-about-surrounding-parentheses) 獲取進一步的信息。
## 自執行函數表達式
要解決上述問題,非常簡單,我們只需要用大括弧將代碼的代碼全部括住就行了,因為JavaScript里括弧()里面不能包含語句,所以在這一點上,解析器在解析function關鍵字的時候,會將相應的代碼解析成function表達式,而不是function聲明。
~~~
// 下面2個括弧()都會立即執行
(function () { /* code */ } ()); // 推薦使用這個
(function () { /* code */ })(); // 但是這個也是可以用的
// 由于括弧()和JS的&&,異或,逗號等操作符是在函數表達式和函數聲明上消除歧義的
// 所以一旦解析器知道其中一個已經是表達式了,其它的也都默認為表達式了
// 不過,請注意下一章節的內容解釋
var i = function () { return 10; } ();
true && function () { /* code */ } ();
0, function () { /* code */ } ();
// 如果你不在意返回值,或者不怕難以閱讀
// 你甚至可以在function前面加一元操作符號
!function () { /* code */ } ();
~function () { /* code */ } ();
-function () { /* code */ } ();
+function () { /* code */ } ();
// 還有一個情況,使用new關鍵字,也可以用,但我不確定它的效率
// http://twitter.com/kuvos/status/18209252090847232
new function () { /* code */ }
new function () { /* code */ } () // 如果需要傳遞參數,只需要加上括弧()
~~~
上面所說的括弧是消除歧義的,其實壓根就沒必要,因為括弧本來內部本來期望的就是函數表達式,但是我們依然用它,主要是為了方便開發人員閱讀,當你讓這些已經自動執行的表達式賦值給一個變量的時候,我們看到開頭有括弧(,很快就能明白,而不需要將代碼拉到最后看看到底有沒有加括弧。
## 用閉包保存狀態
和普通function執行的時候傳參數一樣,自執行的函數表達式也可以這么傳參,因為閉包直接可以引用傳入的這些參數,利用這些被lock住的傳入參數,自執行函數表達式可以有效地保存狀態。
~~~
// 這個代碼是錯誤的,因為變量i從來就沒背locked住
// 相反,當循環執行以后,我們在點擊的時候i才獲得數值
// 因為這個時候i操真正獲得值
// 所以說無論點擊那個連接,最終顯示的都是I am link #10(如果有10個a元素的話)
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的值作為locked的索引存在,在循環執行結束以后,盡管最后i的值變成了a元素總數(例如10)
// 但閉包內部的lockedInIndex值是沒有改變,因為他已經執行完畢了
// 所以當點擊連接的時候,結果是正確的
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);
}
// 你也可以像下面這樣應用,在處理函數那里使用自執行函數表達式
// 而不是在addEventListener外部
// 但是相對來說,上面的代碼更具可讀性
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');
}
~~~
其實,上面2個例子里的lockedInIndex變量,也可以換成i,因為和外面的i不在一個作用于,所以不會出現問題,這也是匿名函數+閉包的威力。
## 自執行匿名函數和立即執行的函數表達式區別
在這篇帖子里,我們一直叫自執行函數,確切的說是自執行匿名函數(Self-executing anonymous function),但英文原文作者一直倡議使用立即調用的函數表達式(Immediately-Invoked Function Expression)這一名稱,作者又舉了一堆例子來解釋,好吧,我們來看看:
~~~
// 這是一個自執行的函數,函數內部執行自身,遞歸
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 */ } ());
// 立即調用的函數表達式(IIFE)也可以自執行,不過可能不常用罷了
(function () { arguments.callee(); } ());
(function foo() { foo(); } ());
// 另外,下面的代碼在黑莓5里執行會出錯,因為在一個命名的函數表達式里,他的名稱是undefined
// 呵呵,奇怪
(function foo() { foo(); } ());
~~~
希望這里的一些例子,可以讓大家明白,什么叫自執行,什么叫立即調用。
_注:arguments.callee在[ECMAScript 5 strict mode](https://developer.mozilla.org/en/JavaScript/Strict_mode#Differences_in_functions)里被廢棄了,所以在這個模式下,其實是不能用的。_
## 最后的旁白:Module模式
在講到這個立即調用的函數表達式的時候,我又想起來了Module模式,如果你還不熟悉這個模式,我們先來看看代碼:
~~~
// 創建一個立即調用的匿名函數表達式
// return一個變量,其中這個變量里包含你要暴露的東西
// 返回的這個變量將賦值給counter,而不是外面聲明的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; // 引用錯誤: i 沒有定義(因為i只存在于閉包)
~~~
關于更多Module模式的介紹,請訪問我的上一篇帖子:深入理解JavaScript系列(2):全面解析Module模式 。
## 更多閱讀
希望上面的一些例子,能讓你對立即調用的函數表達(也就是我們所說的自執行函數)有所了解,如果你想了解更多關于function和Module模式的信息,請繼續訪問下面列出的網站:
1. [ECMA-262-3 in detail. Chapter 5\. Functions.](http://dmitrysoshnikov.com/ecmascript/chapter-5-functions/#question-about-surrounding-parentheses) - Dmitry A. Soshnikov
2. [Functions and function scope](https://developer.mozilla.org/en/JavaScript/Reference/Functions_and_function_scope) - Mozilla Developer Network
3. [Named function expressions](http://kangax.github.com/nfe/) - Juriy “kangax” Zaytsev
4. [全面解析Module模式](http://www.cnblogs.com/TomXu/archive/2011/12/30/2288372.html)- Ben Cherry(大叔翻譯整理)
5. [Closures explained with JavaScript](http://skilldrick.co.uk/2011/04/closures-explained-with-javascript/) - Nick Morgan
- (1)編寫高質量JavaScript代碼的基本要點
- (2)揭秘命名函數表達式
- (3)全面解析Module模式
- (4)立即調用的函數表達式
- (5)強大的原型和原型鏈
- (6)S.O.L.I.D五大原則之單一職責SRP
- (7)S.O.L.I.D五大原則之開閉原則OCP
- (8)S.O.L.I.D五大原則之里氏替換原則LSP
- (9)根本沒有“JSON對象”這回事!
- (10)JavaScript核心(晉級高手必讀篇)
- (11)執行上下文(Execution Contexts)
- (12)變量對象(Variable Object)
- (13)This? Yes, this!
- (14)作用域鏈(Scope Chain)
- (15)函數(Functions)
- (16)閉包(Closures)
- (17)面向對象編程之一般理論
- (18)面向對象編程之ECMAScript實現
- (19)求值策略
- (20)《你真懂JavaScript嗎?》答案詳解
- (21)S.O.L.I.D五大原則之接口隔離原則ISP
- (22)S.O.L.I.D五大原則之依賴倒置原則DIP
- (23)JavaScript與DOM(上)——也適用于新手
- (24)JavaScript與DOM(下)
- (25)設計模式之單例模式
- (26)設計模式之構造函數模式
- (27)設計模式之建造者模式
- (28)設計模式之工廠模式
- (29)設計模式之裝飾者模式
- (30)設計模式之外觀模式
- (31)設計模式之代理模式
- (32)設計模式之觀察者模式
- (33)設計模式之策略模式
- (34)設計模式之命令模式
- (35)設計模式之迭代器模式
- (36)設計模式之中介者模式
- (37)設計模式之享元模式
- (38)設計模式之職責鏈模式
- (39)設計模式之適配器模式
- (40)設計模式之組合模式
- (41)設計模式之模板方法
- (42)設計模式之原型模式
- (43)設計模式之狀態模式
- (44)設計模式之橋接模式
- (45)代碼復用模式(避免篇)
- (46)代碼復用模式(推薦篇)
- (47)對象創建模式(上篇)
- (48)對象創建模式(下篇)
- (49)Function模式(上篇)
- (50)Function模式(下篇)
- (結局篇)