好文,屯一波:原文地址:https://www.jianshu.com/p/c64bfbcd34c3
在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
輸出結果並不是我們所預想的1,2,3
。當然,這個要涉及到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);
}
// 輸出 "1,2,3"
分析:儘管循環執行結束,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){
setTimeout(function(){
console.log(i); // undefined,undefined,undefined
}
,300);
})();
}
for( var i=0;i<3;i++){
setTimeout((function(i){
console.log(i); // undefined,undefined,undefined
})()
,300);
}
2. html元素綁定事件
假設要對頁面上的元素安裝點擊相同的點擊事件。我們會考慮如下方式。
<div id="demo">
<p>p1</p>
<p>p2</p>
<p>p3</p>
<p>p4</p>
<p>p5</p>
</div>
<script type="text/javascript">
var oDiv = document.getElementById("demo");
var eles = oDiv.getElementsByTagName("p");
for ( var k=0; k < eles.length; k++){
eles[k].addEventListener('click',function(e){
alert("index is: " + k + ", and this ele is: " + eles[k]); // index is: 5, and this ele is:undefined
});
/** 安裝事件方式也可以用 onclick 方式。不過這種方式安裝多個onclick觸發事件時,只執行最後安裝的那一個。 */
// eles[k].onclick = function(){
// alert("index is: " + k + ", and this ele is: " + eles[k]);
// }
}
</script>
我們期望點擊某個 p
元素,能得到該元素所在的索引,但實際是,點擊每個p
,索引值都是5
,而對應的元素都是undefined
。
分析:這種現象和上面的延時器類似,JavaScript在執行for
循環語句時,負責給元素安裝點擊事件,但當用戶點擊元素觸發事件時,for
循環語句早就執行完畢了,此時的 i
自然是5
了。
一樣的,我們也希望“鎖住”索引i
。所以可以如上採用自執行函數方式( 在addEventListener外部 ):
/** 1. 自執行函數方式一 */
for ( var k=0; k < eles.length; k++){
(function(k){
eles[k].addEventListener('click',function(e){
alert("index is: " + k + ", and this ele is: " + eles[k].innerHTML);
});
})(k);
}
也可以 在addEventListener裏面 的處理函數使用自執行函數表達式,具體如下。不過上面的方式更具有可讀性。
/** 2. 自執行函數方式二 */
for ( var k=0; k < eles.length; k++){
eles[k].addEventListener('click',function(k){
return function(e){
alert("index is: " + k + ", and this ele is: " + eles[k].innerHTML);
}
}(k));
}
當然,除了自執行函數表達式,我們還有一種討巧的解決辦法:
/** 3. 討巧的解決方案 */
for ( var k=0; k < eles.length; k++){
eles[k].index = k;
eles[k].addEventListener('click',function(e){
alert("index is: " + this.index + ", and this ele is: " + eles[this.index].innerHTML);
});
}
// 把索引 k 保存在元素的屬性中。在點擊元素觸發事件時,巧用 this 關鍵字去取出當前點擊對象的屬性 index,也就是對應的索引。
四、自執行與立即執行
最後來嘮嗑下命名方式。
文中對 (function () {/*code*/} ) ()
這種表達式,稱作爲 自執行匿名函數(Self-executing anonymous function);而參考的英文博文中作者更建議稱它爲 立即調用的函數表達式(Immediately-Invoked Function Expression)。
以下是截取該參考博文的例子:
// 自執行函數。自己調用自己(遞歸)
function foo() { foo(); }
// 自執行的匿名函數。
var foo = function () { arguments.callee(); };
// 立即執行匿名函數。但我們習慣稱其爲:自執行的匿名函數。
(function () { /* code */ } ());
// 立即執行函數。加一個標示名稱,可以方便Debug
(function foo() { /* code */ } ());
// 立即調用的函數表達式(IIFE)也可以自執行,不過可能不常用罷了
(function () { arguments.callee(); } ());
(function foo() { foo(); } ());
注意:arguments.callee在ECMAScript 5 strict mode裏被廢棄了。
個人愚見:上面例子中把 自執行 解釋成 “自己調用自己”,當然和 立即執行 相差很大了。但如果把 自執行 解釋成 “自動執行”,就和 立即執行 異曲同工了。
命名方式絕對統一也沒必要,重要的是能深入瞭解並應用它們。
參考內容: