
閉包不是什么魔法
本篇文章介紹了閉包,方便程序員們能夠進(jìn)一步理解javascript代碼,本文適合有一定編程經(jīng)驗(yàn)的程序員,比如可以看懂如下代碼:大神請(qǐng)繞道。
Example 1
function sayHello(name) {
var text = 'Hello ' + name;
var say = function() { console.log(text); }
say();
}
sayHello('Joe'); //Hello Joe
一旦深刻理解了核心概念,閉包就并不難分析和運(yùn)用了。
一個(gè)關(guān)于閉包的案例
兩句話總結(jié):
- 第一級(jí)函數(shù)支持閉包。閉包它是一個(gè)表達(dá)式,可以在閉包的范圍內(nèi)引用變量(當(dāng)它被首次聲明),被賦值給變量,作為參數(shù)傳遞給函數(shù),或作為函數(shù)結(jié)果返回。(譯者注:在JavaScript世界中函數(shù)是一等公民,它不僅擁有一切傳統(tǒng)函數(shù)的使用方式(聲明和調(diào)用),還可以做到像原始值一樣賦值、傳參、返回,這樣的函數(shù)也稱之為第一級(jí)函數(shù)(First-class Function))
- 閉包是在函數(shù)開始執(zhí)行時(shí)分配的堆棧幀,并且在函數(shù)返回后不會(huì)釋放(就像“堆棧幀”分配在堆上而不是棧上?。?/li>
Example 2
以下代碼返回一個(gè)函數(shù)的引用:
function sayHello2(name) {
var text = 'Hello ' + name; // Local variable
var say = function() { console.log(text); }
return say;
}
var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"
大多數(shù)JavaScript程序員都了解如何將一個(gè)函數(shù)的引用賦給上述代碼中的變量say2。如果你不了解,那么在學(xué)習(xí)閉包之前你需要先了解一下。一個(gè)使用C的程序員會(huì)將其看作是返回指向某函數(shù)的指針,會(huì)認(rèn)為變量say和say2都是指向函數(shù)的指針。
C語言指向函數(shù)的指針和JavaScript的函數(shù)引用之間存在著很關(guān)鍵的區(qū)別。在JavaScript中,您可以將函數(shù)引用變量看作既包含指向函數(shù)的指針,也包含指向閉包的隱藏指針。
上述代碼中存在一個(gè)閉包,因?yàn)槟涿瘮?shù)function() { console.log(text); }在另一個(gè)函數(shù)sayHello2()中聲明。在這個(gè)例子中,如果你在另一個(gè)函數(shù)體里使用function關(guān)鍵字,那么你正在創(chuàng)建閉包。
在C語言和其他大多數(shù)類似語言中,在函數(shù)返回后,所有局部變量不可再被訪問,因?yàn)槎褩呀?jīng)被銷毀了。
而在Javascript語言中,如果你在一個(gè)函數(shù)體內(nèi)再聲明一個(gè)函數(shù),這個(gè)函數(shù)被返回到了全局,局部變量依然可以被訪問。如上面所示,我們?cè)诤瘮?shù)sayHello2()返回后調(diào)用了函數(shù)say2(),請(qǐng)注意,變量text是函數(shù)sayHello2()的局部變量。
function() { console.log(text); } // Output of say2.toString();
注意say2.toString()的輸出,我們可以看到這段代碼引用了變量text,由于sayHello2()的局部變量被保存到閉包內(nèi),所以這個(gè)匿名函數(shù)可以引用存儲(chǔ)"Hello Bob"的變量text。
在Javascript中函數(shù)引用包含指向它所創(chuàng)建的閉包的隱藏指針就類似于js中的事件委托(一個(gè)事情本需要自己做,但自己委托給別人做了)。
更多案例
出于某種原因,閉包似乎很難理解,但是當(dāng)你多看一些案例之后,它的工作原理變得逐漸清晰(我花了很長(zhǎng)時(shí)間才搞清楚)。我建議你仔細(xì)研究這些案例,直至弄明白閉包是如何工作的。如果你在沒弄明白之前就使用閉包,就一定會(huì)碰到一些非常奇怪的錯(cuò)誤。
Example 3
這個(gè)案例表明,局部變量沒有被復(fù)制,而是它們的引用被保存。就好像當(dāng)外部函數(shù)退出后在內(nèi)存保留一個(gè)堆棧幀。
function say667() {
// Local variable that ends up within closure
var num = 42;
var say = function() { console.log(num); }
num++;
return say;
}
var sayNumber = say667();
sayNumber(); // logs 43
Example 4
所有這三個(gè)全局函數(shù)都有一個(gè)對(duì)同一個(gè)閉包的共同引用,因?yàn)樗鼈兌际窃谕粋€(gè)setupSomeGlobals()函數(shù)中聲明的。
var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
// Local variable that ends up within closure
var num = 42;
// Store some references to functions as global variables
gLogNumber = function() { console.log(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5
var oldLog = gLogNumber;
setupSomeGlobals();
gLogNumber(); // 42
oldLog() // 5
這三個(gè)函數(shù)共用一個(gè)閉包——三個(gè)函數(shù)被定義時(shí),函數(shù)setupSomeGlobals()的局部變量。
請(qǐng)注意,在上述案例中,如果再次調(diào)用setupSomeGlobals(),則會(huì)創(chuàng)建一個(gè)新的閉包(堆棧幀)。舊的gLogNumber, gIncreaseNumber, gSetNumber變量被具有新閉包的新函數(shù)覆蓋(在Javascript中,無論何時(shí)在另一個(gè)函數(shù)內(nèi)聲明一個(gè)函數(shù),每次調(diào)用外部函數(shù)時(shí)都會(huì)重新創(chuàng)建內(nèi)部函數(shù))。
Example 5
這個(gè)案例對(duì)于許多人來說是一個(gè)大難題,你需要仔細(xì)理解一下。如果你要在一個(gè)循環(huán)體中定義一個(gè)函數(shù),要非常小心,閉包中的局部變量可不會(huì)想你想當(dāng)然那樣工作。
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
// Using j only to help prevent confusion -- could use i.
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //logs "item2 undefined" 3 times
這行代碼result.push( function() {console.log(item + ' ' + list[i])} );所示,將一個(gè)匿名函數(shù)的引用添加到result數(shù)組中三次。如果你對(duì)匿名函數(shù)不熟悉,也可當(dāng)成如下:
pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);
請(qǐng)注意,當(dāng)案例執(zhí)行時(shí),"item2 undefined"會(huì)輸出三次!這是因?yàn)楦鞍咐粯樱?code>buildList的局部變量只有一個(gè)閉包。當(dāng)在執(zhí)行fnList[j]()調(diào)用匿名函數(shù)時(shí),三個(gè)匿名函數(shù)都共用一個(gè)閉包,并且它們使用的是循環(huán)結(jié)束后的當(dāng)前值作為該閉包中的i和item(循環(huán)已完成,i的值為3,item值為"item2")。請(qǐng)注意,該循環(huán)從0開始索引,到循環(huán)結(jié)束前item值為"item2",而i ++會(huì)將i值增加到3。
Example 6
此案例顯示:在外部函數(shù)退出前,外部函數(shù)內(nèi)聲明的所有全局變量都包含在閉包內(nèi)。請(qǐng)注意,變量alice實(shí)際上是在匿名函數(shù)之后聲明的,匿名函數(shù)是最先聲明的,當(dāng)該函數(shù)被調(diào)用時(shí),它仍然可以訪問alice變量,因?yàn)樵撟兞刻幱谙嗤饔糜騼?nèi)(Javascript聲明提升)。另外,sayAlice()()只是直接調(diào)用從sayAlice()返回的函數(shù)引用。
function sayAlice() {
var say = function() { console.log(alice); }
// Local variable that ends up within closure
var alice = 'Hello Alice';
return say;
}
sayAlice()();// logs "Hello Alice"
需要注意的是:say變量也在閉包中,可以通過sayAlice()中任何可能聲明的其他函數(shù)訪問,或者可以在內(nèi)部函數(shù)內(nèi)遞歸訪問。
Example 7
最后這個(gè)案例表明,每次調(diào)用外部函數(shù)都會(huì)為局部變量創(chuàng)建一個(gè)單獨(dú)的閉包。不是每個(gè)函數(shù)聲明都有單獨(dú)閉包,而是每次函數(shù)調(diào)用都會(huì)創(chuàng)建一個(gè)閉包。
function newClosure(someNum, someRef) {
// Local variables that end up within closure
var num = someNum;
var anArray = [1,2,3];
var ref = someRef;
return function(x) {
num += x;
anArray.push(num);
console.log('num: ' + num +
'; anArray: ' + anArray.toString() +
'; ref.someVar: ' + ref.someVar + ';');
}
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
總結(jié)
如果對(duì)閉包并不完全明白,那么最好的辦法就是回過頭研究研究這些案例。我對(duì)閉包和堆棧幀等概念的解釋可能在專業(yè)上并不完全正規(guī),這都是為了幫助大家更好地理解。一旦這些基礎(chǔ)知識(shí)得到掌握,你可以在以后的日子里去摳那些更專業(yè)的細(xì)節(jié)。
最后幾點(diǎn)
- 任何時(shí)候,你在一個(gè)函數(shù)體內(nèi)用了另外一個(gè)函數(shù),閉包就產(chǎn)生了。
- 任何時(shí)候,你在一個(gè)函數(shù)體內(nèi)用了
eval(),閉包就產(chǎn)生了。在eval中的內(nèi)容可以引用函數(shù)里的局部變量,你甚至可以在eval內(nèi)聲明新的局部變量,比如:eval('var foo = ...')。 - 當(dāng)你在一個(gè)函數(shù)體內(nèi)使用構(gòu)造函數(shù)(
new Function(...)),不會(huì)產(chǎn)生閉包(這個(gè)構(gòu)造函數(shù)不能引用外部函數(shù)的局部變量)。 - Javascript中的閉包就像外部函數(shù)返回后,用來保存所有局部變量的存儲(chǔ)副本一樣。
- 最好可以這樣認(rèn)為:閉包只是一個(gè)函數(shù)的入口,函數(shù)的局部變量被添加到這個(gè)閉包中。
- 每次調(diào)用一個(gè)帶有閉包的函數(shù)時(shí),都會(huì)保存一組新的局部變量(假定該函數(shù)內(nèi)包含一個(gè)函數(shù)聲明,并且返回到外部,或者以某種方式為其保留外部引用)。
- 兩個(gè)函數(shù)可能看起來代碼相同,但是由于“隱藏”的閉包,它們有著完全不同的行為。我并不認(rèn)為通過Javascript代碼可以很容易看出一個(gè)函數(shù)引用是否擁有閉包。
- 如果你想進(jìn)行動(dòng)態(tài)修改代碼(比如:
myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));),如果myFunction是閉包,將行不通(當(dāng)然,你應(yīng)該永遠(yuǎn)不會(huì)想要這樣進(jìn)行源代碼字符串替換,但是……)。 - 很可能出現(xiàn)這種情況:在函數(shù)體內(nèi)的函數(shù)聲明中還有函數(shù)聲明,那么你會(huì)發(fā)現(xiàn)有不止一個(gè)層級(jí)上的閉包出現(xiàn)。
- 我懷疑Javascript中的閉包和那些函數(shù)式語言的閉包不同。
閉包的應(yīng)用(譯者注)
- 實(shí)現(xiàn)封裝,私有化屬性/變量
- 模塊化開發(fā),防止全局污染
- 用作緩存
- 用作公有變量
- 等等……
閉包的危害(譯者注)
閉包會(huì)導(dǎo)致原有作用域鏈不釋放,造成內(nèi)存泄露。
感謝
如果你剛剛學(xué)會(huì)了閉包(在這篇文章或者其他地方),歡迎提出任何意見或建議,因?yàn)槟愕姆答伩赡軙?huì)使這篇文章更加清晰完善,為更多有需要的人帶來方便。我不是Javascript專家也不是閉包專家,歡迎批評(píng)指正。