深入理解JavaScript之立即調(diào)用的函數(shù)表達式

什么是自執(zhí)行?
// 由于該function里返回了另外一個function,其中這個function可以訪問自由變量i// 所有說,這個內(nèi)部的function實際上是有權限可以調(diào)用內(nèi)部的對象。function makeCounter() {// 只能在makeCounter內(nèi)部訪問ivar i = 0;return function () {console.log(++i);};}// 注意,counter和counter2是不同的實例,分別有自己范圍內(nèi)的i。var counter = makeCounter();counter(); // logs: 1counter(); // logs: 2var counter2 = makeCounter();counter2(); // logs: 1counter2(); // logs: 2alert(i); // 引用錯誤:i沒有defind(因為i是存在于makeCounter內(nèi)部)。
問題的核心
// 因為想下面第一個聲明的function可以在后面加一個括弧()就可以自己執(zhí)行了,比如foo(),// 因為foo僅僅是function() { /* code */ }這個表達式的一個引用var foo = function(){ /* code */ }// ...是不是意味著后面加個括弧都可以自動執(zhí)行?function(){ /* code */ }(); // SyntaxError: Unexpected token (//
旁白:函數(shù)(function),括弧(paren),語法錯誤(SyntaxError)
// 下面這個function在語法上是沒問題的,但是依然只是一個語句// 加上括號()以后依然會報錯,因為分組操作符需要包含表達式function foo(){ /* code */ }(); // SyntaxError: Unexpected token )// 但是如果你在括弧()里傳入一個表達式,將不會有異常拋出// 但是foo函數(shù)依然不會執(zhí)行function foo(){ /* code */ }( 1 );// 因為它完全等價于下面這個代碼,一個function聲明后面,又聲明了一個毫無關系的表達式:function foo(){ /* code */ }( 1 );
你可以訪問ECMA-262-3 in detail. Chapter 5. Functions 獲取進一步的信息。
自執(zhí)行函數(shù)表達式
要解決上述問題,非常簡單,我們只需要用大括弧將代碼的代碼全部括住就行了,因為JavaScript里括弧()里面不能包含語句,所以在這一點上,解析器在解析function關鍵字的時候,會將相應的代碼解析成function表達式,而不是function聲明。
// 下面2個括弧()都會立即執(zhí)行(function () { /* code */ } ()); // 推薦使用這個(function () { /* code */ })(); // 但是這個也是可以用的// 由于括弧()和JS的&&,異或,逗號等操作符是在函數(shù)表達式和函數(shù)聲明上消除歧義的// 所以一旦解析器知道其中一個已經(jīng)是表達式了,其它的也都默認為表達式了// 不過,請注意下一章節(jié)的內(nèi)容解釋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/18209252090847232new function () { /* code */ }new function () { /* code */ } () // 如果需要傳遞參數(shù),只需要加上括弧()
上面所說的括弧是消除歧義的,其實壓根就沒必要,因為括弧本來內(nèi)部本來期望的就是函數(shù)表達式,但是我們依然用它,主要是為了方便開發(fā)人員閱讀,當你讓這些已經(jīng)自動執(zhí)行的表達式賦值給一個變量的時候,我們看到開頭有括弧(,很快就能明白,而不需要將代碼拉到最后看看到底有沒有加括弧。
用閉包保存狀態(tài)
和普通function執(zhí)行的時候傳參數(shù)一樣,自執(zhí)行的函數(shù)表達式也可以這么傳參,因為閉包直接可以引用傳入的這些參數(shù),利用這些被lock住的傳入?yún)?shù),自執(zhí)行函數(shù)表達式可以有效地保存狀態(tài)。
// 這個代碼是錯誤的,因為變量i從來就沒背locked住// 相反,當循環(huán)執(zhí)行以后,我們在點擊的時候i才獲得數(shù)值// 因為這個時候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');}// 這個是可以用的,因為他在自執(zhí)行函數(shù)表達式閉包內(nèi)部// i的值作為locked的索引存在,在循環(huán)執(zhí)行結束以后,盡管最后i的值變成了a元素總數(shù)(例如10)// 但閉包內(nèi)部的lockedInIndex值是沒有改變,因為他已經(jīng)執(zhí)行完畢了// 所以當點擊連接的時候,結果是正確的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);}// 你也可以像下面這樣應用,在處理函數(shù)那里使用自執(zhí)行函數(shù)表達式// 而不是在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不在一個作用于,所以不會出現(xiàn)問題,這也是匿名函數(shù)+閉包的威力。
自執(zhí)行匿名函數(shù)和立即執(zhí)行的函數(shù)表達式區(qū)別
在這篇帖子里,我們一直叫自執(zhí)行函數(shù),確切的說是自執(zhí)行匿名函數(shù)(Self-executing anonymous function),但英文原文作者一直倡議使用立即調(diào)用的函數(shù)表達式(Immediately-Invoked Function Expression)這一名稱,作者又舉了一堆例子來解釋,好吧,我們來看看:
// 這是一個自執(zhí)行的函數(shù),函數(shù)內(nèi)部執(zhí)行自身,遞歸function foo() { foo(); }// 這是一個自執(zhí)行的匿名函數(shù),因為沒有標示名稱// 必須使用arguments.callee屬性來執(zhí)行自己var foo = function () { arguments.callee(); };// 這可能也是一個自執(zhí)行的匿名函數(shù),僅僅是foo標示名稱引用它自身// 如果你將foo改變成其它的,你將得到一個used-to-self-execute匿名函數(shù)var foo = function () { foo(); };// 有些人叫這個是自執(zhí)行的匿名函數(shù)(即便它不是),因為它沒有調(diào)用自身,它只是立即執(zhí)行而已。(function () { /* code */ } ());// 為函數(shù)表達式添加一個標示名稱,可以方便Debug// 但一定命名了,這個函數(shù)就不再是匿名的了(function foo() { /* code */ } ());// 立即調(diào)用的函數(shù)表達式(IIFE)也可以自執(zhí)行,不過可能不常用罷了(function () { arguments.callee(); } ());(function foo() { foo(); } ());// 另外,下面的代碼在黑莓5里執(zhí)行會出錯,因為在一個命名的函數(shù)表達式里,他的名稱是undefined// 呵呵,奇怪(function foo() { foo(); } ());
希望這里的一些例子,可以讓大家明白,什么叫自執(zhí)行,什么叫立即調(diào)用。
注:arguments.callee在ECMAScript 5 strict mode里被廢棄了,所以在這個模式下,其實是不能用的。
最后的旁白:Module模式
在講到這個立即調(diào)用的函數(shù)表達式的時候,我又想起來了Module模式,如果你還不熟悉這個模式,我們先來看看代碼:
// 創(chuàng)建一個立即調(diào)用的匿名函數(shù)表達式// return一個變量,其中這個變量里包含你要暴露的東西// 返回的這個變量將賦值給counter,而不是外面聲明的function自身var counter = (function () {var i = 0;return {get: function () {return i;},set: function (val) {i = val;},increment: function () {return ++i;}};} ());// counter是一個帶有多個屬性的對象,上面的代碼對于屬性的體現(xiàn)其實是方法counter.get(); // 0counter.set(3);counter.increment(); // 4counter.increment(); // 5counter.i; // undefined 因為i不是返回對象的屬性i; // 引用錯誤: i 沒有定義(因為i只存在于閉包)

