本文章著作權(quán)歸饑人谷_Lyndon和饑人谷所有寂嘉,轉(zhuǎn)載請注明出處。
閉包對于我而言是一個難點布近,但閉包又是一個很有用的知識點垫释,很多高級應(yīng)用都需要依賴閉包。
所以在參考一些文章加上大量練習(xí)后撑瞧,我來寫一寫自己理解閉包的過程棵譬,首先是弄清楚以下幾個知識點。
>>> Part 1. 變量的作用域
JS中预伺,變量的作用域只有兩種:全局作用域订咸、函數(shù)作用域。對應(yīng)的變量也只有兩種:全局變量酬诀、局部變量脏嚷。
函數(shù)內(nèi)部可以直接讀取全局變量。
var a = 1;
function f(){
console.log(a);
}
f(); // 1
但是函數(shù)外部無法讀取到函數(shù)內(nèi)部的局部變量瞒御。
function f(){
var a = 1;
}
console.log(a); // Uncaught ReferenceError: a is not defined
這一個Part是比較好理解的父叙。
>>> Part 2. 如何從外部讀取到局部變量?
在祿永老師的公開課中肴裙,老師將從外部讀取局部變量這一情況稱作“偉大的逃脫”趾唱。總結(jié)而言蜻懦,有兩種方法來實現(xiàn)甜癞。
- 返回值的方法:函數(shù)作為返回值
function f1(){
var a = 1;
function f2(){
console.log(a);
}
return f2;
}
var result = f1();
result(); // 1
函數(shù)f2包裹在函數(shù)f1內(nèi),根據(jù)作用域鏈的原理:子對象會一級一級向上尋找父對象的變量宛乃,f1所有的局部變量都可以被f2訪問到悠咱,反之則不行。因此只要把f2作為返回值征炼,就可以在f1外部讀取到其中的內(nèi)部變量析既。
- 句柄的方法:定義全局變量
var innerHandler = null;
function outerFunc(){
var outerVar = 1;
function innerFunc(){
console.log(outerVar);
var innerVar = 2;
}
innerHandler = innerFunc;
}
outerFunc();
innerHandler(); // 1
這一方法首先定義了一個值為null
的全局變量innerHandler
,然后讓innerHandler
等于函數(shù)內(nèi)部的函數(shù)谆奥,函數(shù)內(nèi)部的函數(shù)則可以通過作用域鏈訪問到父對象的變量outerVar
渡贾,之后在外部調(diào)用innerHandler
的時候,就可以訪問到outerFunc
函數(shù)中的內(nèi)部變量outerVar
雄右。
>>> Part 3. 閉包
網(wǎng)絡(luò)上有千萬種對閉包的解釋空骚,其實閉包就是上面例子中的兩個函數(shù):f2以及innerFunc。書面解釋就是:能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)擂仍。
在JS中囤屹,因為父函數(shù)內(nèi)部的子函數(shù)才能夠讀取局部變量,因此閉包的常見形式就是:定義在函數(shù)內(nèi)部的函數(shù)逢渔。前者是后者的充分不必要條件肋坚。
一言以蔽之,閉包就是連接函數(shù)內(nèi)部外部的渠道肃廓。
>>> Part 4. 對示例代碼段的解答
- 第一段代碼
function outerFn() {
console.log("Outer function");
function innerFn() {
var innerVar = 0;
innerVar++;
console.log("Inner function\t");
console.log("innerVar = "+innerVar+"");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
fnRef();
var fnRef2 = outerFn();
fnRef2();
fnRef2();
在這一段代碼當(dāng)中智厌,innerFn
不是一個閉包,因為它并不需要讀取其他函數(shù)的內(nèi)部變量盲赊,唯一的變量innerVar
就在innerFn
函數(shù)內(nèi)部铣鹏。在第一個fnRef()
之后,結(jié)果就是首先輸出Outer function
哀蘑,然后輸出Inner function
诚卸,由于innerVar
是函數(shù)innerFn
的內(nèi)部變量且自增,因此從0變?yōu)?绘迁,再輸出innerVar = 1
.
這時候需要明白合溺,當(dāng)再次運(yùn)行fnRef()
時,由于fnRef
本身已經(jīng)變成了函數(shù)innerFn
缀台,所以其輸出結(jié)果就不再有Outer Function
這一句棠赛,而是直接輸出:Inner function
以及innerVar = 1
.原因是此時的innerVar
是一個內(nèi)部變量,其作用域限定在innerFn
函數(shù)中膛腐,每次調(diào)用執(zhí)行innerFn
函數(shù)睛约,innerVar
都會被重寫。
對于下面的fnRef2()
依疼,也是同理痰腮。最后的輸出結(jié)果見下圖:
- 第二段代碼
var globalVar = 0;
function outerFn() {
console.log("Outer function");
function innerFn() {
globalVar++;
console.log("Inner function\t");
console.log("globalVar = " + globalVar + "");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
fnRef();
var fnRef2 = outerFn();
fnRef2();
fnRef2();
這里的globalVar
是一個外部變量,也是一個全局變量律罢,處于全局作用域下膀值。所以當(dāng)執(zhí)行innerFn
時,innerFn
函數(shù)將會訪問到一個每次都自增的全局作用域下的活動對象误辑,因此輸出的結(jié)果會從globalVar = 1
一直到globalVar = 4
.在執(zhí)行間歇中沧踏,globalVar
處于兩個函數(shù)的作用域之外,天高地遠(yuǎn)誰也管不了巾钉,所以它的值會被保存在內(nèi)存中翘狱,并不會立刻被抹去。最后的輸出結(jié)果見下圖:
- 第三段代碼
function outerFn() {
var outerVar = 0;
console.log("Outer function");
function innerFn() {
outerVar++;
console.log("Inner function\t");
console.log("outerVar = " + outerVar + "");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
fnRef();
var fnRef2 = outerFn();
fnRef2();
fnRef2();
閉包來臨了砰苍,這里的fnRef
是一個閉包innerFn
函數(shù)潦匈,但是此時的變量outerVar
來到了父函數(shù)的作用域內(nèi)阱高,不像之前一樣處于子函數(shù)作用域內(nèi)或者處于全局作用域下〔缢酰可以發(fā)現(xiàn)赤惊,這和Part 2中的例子非常相似。
其原理是:外部函數(shù)的調(diào)用環(huán)境為相互獨(dú)立的封閉閉包的環(huán)境凰锡,第二次的fnRef2
調(diào)用outerFn
沒有沿用第一次調(diào)用fnRef
時outerVar
的值未舟,第二次函數(shù)調(diào)用的作用域創(chuàng)建并綁定了一個新的outerVar
實例,兩個閉包環(huán)境中的計數(shù)器是相互獨(dú)立掂为,不存在關(guān)聯(lián)的裕膀。
進(jìn)一步來說,在每個封閉閉包環(huán)境中勇哗,外部函數(shù)的局部變量會保存在內(nèi)存中昼扛,并不會在外部函數(shù)調(diào)用后被自動清除。原因在于:outerFn
是innerFn
的父函數(shù)智绸,而innerFn
被賦值給一個全局變量野揪,因此innerFn
始終在內(nèi)存當(dāng)中,而它又依賴于outerFn
瞧栗,所以outerFn
也必須始終在內(nèi)存中斯稳,不會再函數(shù)被調(diào)用后就被抹去,因此閉包也有一點點不好迹恐,有可能造成內(nèi)存泄漏挣惰。
所以,結(jié)果應(yīng)該是:outerVar = 1
, outerVar = 2
, outerVar = 1
, outerVar = 2
.結(jié)果如下圖所示:
我寫到這自己已經(jīng)完全明白了殴边,我現(xiàn)在要用自己的理解來理順一下最經(jīng)典的問題憎茂。
>>> Part 5. 理順最經(jīng)典問題
<div id="divTest">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
</div>
<script>
var spans = document.querySelectorAll("#divTest span");
for(var i = 0; i < spans.length; i++){
spans[i].onclick = function(){
console.log(i);
}
}
</script>
最經(jīng)典的問題是:為什么我點擊任何數(shù)字,控制臺的輸出結(jié)果永遠(yuǎn)是4锤岸?
這里可使用作用域鏈來幫助理解竖幔,不妨將以上代碼轉(zhuǎn)化為:
// function只是傳遞給了NodeList類型對象中的元素卻并未執(zhí)行,因為后面無括號
spans[0] = function fn0(){console.log(i)};
spans[1] = function fn1(){console.log(i)};
spans[2] = function fn2(){console.log(i)};
spans[3] = function fn3(){console.log(i)};
globalContext = {
AO: {
i: undefined, // 0(fn0)1(fn1)2(fn2)3(fn3)4(終止循環(huán))
spans:[0], [1], [2], [3]
},
scope: null
}
fn0[[scope]] = globalContext.AO,
fn1[[scope]] = globalContext.AO,
fn2[[scope]] = globalContext.AO,
fn3[[scope]] = globalContext.AO
fn0Context = {
AO:{
},
scope: fn0[[scope]]
}
fn1Context = {
AO:{
},
scope: fn1[[scope]]
}
fn2Context = {
AO:{
},
scope: fn2[[scope]]
}
fn3Context = {
AO:{
},
scope: fn3[[scope]]
}
最后點擊span元素的時候i早已變?yōu)?是偷,因此永遠(yuǎn)輸出4.
改進(jìn)的方法可以使用閉包拳氢,也就是:
var spans = document.querySelectorAll("#divTest span");
for(var i = 0; i < spans.length; i++) {
spans[i].onclick = function(i){
return function (){
console.log(i);
}
}(i);
}
這個閉包也可以用作用域鏈來理解:
globalContext = {
AO:{
i: undefined,
spans: [0], [1], [2], [3]
}
}
fn0.scope = globalContext.AO,
fn1.scope = globalContext.AO,
fn2.scope = globalContext.AO,
fn3.scope = globalContext.AO
fn0Context = {
AO:{
i: 0,
function: anonymous
}
fn0[[scope]] = fn0.scope // globalContext.AO
}
function_anonymousContext = {
AO: {
}
function_anonymous[[scope]] = fn0Context.AO
}
...
>>> Part 6. 閉包的問題
如同剛才的分析一樣,當(dāng)涉及到閉包時蛋铆,函數(shù)中的變量都會被保存在內(nèi)存中馋评,因此需要避免濫用閉包,否則就有可能導(dǎo)致內(nèi)存泄露刺啦。