淺談自執行函數-立即調用的函數表達式

 好文,屯一波:原文地址: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 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

輸出結果並不是我們所預想的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裏被廢棄了。

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

參考內容:

  1. 深入理解JavaScript系列(4):立即調用的函數表達式
  2. Immediately-Invoked Function Expression (IIFE)
  3.  
發佈了63 篇原創文章 · 獲贊 27 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章