淺談自執(zhí)行函數(shù)(立即調(diào)用的函數(shù)表達式)

在JavaScript中,會遇到自執(zhí)行匿名函數(shù):(function () {/*code*/} ) ()丙挽。
這個結(jié)構(gòu)大家并不陌生肺孵,但若要說:為什么要括弧起來?它的應(yīng)用場景有哪些颜阐?……就會有點模糊平窘。
此處作個小結(jié)。

本文篇幅比較長凳怨,但例子都很簡單瑰艘,可以跳躍式閱讀。

一肤舞、函數(shù)的聲明與執(zhí)行

我們先來看下最初的函數(shù)聲明與執(zhí)行:

    // 聲明函數(shù)fun0
    function fun0(){
        console.log("fun0");
    }

    //執(zhí)行函數(shù)fun0
    fun0(); // fun0

除了上面這種最常見的函數(shù)聲明方式紫新,還有變量賦值方式的,如下:

    // 聲明函數(shù)fun1 - 變量方式
    var fun1 = function(){
        console.log("fun1");
    }

    // 執(zhí)行函數(shù)fun1
    fun1(); // fun1

二李剖、 函數(shù)的一點猜想

既然函數(shù)名加上括號fun1()就是執(zhí)行函數(shù)芒率。
思考:直接取賦值符號右側(cè)的內(nèi)容直接加個括號,是否也能執(zhí)行篙顺?
試驗如下偶芍,直接加上小括怀湓瘛:

    function(){
        console.log("fun");
    }();

以上會報錯 line1:Uncaught SyntaxError: Unexpected token (
分析: function 是聲明函數(shù)關(guān)鍵字腋寨,若非變量賦值方式聲明函數(shù)聪铺,默認其后面需要跟上函數(shù)名的化焕。

加上函數(shù)名看看:

    function fun2(){
        console.log("fun2");
    }();

以上會報錯 line3:Uncaught SyntaxError: Unexpected token )萄窜。
分析: 聲明函數(shù)的結(jié)構(gòu)花括弧后面不能有其他符號(比如此處的小括弧)撒桨。

不死心的再胡亂試一下查刻,給它加個實參(表達式):

    function fun3(){
        console.log("fun3");
    }(1);

不會報錯,但不會輸出結(jié)果fun3凤类。
分析: 以上代碼相當(dāng)于在聲明函數(shù)后穗泵,又聲明了一個毫無關(guān)系的表達式。相當(dāng)于如下代碼形式:

    function fun3(){
        console.log("fun3");
    }

(1);

// 若此處執(zhí)行fun3函數(shù)谜疤,可以輸出結(jié)果
fun3(); //"fun3"

三佃延、自執(zhí)行函數(shù)表達式

1. 正兒八經(jīng)的自執(zhí)行函數(shù)

想要解決上面問題,可以采用小括弧將要執(zhí)行的代碼包含滓目摹(方式一)履肃,如下:

// 方式一
    (function fun4(){
        console.log("fun4");
    }()); // "fun4"

分析:因為在JavaScript語言中,()里面不能包含語句(只能是表達式)坐桩,所以解析器在解析到function關(guān)鍵字的時候尺棋,會把它們當(dāng)作function表達式,而不是正常的函數(shù)聲明绵跷。
除了上面直接整個包含住膘螟,也可以只包含住函數(shù)體(方式二),如下:

// 方式二
    (function fun5(){
        console.log("fun5");
    })();// "fun4"

寫法上建議采用方式一(這是參考文的建議碾局。但實際上荆残,我個人覺得方式二比較常見)。

2. “歪瓜裂棗”的自執(zhí)行函數(shù)

除了上面()小括弧可以把function關(guān)鍵字作為函數(shù)聲明的含義轉(zhuǎn)換成函數(shù)表達式外净当,JavaScript的&& 與操作内斯、||或操作、,逗號等操作符也有這個效果蚯瞧。

    true && function () { console.log("true &&") } (); // "true &&"
    false || function () { console.log("true ||") } (); // "true ||"
    0, function () { console.log("0,") } (); // "0,"

// 此處要注意: &&, || 的短路效應(yīng)嘿期。即: false && (表達式1)  是不會觸發(fā)表達式1;
// 同理埋合,true || (表達式2) 不會觸發(fā)表達式2

如果不在意返回值备徐,也不在意代碼的可讀性,我們甚至還可以使用一元操作符(! ~ - + )甚颂,函數(shù)同樣也會立即執(zhí)行蜜猾。

    !function () { console.log("!"); } (); //"!"
    ~function () { console.log("~"); } (); //"~"
    -function () { console.log("-"); } (); //"-"
    +function () { console.log("+"); } (); //"+"

甚至還可以使用new關(guān)鍵字:

// 注意:采用new方式秀菱,可以不要再解釋花括弧 `}` 后面加小括弧 `()` 
new function () { console.log("new"); } //"new"

// 如果需要傳遞參數(shù)
new function (a) { console.log(a); } ("newwwwwwww"); //"newwwwwwww"

嗯,最好玩的是賦值符號=同樣也有此效用(例子中的i變量方式):

//此處 要注意區(qū)分 i 和 j 不同之處蹭睡。前者是函數(shù)自執(zhí)行后返回值給 i 衍菱;后者是聲明一個函數(shù),函數(shù)名為 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;}

上面提及到脊串,要注意區(qū)分 var ivar j 不同之處(前者是函數(shù)自執(zhí)行后返回值給i ;后者是聲明一個函數(shù)清钥,函數(shù)名為j)琼锋。如果是看代碼,我們需要查看代碼結(jié)尾是否有沒有()才能區(qū)分祟昭。一般為了方便開發(fā)人員閱讀缕坎,我們會采用下面這種方式:

    var i2 = (function () { console.log("output i2:"); return 10; } ()); // "output i2:"
    var i3 = (function () { console.log("output i3:"); return 10; }) (); // "output i3:"
// 以上兩種都可以,但依舊建議采用第一種 i2 的方式篡悟。(個人依舊喜歡第二種i3方式)

四谜叹、自執(zhí)行函數(shù)的應(yīng)用

1. for循環(huán) + setTimeout 例子

直接來看一個例子。for 循環(huán)里面通過延時器輸出索引 i

for( var i=0;i<3;i++){
    setTimeout(function(){
        console.log(i);
    }
    ,300);
}
// 輸出結(jié)果 3,3,3

輸出結(jié)果并不是我們所預(yù)想的1,2,3搬葬。當(dāng)然荷腊,這個要涉及到setTimeout 的原理了,即使把300ms改成0ms踩萎,同樣也會輸出3,3,3停局。具體可以查看博文 setTimeout(0) 的作用 。這里摘取其中一段說明香府。

JavaScript是單線程執(zhí)行的董栽,無法同時執(zhí)行多段代碼。當(dāng)某段代碼正在執(zhí)行時企孩,后續(xù)任務(wù)都必須等待锭碳,形成一個隊列。只有當(dāng)前任務(wù)執(zhí)行完畢勿璃,才會從隊列中取出下一個任務(wù)——也就是常說的“阻塞式執(zhí)行”擒抛。

上面代碼中設(shè)定了一個setTimeout,那瀏覽器會在合適時間(此處是300ms后)把代碼插入任務(wù)隊列补疑,等待當(dāng)前的for循環(huán)代碼執(zhí)行完畢再執(zhí)行歧沪。(注意:setTimeout 雖然指定了延時的時間,但并不能保證執(zhí)行的時間與設(shè)定的延時時間一直莲组,是否準確取決于 JavaScript 線程是擁擠還是空閑诊胞。)

上面說了那么多,都是在分析為什么會輸出3,3,3锹杈。那怎么樣才能輸出1,2,3呢撵孤?
看看下面的方式(寫法一):把setTimeout代碼包含在匿名自執(zhí)行函數(shù)里面迈着,就可以實現(xiàn)“鎖住”索引i,正常輸出索引值邪码。

for( var i=0;i<3;i++){
    (function(lockedIndex){
        setTimeout(function(){
            console.log(lockedIndex);
        }
        ,300);
    })(i);
}
// 輸出 "1,2,3"

分析:盡管循環(huán)執(zhí)行結(jié)束裕菠,i值已經(jīng)變成了3。但因遇到了自執(zhí)行函數(shù)闭专,當(dāng)時的i值已經(jīng)被 lockedIndex鎖住了奴潘。也可以理解為 自執(zhí)行函數(shù)屬于for循環(huán)一部分,每次遍歷i喻圃,自執(zhí)行函數(shù)也會立即執(zhí)行萤彩。所以盡管有延時器粪滤,但依舊會保留住立即執(zhí)行時的i值斧拍。
上面的分析有點模糊和牽強,也可以從 閉包 角度出發(fā)分析的杖小。但鄙人“閉包”概念模糊肆汹,先遺憾下,以后再補充分析了予权。QAQ

除了上面的寫法昂勉,也可以直接在 setTimeout 第一個參數(shù)做自執(zhí)行(寫法二),如下扫腺。
注意: 寫法二 會比 寫法一 先執(zhí)行岗照。原因不明。

for( var i=0;i<3;i++){
    setTimeout((function(lockedInIndex){
        console.log(lockedInIndex);
    })(i)
    ,300);
}

關(guān)于 自執(zhí)行函數(shù)參數(shù) lockedInIndex 笆环,補充說明以下幾點攒至。
注意:自執(zhí)行函數(shù)在 setTimeout 和在 setTimeout 里在第2、3中情況有區(qū)別(原因不明躁劣,后續(xù)再補)迫吐。

// 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. 自執(zhí)行函數(shù)不帶入?yún)?shù) 
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. 自執(zhí)行函數(shù)只有實參沒有寫形參
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. 自執(zhí)行函數(shù)只有形參沒有寫實參志膀,這種情況不行。因為會導(dǎo)致輸出 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元素綁定事件

假設(shè)要對頁面上的元素安裝點擊相同的點擊事件溉浙。我們會考慮如下方式。

<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觸發(fā)事件時戳稽,只執(zhí)行最后安裝的那一個。 */
        // eles[k].onclick = function(){
        //  alert("index is: " + k + ", and this ele is: " + eles[k]);
        // }
    }
</script>

我們期望點擊某個 p元素圆裕,能得到該元素所在的索引广鳍,但實際是荆几,點擊每個p,索引值都是5赊时,而對應(yīng)的元素都是undefined吨铸。
分析:這種現(xiàn)象和上面的延時器類似,JavaScript在執(zhí)行for循環(huán)語句時祖秒,負責(zé)給元素安裝點擊事件诞吱,但當(dāng)用戶點擊元素觸發(fā)事件時,for循環(huán)語句早就執(zhí)行完畢了竭缝,此時的 i 自然是5了房维。

一樣的,我們也希望“鎖住”索引i抬纸。所以可以如上采用自執(zhí)行函數(shù)方式( 在addEventListener外部 ):

/** 1. 自執(zhí)行函數(shù)方式一 */
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里面 的處理函數(shù)使用自執(zhí)行函數(shù)表達式咙俩,具體如下。不過上面的方式更具有可讀性湿故。

        /** 2. 自執(zhí)行函數(shù)方式二 */
    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));
    }

當(dāng)然阿趁,除了自執(zhí)行函數(shù)表達式,我們還有一種討巧的解決辦法:

    /** 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 保存在元素的屬性中坛猪。在點擊元素觸發(fā)事件時脖阵,巧用 this 關(guān)鍵字去取出當(dāng)前點擊對象的屬性 index,也就是對應(yīng)的索引墅茉。

四命黔、自執(zhí)行與立即執(zhí)行

最后來嘮嗑下命名方式。
文中對 (function () {/*code*/} ) () 這種表達式就斤,稱作為 自執(zhí)行匿名函數(shù)(Self-executing anonymous function)悍募;而參考的英文博文中作者更建議稱它為 立即調(diào)用的函數(shù)表達式(Immediately-Invoked Function Expression)。
以下是截取該參考博文的例子:

// 自執(zhí)行函數(shù)战转。自己調(diào)用自己(遞歸)
function foo() { foo(); }

// 自執(zhí)行的匿名函數(shù)搜立。
var foo = function () { arguments.callee(); }; 

// 立即執(zhí)行匿名函數(shù)。但我們習(xí)慣稱其為:自執(zhí)行的匿名函數(shù)槐秧。
(function () { /* code */ } ());

// 立即執(zhí)行函數(shù)啄踊。加一個標示名稱,可以方便Debug
(function foo() { /* code */ } ());

// 立即調(diào)用的函數(shù)表達式(IIFE)也可以自執(zhí)行刁标,不過可能不常用罷了
(function () { arguments.callee(); } ());
(function foo() { foo(); } ());

注意:arguments.callee在ECMAScript 5 strict mode里被廢棄了颠通。

個人愚見:上面例子中把 自執(zhí)行 解釋成 “自己調(diào)用自己”,當(dāng)然和 立即執(zhí)行 相差很大了膀懈。但如果把 自執(zhí)行 解釋成 “自動執(zhí)行”顿锰,就和 立即執(zhí)行 異曲同工了。
命名方式絕對統(tǒng)一也沒必要,重要的是能深入了解并應(yīng)用它們硼控。

參考內(nèi)容:

  1. 深入理解JavaScript系列(4):立即調(diào)用的函數(shù)表達式
  2. Immediately-Invoked Function Expression (IIFE)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刘陶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子牢撼,更是在濱河造成了極大的恐慌匙隔,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熏版,死亡現(xiàn)場離奇詭異纷责,居然都是意外死亡,警方通過查閱死者的電腦和手機撼短,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門再膳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人曲横,你說我怎么就攤上這事喂柒。” “怎么了胜榔?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵胳喷,是天一觀的道長。 經(jīng)常有香客問我夭织,道長,這世上最難降的妖魔是什么吠撮? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任尊惰,我火速辦了婚禮,結(jié)果婚禮上泥兰,老公的妹妹穿的比我還像新娘弄屡。我一直安慰自己,他們只是感情好鞋诗,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布膀捷。 她就那樣靜靜地躺著,像睡著了一般削彬。 火紅的嫁衣襯著肌膚如雪全庸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天融痛,我揣著相機與錄音壶笼,去河邊找鬼。 笑死雁刷,一個胖子當(dāng)著我的面吹牛覆劈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼责语,長吁一口氣:“原來是場噩夢啊……” “哼炮障!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起坤候,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤铝阐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后铐拐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體徘键,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年遍蟋,在試婚紗的時候發(fā)現(xiàn)自己被綠了吹害。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡虚青,死狀恐怖它呀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情棒厘,我是刑警寧澤纵穿,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站奢人,受9級特大地震影響谓媒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜何乎,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一句惯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧支救,春花似錦抢野、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贬堵,卻和暖如春恃轩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背扁瓢。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工详恼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人引几。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓昧互,卻偏偏與公主長得像挽铁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子敞掘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容