本文是深入理解JavaScript系列的第四篇讀文筆記,博客原文在這里。
本文闡述的內(nèi)容是JavaScript中經(jīng)常遇到的兩個知識點:自執(zhí)行函數(shù)和函數(shù)閉包。如果你之前稍微接觸過過JavaScript,你應(yīng)該能夠明白我所指的意思,這里我就不像大叔原文中那么較真這個行為的具體叫法了。
在JavaScript的世界中,如果你能夠?qū)ψ詧?zhí)行函數(shù)和函數(shù)閉包了若指掌,在實際編碼中能夠信手拈來,那么,一般來說你JavaScript的功力至少有中級以上了,呵呵,這可能還是一種保守的估計。
如果你有讀過流行JavaScript類庫源碼的話,你可能會發(fā)現(xiàn),源碼的作者對自執(zhí)行函數(shù)和函數(shù)閉包的使用是比較頻繁的,再結(jié)合一些具體的業(yè)務(wù)場景,往往會得到一些非常美妙的設(shè)計。如果你去悉心品讀,可能會發(fā)現(xiàn)高手寫出來的JavaScript代碼和新手寫出的JavaScript代碼完全是天壤之別。
大叔的原文中只針對自執(zhí)行函數(shù)作了比較透徹的說明,而對函數(shù)閉包僅僅用了一個示例就一筆帶過。這篇讀文筆記中,我將會針對這兩點分別作一些詳細說明,盡量用簡明的話將我理解中的這兩個概念闡述清楚。
首先,什么叫自執(zhí)行?在JavaScript中函數(shù)在執(zhí)行的時候會創(chuàng)建一個叫做執(zhí)行上下文的東西,這里問題又來了,這個執(zhí)行上下文又是什么東西呢?
簡單的說,執(zhí)行上下文就是JavaScript代碼在執(zhí)行時創(chuàng)建的一個容器,這個容器中可以隨時創(chuàng)建只屬于這一塊代碼的變量,函數(shù)聲明等等。
其實,執(zhí)行上下文是ECMA-262規(guī)定的一個非常抽象的概念,我們這里只要對這個概念有個把握就可以了,更多的解釋我就不多作筆墨了,如有興趣,可查閱相關(guān)文檔。
上面說到,執(zhí)行上下文其實代碼執(zhí)行時才會生成的一個東西,如果我就簡單的寫一段JavaScript代碼放在這里,我并沒有在瀏覽器中引入這個JavaScript片段執(zhí)行它,那么它就不會有執(zhí)行上下文了。所以,自執(zhí)行的含義,簡單來說,一段JavaScript代碼中自己執(zhí)行了。這里的一段JavaScript代碼一般都是指一個JavaScript函數(shù),所以這里的自執(zhí)行就是指函數(shù)調(diào)用。
我們來看個例子,
function makeCounter() {
// 只能在makeCounter內(nèi)部訪問i
var i = 0;
return function () {
console.log(++i);
};
}
// 注意,counter和counter2是不同的實例,分別有自己范圍內(nèi)的i。
var counter = makeCounter();
counter(); // logs: 1
counter(); // logs: 2
var counter2 = makeCounter();
counter2(); // logs: 1
counter2(); // logs: 2
alert(i); // 引用錯誤:i沒有defind(因為i是存在于makeCounter內(nèi)部)。
這里,我們每次調(diào)用函數(shù)makeCounter()
時,其實都會生成一個獨立的執(zhí)行上下文。具體來看,makeCounter
生成的執(zhí)行上下文中包含了一個變量i
以及一個匿名函數(shù)。
這里需要特別提出的一點是,每個獨立的執(zhí)行上下文,其中的變量都是相互獨立的,即counter
和counter2
其實是不同的實例。
當(dāng)你聲明類似這樣的函數(shù),
function foo() {
// function body
}
var foo2 = function() {
// function body
}
我們可以簡單在函數(shù)名foo
(或者變量名foo2
)的后面加上()
即可實現(xiàn)自執(zhí)行。如下,
foo();
foo2();
那是不是意為著我只要在函數(shù)的后面加上一對()
就可以達到自執(zhí)行的目的呢?我們看下面的代碼,
function() {
return 'test';
}();
function foo() {
return 'test2';
}();
遺憾的是,這兩種方式,不管是在匿名函數(shù)后加()
還是在普通的函數(shù)聲明后加()
都達不到讓函數(shù)自執(zhí)行的目的。這兩種情況下,你都會得到一個報錯。
上面提到的兩種錯誤方式,其實出錯的原理還不太一樣,
function
關(guān)鍵字時,默認其是函數(shù)聲明,函數(shù)聲明要求必須有一個函數(shù)名。()
,這個()
其實是一個分組操作符,這里報錯的原因是因為分組操作符需要一個表達式語句而不是一個聲明語句。經(jīng)過上面的說明,我們知道,不管是匿名函數(shù)(雖然這個匿名的聲明也有問題)還是函數(shù)foo
其實都只是函數(shù)聲明,而這里的()
是一個運算符,它要求前面的東西必須為(函數(shù))表達式!
所以,我們只需要將()
前面的內(nèi)容變成函數(shù)表達式就行了。我們看下面的代碼,
// 下面2個括弧()都會立即執(zhí)行
(function(){ /* code */ }());
(function(){ /* code */ })();
// 由于括弧()和JS的&&,異或,逗號等操作符是在函數(shù)表達式和函數(shù)聲明上消除歧義的
// 所以一旦解析器知道其中一個已經(jīng)是表達式了,其它的也都默認為表達式了
var i = function(){ /* code */ }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
// 如果你不在意返回值,或者不怕難以閱讀
// 你甚至可以在function前面加一元操作符號
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
// 上面這種使用一元表達式這種方式其實是不太常見的
// 而且有時候肯定在一些場景下存在一些弊端,因為一元表達式會有一個不為undefined的返回值
// 要想返回值為undefined,那么最保險的就是使用void關(guān)鍵字
void function(){/* code */}();
一般常用的兩種形式就是(function(){}());
和(function(){})();
,大叔的原文中說第一種是推薦的寫法,但是不知道為什么現(xiàn)在很多人都是用的第二種~~
原文中還提到了這個話題。額,其實是英文原文的作者提到的。其實在我看來,自執(zhí)行匿名函數(shù)和立即執(zhí)行函數(shù)表達式的區(qū)別基本上可以忽略,在實際的使用其實都是一回事,只不過兩種形式的函數(shù)主體不太一致。如下代碼,
(function() {
return '我是自執(zhí)行匿名函數(shù)';
})();
(function foo() {
foo();
})();
好吧,我承認第二種其實是不太常見的。
想想前篇文章說的Module模式,我們常常使用Module模式配合自執(zhí)行函數(shù)來封裝一個工具。
下面是一個例子,
// 創(chuàng)建一個立即調(diào)用的匿名函數(shù)表達式
// return一個變量,其中這個變量里包含你要暴露的東西
// 返回的這個變量將賦值給counter,而不是外面聲明的function自身
var counter = (function () {
var i = 0;
return {
get: function () {
return i;
},
set: function (val) {
i = val;
},
increment: function () {
return ++i;
}
};
} ());
// counter是一個帶有多個屬性的對象,上面的代碼對于屬性的體現(xiàn)其實是方法
counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5
counter.i; // undefined 因為i不是返回對象的屬性
i; // 引用錯誤: i 沒有定義(因為i只存在于閉包)
當(dāng)然這里還用到了閉包的概念。我自己就經(jīng)常使用這種技巧來封裝一些配置類或者工具類的東西。封裝后,只要暴露一個對象就可以了,從而達到了對內(nèi)部變量的隱藏。
什么叫(函數(shù))閉包呢?各種專業(yè)文獻上對這個詞的解釋比較抽象,不是太好理解。我個人對閉包的理解就是:閉包就是一個帶有了父作用域相關(guān)變量的函數(shù)。或者更加通俗一點就是:閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。
我想先談?wù)凧avaScript中為什么會有閉包這個東西。
我們知道在JavaScript中,函數(shù)第一等公民,函數(shù)的用途非常廣泛,函數(shù)可以參數(shù)傳入另一個函數(shù),還可以返回值從一個函數(shù)中返回。我們看下面的代碼,
function fn1() {
var a = 1;
return function fn2() {
return 1 + a;
};
}
var foo = fn1(); // typeof foo === 'function'
foo(); // 2
這里foo = fn1()
后,foo
其實是一個函數(shù)引用,通俗點說,foo
就是一個函數(shù)表達式。那么這個foo
在執(zhí)行的時候,它需要訪問變量a
,但是這個a
并沒有在fn2
中定義,它是定義在fn1
中的。所以foo
(也就是fn2
)在執(zhí)行的過程中,會向其父作用域(即fn1
所在的作用域)查找變量a
。此時,fn2
中就保持了一個對父作用域的引用。
類似這樣的場景就是我們所說的(函數(shù))閉包。其實閉包從某種意義上來說,就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁。
閉包最大的作用有兩個,
我們來看下面的一個例子,
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); // The Window
作一點改動后,
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var self = this;
return function(){
return self.name;
};
}
};
alert(object.getNameFunc()()); // My Object
我們來稍微分析一下。第一種情況中,
object.getNameFunc()
的執(zhí)行結(jié)果是其實是一個函數(shù)引用。而且這個getNameFunc
函數(shù)在執(zhí)行時,其內(nèi)部的this
指針是指向object
的。接下來,object.getNameFunc()()
其實等價于,
var name = "The Window";
(function() {
return this.name;
})();
這個代碼片段在執(zhí)行的時候,會檢索this
的值。這里,它最終檢索的結(jié)果就是全局對象window
,然后返回的結(jié)果就是name = 'The Window'
。
而第二種情況中,我們使用變量self
暫存了匿名函數(shù)(其實就是getNameFunc
函數(shù)表達式)的this
指針,而這個this
指針在運行時的指向正是object
。函數(shù)getNameFunc
返回的匿名函數(shù)毫無疑問,它是一個閉包,而且它保持了對父作用域變量self
的持續(xù)引用。
更多內(nèi)容,推薦閱讀阮一峰的學(xué)習(xí)Javascript閉包(Closure)。
在使用閉包的時候,有一個常見的誤區(qū),我們看下面的代碼,
for (var i = 0; i < 10; i++) {
setTimeout(function(){
console.log(i);
}, 1000);
}
這段代碼的運行結(jié)果將會連續(xù)打印10個10。
你可能會問:???怎么會這樣?不是說好的打印從0到9的序列么?
我們來稍微分析一下。
for
循環(huán)中連續(xù)創(chuàng)建了10個延時函數(shù),每個延時函數(shù)的函數(shù)體是打印迭代變量i
。這里我們先忽略10個延時函數(shù)由于創(chuàng)建先后順序以及CPU時間片造成誤差。當(dāng)10次循環(huán)結(jié)束后,肯定還是沒有經(jīng)過1000ms,不過此時由于迭代的結(jié)果,迭代變量i
已經(jīng)變成10了。接下里延時計時器結(jié)束,開始執(zhí)行延時函數(shù),函數(shù)中需要訪問變量i
,不幸的是,此時的i
已經(jīng)變成10了,所以打印出來的10個數(shù)據(jù)都是10。
那我們?nèi)绾涡薷哪軌蜻_到我們本來的目的呢?即按照迭代變量的順序,依次打印出0-9呢?
for (var i = 0; i < 10; i++) {
setTimeout((function(index){
return function() {
console.log(index);
};
})(i), 1000);
}
代碼中應(yīng)該看的很清楚了,延時函數(shù)中使用了一個閉包,這個閉包保持了對父作用域中參考變量index
的持續(xù)引用,而這個index
是隨著每次for循環(huán)實時傳遞進來的迭代變量。所以它將會打印出0-9。
這篇對自執(zhí)行函數(shù)(或者叫立即調(diào)用匿名函數(shù)表達式)以及函數(shù)閉包作了細致的闡述,基本上涵蓋了這兩個知識點所有的方方面面,更多的內(nèi)容就是需要在實際編碼中進行實戰(zhàn)了。
我還是想強調(diào)那句話,只要對JavaScript中的這兩個要點了若指掌,編碼時能夠做到信手拈來,那么假以時日必定能夠成為JavaScript高手。
更多建議: