2016-9-28 藍(lán)藍(lán)設(shè)計(jì)的小編
如果您想訂閱本博客內(nèi)容,每天自動(dòng)發(fā)到您的郵箱中, 請(qǐng)點(diǎn)這里
我研究JavaScript 閉包(closure)已經(jīng)有一段時(shí)間了。我之前只是學(xué)會(huì)了如何使用它們,而沒(méi)有透徹地了解它們具體是如何運(yùn)作的。那么,究竟什么是閉包?
Wikipedia給出的解釋并沒(méi)有太大的幫助。閉包是什么時(shí)候被創(chuàng)建的,什么時(shí)候被銷(xiāo)毀的?具體的實(shí)現(xiàn)又是怎么樣的?
"use strict"; var myClosure = (function outerFunction() { var hidden = 1; return {
inc: function innerFunction() { return hidden++;
}
};
}());
myClosure.inc(); // 返回 1 myClosure.inc(); // 返回 2 myClosure.inc(); // 返回 3 // 相信對(duì)JS熟悉的朋友都能很快理解這段代碼 // 那么在這段代碼運(yùn)行的背后究竟發(fā)生了怎樣的事情呢?
現(xiàn)在,我終于知道了答案,我感到很興奮并且決定向大家解釋這個(gè)答案。至少,我一定是不會(huì)忘記這個(gè)答案的。
Tell me and I forget. Teach me and I remember. Involve me and I learn.
© Benjamin Franklin
并且,在我閱讀與閉包相關(guān)的現(xiàn)存的資料時(shí),我很努力地嘗試著去在腦海中想想每個(gè)事物之間的聯(lián)系:對(duì)象之間是如何引用的,對(duì)象之間的繼承關(guān)系是什么,等等。我找不到關(guān)于這些負(fù)責(zé)關(guān)系的很好的圖表,于是我決定自己畫(huà)一些。
我將假設(shè)讀者對(duì)JavaScript已經(jīng)比較熟悉了,知道什么是全局對(duì)象,知道函數(shù)在JavaScript當(dāng)中是“first-class objects”,等等。
當(dāng)JavaScript在運(yùn)行的時(shí)候,它需要一些空間讓它來(lái)存儲(chǔ)本地變量(local variables)。我們將這些空間稱(chēng)為作用域?qū)ο螅⊿cope object),有時(shí)候也稱(chēng)作LexicalEnvironment
。例如,當(dāng)你調(diào)用函數(shù)時(shí),函數(shù)定義了一些本地變量,這些變量就被存儲(chǔ)在一個(gè)作用域?qū)ο笾?。你可以將作用域函?shù)想象成一個(gè)普通的JavaScript對(duì)象,但是有一個(gè)很大的區(qū)別就是你不能夠直接在JavaScript當(dāng)中直接獲取這個(gè)對(duì)象。你只可以修改這個(gè)對(duì)象的屬性,但是你不能夠獲取這個(gè)對(duì)象的引用。
作用域?qū)ο蟮母拍钍沟肑avaScript和C、C++非常不同。在C、C++中,本地變量被保存在棧(stack)中。在JavaScript中,作用域?qū)ο笫窃诙阎斜粍?chuàng)建的(至少表現(xiàn)出來(lái)的行為是這樣的),所以在函數(shù)返回后它們也還是能夠被訪問(wèn)到而不被銷(xiāo)毀。
正如你做想的,作用域?qū)ο笫强梢杂懈缸饔糜驅(qū)ο螅╬arent scope object)的。當(dāng)代碼試圖訪問(wèn)一個(gè)變量的時(shí)候,解釋器將在當(dāng)前的作用域?qū)ο笾胁檎疫@個(gè)屬性。如果這個(gè)屬性不存在,那么解釋器就會(huì)在父作用域?qū)ο笾胁檎疫@個(gè)屬性。就這樣,一直向父作用域?qū)ο蟛檎遥钡秸业皆搶傩曰蛘咴僖矝](méi)有父作用域?qū)ο?。我們將這個(gè)查找變量的過(guò)程中所經(jīng)過(guò)的作用域?qū)ο蟪俗饔糜蜴湥⊿cope chain)。
在作用域鏈中查找變量的過(guò)程和原型繼承(prototypal inheritance)有著非常相似之處。但是,非常不一樣的地方在于,當(dāng)你在原型鏈(prototype chain)中找不到一個(gè)屬性的時(shí)候,并不會(huì)引發(fā)一個(gè)錯(cuò)誤,而是會(huì)得到undefined
。但是如果你試圖訪問(wèn)一個(gè)作用域鏈中不存在的屬性的話,你就會(huì)得到一個(gè)ReferenceError
。
在作用域鏈的最頂層的元素就是全局對(duì)象(Global Object)了。運(yùn)行在全局環(huán)境的JavaScript代碼中,作用域鏈?zhǔn)冀K只含有一個(gè)元素,那就是全局對(duì)象。所以,當(dāng)你在全局環(huán)境中定義變量的時(shí)候,它們就會(huì)被定義到全局對(duì)象中。當(dāng)函數(shù)被調(diào)用的時(shí)候,作用域鏈就會(huì)包含多個(gè)作用域?qū)ο蟆?
好了,理論就說(shuō)到這里。接下來(lái)我們來(lái)從實(shí)際的代碼入手。
// my_script.js "use strict"; var foo = 1; var bar = 2;
我們?cè)谌汁h(huán)境中創(chuàng)建了兩個(gè)變量。正如我剛才所說(shuō),此時(shí)的作用域?qū)ο缶褪侨謱?duì)象。
在上面的代碼中,我們有一個(gè)執(zhí)行的上下文(myscript.js自身的代碼),以及它所引用的作用域?qū)ο?。全局?duì)象里面還含有很多不同的屬性,在這里我們就忽略掉了。
接下來(lái),我們看這段代碼
"use strict"; var foo = 1; var bar = 2; function myFunc() { //-- define local-to-function variables var a = 1; var b = 2; var foo = 3; console.log("inside myFunc");
} console.log("outside"); //-- and then, call it: myFunc();
當(dāng)myFunc
被定義的時(shí)候,myFunc
的標(biāo)識(shí)符(identifier)就被加到了當(dāng)前的作用域?qū)ο笾校ㄔ谶@里就是全局對(duì)象),并且這個(gè)標(biāo)識(shí)符所引用的是一個(gè)函數(shù)對(duì)象(function object)。函數(shù)對(duì)象中所包含的是函數(shù)的源代碼以及其他的屬性。其中一個(gè)我們所關(guān)心的屬性就是內(nèi)部屬性[[scope]]
。[[scope]]
所指向的就是當(dāng)前的作用域?qū)ο?。也就是指的就是函?shù)的標(biāo)識(shí)符被創(chuàng)建的時(shí)候,我們所能夠直接訪問(wèn)的那個(gè)作用域?qū)ο螅ㄔ谶@里就是全局對(duì)象)。
“直接訪問(wèn)”的意思就是,在當(dāng)前作用域鏈中,該作用域?qū)ο筇幱谧畹讓?,沒(méi)有子作用域?qū)ο蟆?
所以,在console.log("outside")
被運(yùn)行之前,對(duì)象之間的關(guān)系是如下圖所示。
溫習(xí)一下。myFunc
所引用的函數(shù)對(duì)象其本身不僅僅含有函數(shù)的代碼,并且還含有指向其被創(chuàng)建的時(shí)候的作用域?qū)ο?/strong>。這一點(diǎn)非常重要!
當(dāng)myFunc
函數(shù)被調(diào)用的時(shí)候,一個(gè)新的作用域?qū)ο蟊粍?chuàng)建了。新的作用域?qū)ο笾邪?code style="font-size:12px;font-family:'courier new';color:#777777;padding-bottom:1px;padding-top:1px;padding-left:4px;margin:0px 4px;padding-right:4px;background-color:#eeeeee;border-radius:2px;">myFunc函數(shù)所定義的本地變量,以及其參數(shù)(arguments)。這個(gè)新的作用域?qū)ο蟮母缸饔糜驅(qū)ο缶褪窃谶\(yùn)行myFunc
時(shí)我們所能直接訪問(wèn)的那個(gè)作用域?qū)ο蟆?
所以,當(dāng)myFunc
被執(zhí)行的時(shí)候,對(duì)象之間的關(guān)系如下圖所示。
現(xiàn)在我們就擁有了一個(gè)作用域鏈。當(dāng)我們?cè)噲D在myFunc
當(dāng)中訪問(wèn)某些變量的時(shí)候,JavaScript會(huì)先在其能直接訪問(wèn)的作用域?qū)ο螅ㄟ@里就是myFunc() scope
)當(dāng)中查找這個(gè)屬性。如果找不到,那么就在它的父作用域?qū)ο螽?dāng)中查找(在這里就是Global Object
)。如果一直往上找,找到?jīng)]有父作用域?qū)ο鬄橹惯€沒(méi)有找到的話,那么就會(huì)拋出一個(gè)ReferenceError
。
例如,如果我們?cè)?code style="font-size:12px;font-family:'courier new';color:#777777;padding-bottom:1px;padding-top:1px;padding-left:4px;margin:0px 4px;padding-right:4px;background-color:#eeeeee;border-radius:2px;">myFunc中要訪問(wèn)a
這個(gè)變量,那么在myFunc scope
當(dāng)中就可以找到它,得到值為1
。
如果我們嘗試訪問(wèn)foo
,我們就會(huì)在myFunc() scope
中得到3
。只有在myFunc() scope
里面找不到foo
的時(shí)候,JavaScript才會(huì)往Global Object
去查找。所以,這里我們不會(huì)訪問(wèn)到Global Object
里面的foo
。
如果我們嘗試訪問(wèn)bar
,我們?cè)?code style="font-size:12px;font-family:'courier new';color:#777777;padding-bottom:1px;padding-top:1px;padding-left:4px;margin:0px 4px;padding-right:4px;background-color:#eeeeee;border-radius:2px;">myFunc() scope當(dāng)中找不到它,于是就會(huì)在Global Object
當(dāng)中查找,因此查找到2。
很重要的是,只要這些作用域?qū)ο笠廊槐灰茫鼈兙筒粫?huì)被垃圾回收器(garbage collector)銷(xiāo)毀,我們就一直能訪問(wèn)它們。當(dāng)然,當(dāng)引用一個(gè)作用域?qū)ο蟮淖詈笠粋€(gè)引用被解除的時(shí)候,并不代表垃圾回收器會(huì)立刻回收它,只是它現(xiàn)在可以被回收了。
所以,當(dāng)myFunc()
返回的時(shí)候,再也沒(méi)有人引用myFunc() scope
了。當(dāng)垃圾回收結(jié)束后,對(duì)象之間的關(guān)系變成回了調(diào)用前的關(guān)系。
接下來(lái),為了圖表直觀起見(jiàn),我將不再將函數(shù)對(duì)象畫(huà)出來(lái)。但是,請(qǐng)永遠(yuǎn)記著,函數(shù)對(duì)象里面的[[scope]]
屬性,保存著該函數(shù)被定義的時(shí)候所能夠直接訪問(wèn)的作用域?qū)ο蟆?
正如前面所說(shuō),當(dāng)一個(gè)函數(shù)返回后,沒(méi)有其他對(duì)象會(huì)保存對(duì)其的引用。所以,它就可能被垃圾回收器回收。但是如果我們?cè)诤瘮?shù)當(dāng)中定義嵌套的函數(shù)并且返回,被調(diào)用函數(shù)的一方所存儲(chǔ)呢?(如下面的代碼)
function myFunc() { return innerFunc() { // ... }
} var innerFunc = myFunc();
你已經(jīng)知道的是,函數(shù)對(duì)象中總是有一個(gè)[[scope]]
屬性,保存著該函數(shù)被定義的時(shí)候所能夠直接訪問(wèn)的作用域?qū)ο?。所以,?dāng)我們?cè)诙x嵌套的函數(shù)的時(shí)候,這個(gè)嵌套的函數(shù)的[[scope]]
就會(huì)引用外圍函數(shù)(Outer function)的當(dāng)前作用域?qū)ο蟆?
如果我們將這個(gè)嵌套函數(shù)返回,并被另外一個(gè)地方的標(biāo)識(shí)符所引用的話,那么這個(gè)嵌套函數(shù)及其[[scope]]
所引用的作用域?qū)ο缶筒粫?huì)被垃圾回收所銷(xiāo)毀。
"use strict"; function createCounter(initial) { var counter = initial; function increment(value) {
counter += value;
} function get() { return counter;
} return {
increment: increment,
get: get
};
} var myCounter = createCounter(100); console.log(myCounter.get()); // 返回 100 myCounter.increment(5); console.log(myCounter.get()); // 返回 105
當(dāng)我們調(diào)用createCounter(100)
的那一瞬間,對(duì)象之間的關(guān)系如下圖
注意increment
和get
函數(shù)都存有指向createCounter(100) scope
的引用。如果createCounter(100)
沒(méi)有任何返回值,那么createCounter(100) scope
不再被引用,于是就可以被垃圾回收。但是因?yàn)?code style="font-size:12px;font-family:'courier new';color:#777777;padding-bottom:1px;padding-top:1px;padding-left:4px;margin:0px 4px;padding-right:4px;background-color:#eeeeee;border-radius:2px;">createCounter(100)實(shí)際上是有返回值的,并且返回值被存儲(chǔ)在了myCounter
中,所以對(duì)象之間的引用關(guān)系變成了如下圖所示
所以,createCounter(100)
雖然已經(jīng)返回了,但是它的作用域?qū)ο笠廊淮嬖?,可?strong style="padding-bottom:0px;padding-top:0px;padding-left:0px;margin:0px;padding-right:0px;">且僅只能被嵌套的函數(shù)(increment
和get
)所訪問(wèn)。
讓我們?cè)囍\(yùn)行myCounter.get()
。剛才說(shuō)過(guò),函數(shù)被調(diào)用的時(shí)候會(huì)創(chuàng)建一個(gè)新的作用域?qū)ο?,并且該作用域?qū)ο蟮母缸饔糜驅(qū)ο髸?huì)是當(dāng)前可以直接訪問(wèn)的作用域?qū)ο?。所以,?dāng)myCounter.get()
被調(diào)用時(shí)的一瞬間,對(duì)象之間的關(guān)系如下。
在myCounter.get()
運(yùn)行的過(guò)程中,作用域鏈最底層的對(duì)象就是get() scope
,這是一個(gè)空對(duì)象。所以,當(dāng)myCounter.get()
訪問(wèn)counter
變量時(shí),JavaScript在get() scope
中找不到這個(gè)屬性,于是就向上到createCounter(100) scope
當(dāng)中查找。然后,myCounter.get()
將這個(gè)值返回。
調(diào)用myCounter.increment(5)
的時(shí)候,事情變得更有趣了,因?yàn)檫@個(gè)時(shí)候函數(shù)調(diào)用的時(shí)候傳入了參數(shù)。
正如你所見(jiàn),increment(5)
的調(diào)用創(chuàng)建了一個(gè)新的作用域?qū)ο螅⑶移渲泻袀魅氲膮?shù)value
。當(dāng)這個(gè)函數(shù)嘗試訪問(wèn)value
的時(shí)候,JavaScript立刻就能在當(dāng)前的作用域?qū)ο笳业剿?。然而,這個(gè)函數(shù)試圖訪問(wèn)counter
的時(shí)候,JavaScript無(wú)法在當(dāng)前的作用域?qū)ο笳业剿?,于是就?huì)在其父作用域createCounter(100) scope
中查找。
我們可以注意到,在createCounter
函數(shù)之外,除了被返回的get
和increment
兩個(gè)方法,沒(méi)有其他的地方可以訪問(wèn)到value
這個(gè)變量了。這就是用閉包實(shí)現(xiàn)“私有變量”的方法。
我們注意到initial
變量也被存儲(chǔ)在createCounter()
所創(chuàng)建的作用域?qū)ο笾校M管它沒(méi)有被用到。所以,我們實(shí)際上可以去掉var counter = initial;
,將initial
改名為counter
。但是為了代碼的可讀性起見(jiàn),我們保留原有的代碼不做變化。
需要注意的是作用域鏈?zhǔn)遣粫?huì)被復(fù)制的。每次函數(shù)調(diào)用只會(huì)往作用域鏈下面新增一個(gè)作用域?qū)ο蟆K?,如果在函?shù)調(diào)用的過(guò)程當(dāng)中對(duì)作用域鏈中的任何一個(gè)作用域?qū)ο蟮淖兞窟M(jìn)行修改的話,那么同時(shí)作用域鏈中也擁有該作用域?qū)ο蟮暮瘮?shù)對(duì)象也是能夠訪問(wèn)到這個(gè)變化后的變量的。
這也就是為什么下面這個(gè)大家都很熟悉的例子會(huì)不能產(chǎn)出我們想要的結(jié)果。
"use strict"; var elems = document.getElementsByClassName("myClass"), i; for (i = 0; i < elems.length; i++) {
elems[i].addEventListener("click", function () { this.innerHTML = i;
});
}
在上面的循環(huán)中創(chuàng)建了多個(gè)函數(shù)對(duì)象,所有的函數(shù)對(duì)象的[[scope]]
都保存著對(duì)當(dāng)前作用域?qū)ο蟮囊谩6兞?code style="font-size:12px;font-family:'courier new';color:#777777;padding-bottom:1px;padding-top:1px;padding-left:4px;margin:0px 4px;padding-right:4px;background-color:#eeeeee;border-radius:2px;">i正好就在當(dāng)前作用域鏈中,所以循環(huán)每次對(duì)i
的修改,對(duì)于每個(gè)函數(shù)對(duì)象都是能夠看到的。
現(xiàn)在我們來(lái)看一個(gè)更有趣的例子。
"use strict"; function createCounter(initial) { // ... } var myCounter1 = createCounter(100); var myCounter2 = createCounter(200);
當(dāng)myCounter1
和myCounter2
被創(chuàng)建后,對(duì)象之間的關(guān)系為
在上面的例子中,myCounter1.increment
和myCounter2.increment
的函數(shù)對(duì)象擁有著一樣的代碼以及一樣的屬性值(name
,length
等等),但是它們的[[scope]]
指向的是不一樣的作用域?qū)ο?/strong>。
這才有了下面的結(jié)果
var a, b;
a = myCounter1.get(); // a 等于 100 b = myCounter2.get(); // b 等于 200 myCounter1.increment(1);
myCounter1.increment(2);
myCounter2.increment(5);
a = myCounter1.get(); // a 等于 103 b = myCounter2.get(); // b 等于 205
this
this
的值不會(huì)被保存在作用域鏈中,this
的值取決于函數(shù)被調(diào)用的時(shí)候的情景。
譯者注:對(duì)這部分,譯者自己曾經(jīng)寫(xiě)過(guò)一篇更加詳盡的文章,請(qǐng)參考《用自然語(yǔ)言的角度理解JavaScript中的this關(guān)鍵字》。原文的這一部分以及“
this
在嵌套的函數(shù)中的使用”譯者便不再翻譯。
讓我們來(lái)回想我們?cè)诒疚拈_(kāi)頭提到的一些問(wèn)題。
本文采用下面的專(zhuān)有名詞翻譯表,如有更好的翻譯請(qǐng)告知,尤其是加*
的翻譯
藍(lán)藍(lán)設(shè)計(jì)( tweetduck.com )是一家專(zhuān)注而深入的界面設(shè)計(jì)公司,為期望卓越的國(guó)內(nèi)外企業(yè)提供卓越的UI界面設(shè)計(jì)、BS界面設(shè)計(jì) 、 cs界面設(shè)計(jì) 、 ipad界面設(shè)計(jì) 、 包裝設(shè)計(jì) 、 圖標(biāo)定制 、 用戶(hù)體驗(yàn) 、交互設(shè)計(jì)、 網(wǎng)站建設(shè) 、平面設(shè)計(jì)服務(wù)
藍(lán)藍(lán)設(shè)計(jì)的小編 http://tweetduck.com