在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 i
和 var 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)容: