自調用函數


在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 ivar 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){ 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里被廢棄了。

個人愚見:上面例子中把 自執行 解釋成 “自己調用自己”,當然和 立即執行 相差很大了。但如果把 自執行 解釋成 “自動執行”,就和 立即執行 異曲同工了。
命名方式絕對統一也沒必要,重要的是能深入了解並應用它們。



作者:celineWong7
鏈接:https://www.jianshu.com/p/c64bfbcd34c3
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM