在使用JavaScript的時候經常會看見類似如下的函數(shù)調用方式:
(function(){
console.log("test");
})();
或者
(function(){
console.log("test");
}());
一些流行的庫也是這樣蠢挡,比如jQuery
(function( window, undefined ) {
// code here
}) ( window );
社區(qū)對此種用法的稱呼不盡相同,其中包括「自執(zhí)行匿名函數(shù)」(self-executing anonymous function),「立即執(zhí)行函數(shù)表達式」(Immediately-Invoked Function Expression,以下簡稱IIFE),筆者傾向于第二種叫法。
普通的函數(shù)聲明與調用方式有以下幾種:
// 聲明函數(shù)f1
function f1() {
console.log("f1");
}
// 通過()來調用此函數(shù)
f1();
// 或者
// 建立匿名函數(shù)并賦予變量f2
var f2 = function() {
console.log("f2");
}
// 通過()來調用此函數(shù)
f2();
以上都是以顯示的方式聲明函數(shù)岩调,然后再進行通過()
來進行調用炒俱,那么如果想要直接調用匿名函數(shù)呢盐肃?可以做到嗎?試一下:
// 直接在匿名函數(shù)后邊加()(方式A)
function () {console.log("f1");}(); // SyntaxError: Unexpected token (
很不幸权悟,出錯了:SyntaxError: Unexpected token (
砸王。那么試試前言中的方法:
// 在匿名函數(shù)外面套一個(),然后再用()來調用(方式B)
(function(){ console.log("test");})(); // test
// 在方式A的外層套一個()(方式C)
(function(){ console.log("test");}()); // test
以上這兩種方式都是可以正常運行的峦阁,如果僅僅到這里是不夠的谦铃,必需要「知其然,知其所以然」榔昔,讓我們看看為什么A不可以運行驹闰,而B和C是可以運行的。原來撒会,JavaScript解釋器會在默認的情況下把遇到的function
關鍵字當作是函數(shù)聲明語句(statement)來進行解釋的嘹朗,而函數(shù)聲明語句是這樣的:
function name([param] [, param] [..., param]) {
statements
}
A的調用方式明顯是有語法錯誤的,所以才會拋出異常SyntaxError: Unexpected token (诵肛。但是B屹培,C為什么可以正常運行?這是因為在JavaScript中()之間只能包含表達式(expression)怔檩,所以在以B褪秀,C方式運行的時候,解釋器把()中的內容當作表達式(expression)而不是語句(statement)來執(zhí)行薛训。為了能夠更好的解釋B溜歪,C調用方式的原理,在這里插入對()操作符的簡單介紹:
// 如果傳入字面量(literal)许蓖,則返回表達式(expression)
(1) // 1
(function(){console.log("f");}) // function () {console.log("f")}
這里不會對()做更深入的探討蝴猪,如果讀者感興趣调衰,可以自行google。OK自阱,我們先看B的調用方式:
(function(){ console.log("test");})(); // test
由于把函數(shù)的聲明寫在了()之中嚎莉,所以解釋器以表達式(expression)來解析其中代碼,而根據(jù)我們上面的介紹知道如果向()中傳入函數(shù)聲明會直接返回此函數(shù)沛豌,此步執(zhí)行完成之后趋箩,臨時結果用偽代碼來表示的話,應該類似這樣:
anonymousFunction();
這個就是函數(shù)的通用調用方式加派,所以繼續(xù)執(zhí)行叫确。對于C的調用方式就更加容易理解了,直接把()中的內容當作表達式來進行解釋芍锦。
所以根據(jù)上面的解釋竹勉,我們知道只要能讓JavaScript解釋器以「函數(shù)表達式」而不是「函數(shù)聲明」來處理匿名函數(shù)的立即執(zhí)行就可以了,把語句放在()之中只是其中的一種方法而已娄琉,根據(jù)這個思路我們可以用其他方式來實現(xiàn)同樣的目的次乓,比如:
// 如果本身就是expression,那么根本不需要做任何處理
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
// 如果你不在乎返回值孽水,可以這么做
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
// 還有更奇葩的方式票腰,但是不知道性能如何,來自
// http://twitter.com/kuvos/status/18209252090847232
new function(){ /* code */ }
new function(){ /* code */ }()
為什么要用立即執(zhí)行函數(shù)表達式
模擬塊作用域
眾所周知女气,JavaScript沒有C或Java中的塊作用域(block)杏慰,只有函數(shù)作用域,在同時調用多個庫的情況下炼鞠,很容易造成對象或者變量的覆蓋缘滥,比如:
liba.js
var num = 1;
// code....
libb.js
var num = 2;
// code....
如果在頁面中同時引用liba.js和liba.js兩個庫,必然導致num變量被覆蓋簇搅,為了解決這個問題完域,可以通過IIFE來解決:
liba.js
(function(){
var num = 1;
// code....
})();
libb.js
(function(){
var num = 2;
// code....
})();
經過改造之后软吐,兩個庫的代碼就完全獨立瘩将,并不會互相影響。
解決閉包沖突
閉包(closure)是JavaScript的一個語言特性凹耙,簡單來說就是在函數(shù)內部所定義的函數(shù)可以持有外層函數(shù)的執(zhí)行環(huán)境姿现,即使在外層函數(shù)已經執(zhí)行完畢的情況下,在這里就不詳細介紹了肖抱,感興趣的可以自行Google备典。我們這里只舉一個由閉包引起的最常見的問題:
var f1 = function() {
var res = [];
var fun = null;
for(var i = 0; i < 10; i++) {
fun = function() { console.log(i);};//產生閉包
res.push(fun);
}
return res;
}
// 會輸出10個10,而不是預期的0 1 2 3 4 5 6 7 8 9
var res = f1();
for(var i = 0; i < res.length; i++) {
res[i]();
}
修改成:
var f1 = function() {
var res = [];
for(var i = 0; i < 10; i++) {
// 添加一個IIFE
(function(index) {
fun = function() {console.log(index);};
res.push(fun);
})(i);
}
return res;
}
// 輸出結果為0 1 2 3 4 5 6 7 8 9
var res = f1();
for(var i = 0; i < res.length; i++) {
res[i]();
}
模擬單例
在JavaScript的OOP中意述,我們可以通過IIFE來實現(xiàn)提佣,如下:
var counter = (function(){
var i = 0;
return {
get: function(){
return i;
},
set: function( val ){
i = val;
},
increment: function() {
return ++i;
}
};
}());
counter.get(); // 0
counter.set( 3 );
counter.increment(); // 4
counter.increment(); // 5