想要理解閉包之前,就必須理解函式的建立過程、活動變數AO、作用域鏈。我曾寫過相關的文章
網上相關對閉包的定義:
- MDN:函式和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起構成閉包(closure)。也就是說,閉包可以讓你從內部函式訪問外部函式作用域。在 JavaScript 中,每當函式被建立,就會在函式生成時生成閉包。
- 你不知道的JavaScript:是指有權訪問另外一個函式作用域中的變數的函式。建立閉包的常見方式就是在一個函式內部建立另外一個函式。
- Javascript核心技術開發解密:閉包是一種特殊物件,由兩部分組成:執行上下文A + 該執行上下文建立的函式B
我對這些定義的理解:
- 《MDN》的解釋更加接近原理,
- 《你不知道的Javascript》的解釋更多講的是現象,
- 《Javascript核心技術開發解密》的解釋更能說明閉包的真實存在:閉包是一種特殊物件。
---------------------------------人工分割線------------------------------
下面我們通過幾個栗子來一步步講解閉包的原理:
栗子一:
function makeFunc() {
var name = "Mozilla";
function displayName() {
console.log(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
列印結果:Mozilla
- 混淆點1當執行到var myFunc = makeFunc();makeFunc函式在執行完之後裡面的name不是應該被垃圾回收機制給處理掉了嗎?
答案:其實通過垃圾回收機制大概也知道name屬性不可能被回收,因為還有myFunc函式持有name的引用。 - 混淆點2:與普通函式有什麼不同?
答案:如果沒產生閉包,那麼函式中的臨時變數都被回收了。 - 混淆點3:name屬性如果不回收,那麼存放在哪裡?
答案:這個問題後面會講到
【myFunc在google瀏覽器的內部屬性,生成了一個閉包物件makeFunc】
先來思考一個問題:我們知道當執行了makeFunc函式後會產生對應的變數物件{變數物件儲存了函式中的臨時變數},那麼上圖中的閉包物件makeFunc是否就等於makeFunc函式所產生的變數物件?
帶著這個問題看下一個栗子--
栗子二:
function makeFunc() {
var name = "Mozilla";
var age = 12;
function displayName() {
console.log(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
思考: 此時的閉包物件makeFunc是否帶有age屬性?
答案:沒有age屬性
說明:閉包物件不等於makeFunc的變數物件。閉包物件僅儲存跨域的屬性。
延申到另一個常見問題:如何清除閉包?
我們知道閉包物件存在於myFunc函式內,所以一句:myFunc = null。使得閉包物件沒有引用持有那麼等待他的就是垃圾回收。
再延申到另一個常見問題:MDN:在 JavaScript 中,每當函式被建立,就會在函式生成時生成閉包。那麼豈不是記憶體很快就洩露了?
實際上你的閉包大多數都是沒有引用持有,很快就會被回收掉的。並且JS對閉包也有相關的優化處理。
然而:這個時候,我們是否明白這個閉包物件與作用域鏈的關係是什麼?
栗子三:
var a = 20;
function test () {
var b = a + 10;
function innerTest () {
debugger
var c = 10;
return b + c;
}
innerTest();
}
test();
??? 當執行到debugger時,此時innerTest函式的作用鏈是什麼呢?閉包物件是否產生?
:::此時innerTest函式的作用鏈:
閉包物件已經產生,並且閉包物件作為作用域鏈中的物件。
你是否記得很多書上都說作用域鏈是一條又每個函式的VO物件組成的鏈條。但這裡看到的卻不是VO物件,而是閉包物件。
我的看法是:如果單純從函式的作用域來看:作用域鏈是一條又每個函式的VO物件組成的鏈條。這個說法很正確,這是真正能夠以此幫助我們判斷訪問作用域邊界的依據。但是在程式實際的執行中,經過詞法編譯的階段,JS引擎已經通過程式碼把各個實際上閉包產生的變數已經提煉出來。而不是直接就把VO物件放在作用域鏈。這也有利於提高訪問速度。
總結:
function makeFunc() {
var name = "Mozilla";
function displayName() {
console.log(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
執行過程有關閉包的變化: 當呼叫makeFunc()函式進入函式建立階段時發現displayName函式含有name的跨函式變數,所以在對displayName函式進行提升的時候就已經給displayName函式初始化了閉包物件【makeFunc閉包】。所以當執行myFunc()函式的時候,從當前myFunc()的VO物件找不到的話就會從作用域鏈中的上一級【makeFunc閉包】物件中找。
來個圖清晰一點:
--- 以上便是對閉包最新的理解。不對的望多多指出。