在JavaScript中,會遇到自執行匿名函數:
(function () {/*code*/} ) ()
。
這個結構大家並不陌生,但若要說:為什么要括弧起來?它的應用場景有哪些?……就會有點模糊。
此處作個小結。
本文篇幅比較長,但例子都很簡單,可以跳躍式閱讀。
一、函數的聲明與執行
我們先來看下最初的函數聲明與執行:
// 聲明函數fun0 function fun0(){ console.log("fun0"); } //執行函數fun0 fun0(); // fun0
除了上面這種最常見的函數聲明方式,還有變量賦值方式的,如下:
// 聲明函數fun1 - 變量方式 var fun1 = function(){ console.log("fun1"); } // 執行函數fun1 fun1(); // fun1
二、 函數的一點猜想
既然函數名加上括號fun1()
就是執行函數。
思考:直接取賦值符號右側的內容直接加個括號,是否也能執行?
試驗如下,直接加上小括弧:
function(){ console.log("fun"); }();
以上會報錯 line1:Uncaught SyntaxError: Unexpected token (
。
分析: function
是聲明函數關鍵字,若非變量賦值方式聲明函數,默認其后面需要跟上函數名的。
加上函數名看看:
function fun2(){ console.log("fun2"); }();
以上會報錯 line3:Uncaught SyntaxError: Unexpected token )
。
分析: 聲明函數的結構花括弧后面不能有其他符號(比如此處的小括弧)。
不死心的再胡亂試一下,給它加個實參(表達式):
function fun3(){ console.log("fun3"); }(1);
不會報錯,但不會輸出結果fun3
。
分析: 以上代碼相當於在聲明函數后,又聲明了一個毫無關系的表達式。相當於如下代碼形式:
function fun3(){ console.log("fun3"); } (1); // 若此處執行fun3函數,可以輸出結果 fun3(); //"fun3"
三、自執行函數表達式
1. 正兒八經的自執行函數
想要解決上面問題,可以采用小括弧將要執行的代碼包含住(方式一),如下:
// 方式一 (function fun4(){ console.log("fun4"); }()); // "fun4"
分析:因為在JavaScript語言中,()
里面不能包含語句(只能是表達式),所以解析器在解析到function
關鍵字的時候,會把它們當作function表達式,而不是正常的函數聲明。
除了上面直接整個包含住,也可以只包含住函數體(方式二),如下:
// 方式二 (function fun5(){ console.log("fun5"); })();// "fun4"
寫法上建議采用方式一(這是參考文的建議。但實際上,我個人覺得方式二比較常見)。
2. “歪瓜裂棗”的自執行函數
除了上面()
小括弧可以把function
關鍵字作為函數聲明的含義轉換成函數表達式外,JavaScript的&&
與操作、||
或操作、,
逗號等操作符也有這個效果。
true && function () { console.log("true &&") } (); // "true &&" false || function () { console.log("true ||") } (); // "true ||" 0, function () { console.log("0,") } (); // "0," // 此處要注意: &&, || 的短路效應。即: false && (表達式1) 是不會觸發表達式1; // 同理,true || (表達式2) 不會觸發表達式2
如果不在意返回值,也不在意代碼的可讀性,我們甚至還可以使用一元操作符(!
~
-
+
),函數同樣也會立即執行。
!function () { console.log("!"); } (); //"!" ~function () { console.log("~"); } (); //"~" -function () { console.log("-"); } (); //"-" +function () { console.log("+"); } (); //"+"
甚至還可以使用new
關鍵字:
// 注意:采用new方式,可以不要再解釋花括弧 `}` 后面加小括弧 `()` new function () { console.log("new"); } //"new" // 如果需要傳遞參數 new function (a) { console.log(a); } ("newwwwwwww"); //"newwwwwwww"
嗯,最好玩的是賦值符號=
同樣也有此效用(例子中的i
變量方式):
//此處 要注意區分 i 和 j 不同之處。前者是函數自執行后返回值給 i ;后者是聲明一個函數,函數名為 j 。 var i = function () { console.log("output i:"); return 10; } (); // "output i:" var j = function () { console.log("output j:"); return 99;} console.log(i); // 10 console.log(j); // ƒ () { console.log("output j:"); return 99;}
上面提及到,要注意區分 var i
和 var j
不同之處(前者是函數自執行后返回值給i
;后者是聲明一個函數,函數名為j
)。如果是看代碼,我們需要查看代碼結尾是否有沒有()
才能區分。一般為了方便開發人員閱讀,我們會采用下面這種方式:
var i2 = (function () { console.log("output i2:"); return 10; } ()); // "output i2:" var i3 = (function () { console.log("output i3:"); return 10; }) (); // "output i3:" // 以上兩種都可以,但依舊建議采用第一種 i2 的方式。(個人依舊喜歡第二種i3方式)
四、自執行函數的應用
1. for循環 + setTimeout 例子
直接來看一個例子。for
循環里面通過延時器輸出索引 i
for( var i=0;i<3;i++){ setTimeout(function(){ console.log(i); } ,300); } // 輸出結果 3,3,3
輸出結果並不是我們所預想的0,1,2。當然,這個要涉及到setTimeout 的原理了,即使把300ms改成0ms,同樣也會輸出3,3,3
。具體可以查看博文 setTimeout(0) 的作用 。這里摘取其中一段說明。
JavaScript是單線程執行的,無法同時執行多段代碼。當某段代碼正在執行時,后續任務都必須等待,形成一個隊列。只有當前任務執行完畢,才會從隊列中取出下一個任務——也就是常說的“阻塞式執行”。
上面代碼中設定了一個setTimeout
,那瀏覽器會在合適時間(此處是300ms后)把代碼插入任務隊列,等待當前的for
循環代碼執行完畢再執行。(注意:setTimeout 雖然指定了延時的時間,但並不能保證執行的時間與設定的延時時間一直,是否准確取決於 JavaScript 線程是擁擠還是空閑。)
上面說了那么多,都是在分析為什么會輸出3,3,3
。那怎么樣才能輸出1,2,3
呢?
看看下面的方式(寫法一):把setTimeout
代碼包含在匿名自執行函數里面,就可以實現“鎖住”索引i
,正常輸出索引值。
for( var i=0;i<3;i++){ (function(lockedIndex){ setTimeout(function(){ console.log(lockedIndex); } ,300); })(i); } // 輸出 "0,1,2"
分析:盡管循環執行結束,i
值已經變成了3。但因遇到了自執行函數,當時的i
值已經被 lockedIndex
鎖住了。也可以理解為 自執行函數屬於for循環一部分,每次遍歷i
,自執行函數也會立即執行。所以盡管有延時器,但依舊會保留住立即執行時的i
值。
上面的分析有點模糊和牽強,也可以從 閉包 角度出發分析的。但鄙人“閉包”概念模糊,先遺憾下,以后再補充分析了。QAQ
除了上面的寫法,也可以直接在 setTimeout
第一個參數做自執行(寫法二),如下。
注意: 寫法二 會比 寫法一 先執行。原因不明。
for( var i=0;i<3;i++){ setTimeout((function(lockedInIndex){ console.log(lockedInIndex); })(i) ,300); }
關於 自執行函數參數 lockedInIndex
,補充說明以下幾點。
注意:自執行函數在 setTimeout 和在 setTimeout 里在第2、3中情況有區別(原因不明,后續再補)。
// 1. lockedInIndex變量,也可以換成i,因為和外面的i不在一個作用域 for( var i=0;i<3;i++){ (function(i){ setTimeout(function(){ console.log(i); // 1,2,3 } ,300); })(i); } for( var i=0;i<3;i++){ setTimeout((function(i){ console.log(i); // 1,2,3 })(i) ,300); } // 2. 自執行函數不帶入參數 for( var i=0;i<3;i++){ (function(){ setTimeout(function(){ console.log(i); // 3,3,3 } ,300); })(); } for( var i=0;i<3;i++){ setTimeout((function(){ console.log(i); // 1,2,3 })() ,300); } // 3. 自執行函數只有實參沒有寫形參 for( var i=0;i<3;i++){ (function(){ setTimeout(function(){ console.log(i); // 3,3,3 } ,300); })(i); } for( var i=0;i<3;i++){ setTimeout((function(){ console.log(i); // 1,2,3 })(i) ,300); } // 4. 自執行函數只有形參沒有寫實參,這種情況不行。因為會導致輸出 undefined。 for( var i=0;i<3;i++){ (function(i