閉包不是什么魔法
本篇文章介紹了閉包屯烦,方便程序員們能夠進(jìn)一步理解javascript代碼迄埃,本文適合有一定編程經(jīng)驗的程序員削饵,比如可以看懂如下代碼:大神請繞道生棍。
Example 1
function sayHello(name) {
var text = 'Hello ' + name;
var say = function() { console.log(text); }
say();
}
sayHello('Joe'); //Hello Joe
一旦深刻理解了核心概念尝苇,閉包就并不難分析和運用了雳旅。
一個關(guān)于閉包的案例
兩句話總結(jié):
- 第一級函數(shù)支持閉包。閉包它是一個表達(dá)式杆怕,可以在閉包的范圍內(nèi)引用變量(當(dāng)它被首次聲明)族购,被賦值給變量,作為參數(shù)傳遞給函數(shù)陵珍,或作為函數(shù)結(jié)果返回寝杖。(譯者注:在JavaScript世界中函數(shù)是一等公民,它不僅擁有一切傳統(tǒng)函數(shù)的使用方式(聲明和調(diào)用)互纯,還可以做到像原始值一樣賦值瑟幕、傳參、返回留潦,這樣的函數(shù)也稱之為第一級函數(shù)(First-class Function))
- 閉包是在函數(shù)開始執(zhí)行時分配的堆棧幀只盹,并且在函數(shù)返回后不會釋放(就像“堆棧幀”分配在堆上而不是棧上!)兔院。
Example 2
以下代碼返回一個函數(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程序員都了解如何將一個函數(shù)的引用賦給上述代碼中的變量say2
殖卑。如果你不了解,那么在學(xué)習(xí)閉包之前你需要先了解一下坊萝。一個使用C的程序員會將其看作是返回指向某函數(shù)的指針孵稽,會認(rèn)為變量say
和say2
都是指向函數(shù)的指針。
C語言指向函數(shù)的指針和JavaScript的函數(shù)引用之間存在著很關(guān)鍵的區(qū)別十偶。在JavaScript中菩鲜,您可以將函數(shù)引用變量看作既包含指向函數(shù)的指針,也包含指向閉包的隱藏指針惦积。
上述代碼中存在一個閉包接校,因為匿名函數(shù)function() { console.log(text); }
在另一個函數(shù)sayHello2()中聲明。在這個例子中狮崩,如果你在另一個函數(shù)體里使用function
關(guān)鍵字蛛勉,那么你正在創(chuàng)建閉包。
在C語言和其他大多數(shù)類似語言中厉亏,在函數(shù)返回后董习,所有局部變量不可再被訪問烈和,因為堆棧幀已經(jīng)被銷毀了爱只。
而在Javascript語言中,如果你在一個函數(shù)體內(nèi)再聲明一個函數(shù)招刹,這個函數(shù)被返回到了全局恬试,局部變量依然可以被訪問窝趣。如上面所示,我們在函數(shù)sayHello2()
返回后調(diào)用了函數(shù)say2()
训柴,請注意哑舒,變量text
是函數(shù)sayHello2()
的局部變量。
function() { console.log(text); } // Output of say2.toString();
注意say2.toString()
的輸出幻馁,我們可以看到這段代碼引用了變量text
洗鸵,由于sayHello2()
的局部變量被保存到閉包內(nèi),所以這個匿名函數(shù)可以引用存儲"Hello Bob"
的變量text
仗嗦。
在Javascript中函數(shù)引用包含指向它所創(chuàng)建的閉包的隱藏指針就類似于js中的事件委托(一個事情本需要自己做膘滨,但自己委托給別人做了)。
更多案例
出于某種原因稀拐,閉包似乎很難理解火邓,但是當(dāng)你多看一些案例之后,它的工作原理變得逐漸清晰(我花了很長時間才搞清楚)德撬。我建議你仔細(xì)研究這些案例铲咨,直至弄明白閉包是如何工作的。如果你在沒弄明白之前就使用閉包蜓洪,就一定會碰到一些非常奇怪的錯誤纤勒。
Example 3
這個案例表明,局部變量沒有被復(fù)制隆檀,而是它們的引用被保存踊东。就好像當(dāng)外部函數(shù)退出后在內(nèi)存保留一個堆棧幀。
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
所有這三個全局函數(shù)都有一個對同一個閉包的共同引用刚操,因為它們都是在同一個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
這三個函數(shù)共用一個閉包——三個函數(shù)被定義時,函數(shù)setupSomeGlobals()
的局部變量菊霜。
請注意坚冀,在上述案例中,如果再次調(diào)用setupSomeGlobals()
鉴逞,則會創(chuàng)建一個新的閉包(堆棧幀)记某。舊的gLogNumber
, gIncreaseNumber
, gSetNumber
變量被具有新閉包的新函數(shù)覆蓋(在Javascript中,無論何時在另一個函數(shù)內(nèi)聲明一個函數(shù)构捡,每次調(diào)用外部函數(shù)時都會重新創(chuàng)建內(nèi)部函數(shù))液南。
Example 5
這個案例對于許多人來說是一個大難題,你需要仔細(xì)理解一下勾徽。如果你要在一個循環(huán)體中定義一個函數(shù)滑凉,要非常小心,閉包中的局部變量可不會想你想當(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])} );
所示畅姊,將一個匿名函數(shù)的引用添加到result
數(shù)組中三次咒钟。如果你對匿名函數(shù)不熟悉,也可當(dāng)成如下:
pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);
請注意若未,當(dāng)案例執(zhí)行時朱嘴,"item2 undefined"
會輸出三次!這是因為跟之前案例一樣粗合,buildList
的局部變量只有一個閉包萍嬉。當(dāng)在執(zhí)行fnList[j]()
調(diào)用匿名函數(shù)時,三個匿名函數(shù)都共用一個閉包隙疚,并且它們使用的是循環(huán)結(jié)束后的當(dāng)前值作為該閉包中的i
和item
(循環(huán)已完成帚湘,i
的值為3,item
值為"item2")甚淡。請注意大诸,該循環(huán)從0開始索引,到循環(huán)結(jié)束前item
值為"item2"贯卦,而i ++
會將i
值增加到3资柔。
Example 6
此案例顯示:在外部函數(shù)退出前,外部函數(shù)內(nèi)聲明的所有全局變量都包含在閉包內(nèi)撵割。請注意贿堰,變量alice
實際上是在匿名函數(shù)之后聲明的,匿名函數(shù)是最先聲明的啡彬,當(dāng)該函數(shù)被調(diào)用時羹与,它仍然可以訪問alice
變量,因為該變量處于相同作用域內(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
最后這個案例表明峻呕,每次調(diào)用外部函數(shù)都會為局部變量創(chuàng)建一個單獨的閉包利职。不是每個函數(shù)聲明都有單獨閉包,而是每次函數(shù)調(diào)用都會創(chuàng)建一個閉包瘦癌。
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é)
如果對閉包并不完全明白猪贪,那么最好的辦法就是回過頭研究研究這些案例。我對閉包和堆棧幀等概念的解釋可能在專業(yè)上并不完全正規(guī)讯私,這都是為了幫助大家更好地理解热押。一旦這些基礎(chǔ)知識得到掌握西傀,你可以在以后的日子里去摳那些更專業(yè)的細(xì)節(jié)。
最后幾點
- 任何時候楞黄,你在一個函數(shù)體內(nèi)用了另外一個函數(shù),閉包就產(chǎn)生了抡驼。
- 任何時候鬼廓,你在一個函數(shù)體內(nèi)用了
eval()
,閉包就產(chǎn)生了致盟。在eval
中的內(nèi)容可以引用函數(shù)里的局部變量碎税,你甚至可以在eval
內(nèi)聲明新的局部變量,比如:eval('var foo = ...')
馏锡。 - 當(dāng)你在一個函數(shù)體內(nèi)使用構(gòu)造函數(shù)(
new Function(...)
)雷蹂,不會產(chǎn)生閉包(這個構(gòu)造函數(shù)不能引用外部函數(shù)的局部變量)。 - Javascript中的閉包就像外部函數(shù)返回后杯道,用來保存所有局部變量的存儲副本一樣匪煌。
- 最好可以這樣認(rèn)為:閉包只是一個函數(shù)的入口,函數(shù)的局部變量被添加到這個閉包中党巾。
- 每次調(diào)用一個帶有閉包的函數(shù)時萎庭,都會保存一組新的局部變量(假定該函數(shù)內(nèi)包含一個函數(shù)聲明,并且返回到外部齿拂,或者以某種方式為其保留外部引用)驳规。
- 兩個函數(shù)可能看起來代碼相同,但是由于“隱藏”的閉包署海,它們有著完全不同的行為吗购。我并不認(rèn)為通過Javascript代碼可以很容易看出一個函數(shù)引用是否擁有閉包。
- 如果你想進(jìn)行動態(tài)修改代碼(比如:
myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));
)砸狞,如果myFunction
是閉包捻勉,將行不通(當(dāng)然,你應(yīng)該永遠(yuǎn)不會想要這樣進(jìn)行源代碼字符串替換刀森,但是……)贯底。 - 很可能出現(xiàn)這種情況:在函數(shù)體內(nèi)的函數(shù)聲明中還有函數(shù)聲明,那么你會發(fā)現(xiàn)有不止一個層級上的閉包出現(xiàn)撒强。
- 我懷疑Javascript中的閉包和那些函數(shù)式語言的閉包不同禽捆。
閉包的應(yīng)用(譯者注)
- 實現(xiàn)封裝,私有化屬性/變量
- 模塊化開發(fā)飘哨,防止全局污染
- 用作緩存
- 用作公有變量
- 等等……
閉包的危害(譯者注)
閉包會導(dǎo)致原有作用域鏈不釋放胚想,造成內(nèi)存泄露。
感謝
如果你剛剛學(xué)會了閉包(在這篇文章或者其他地方)芽隆,歡迎提出任何意見或建議浊服,因為你的反饋可能會使這篇文章更加清晰完善统屈,為更多有需要的人帶來方便。我不是Javascript專家也不是閉包專家牙躺,歡迎批評指正愁憔。