本篇文章,咱們來講說老(bei)生(xie)常(lan)談(le)的閉包,不少文章包括一些權威書籍中對於閉包的解釋不盡相同,每一個人的理解也都不同。而且在其餘語言中,也有對閉包的不一樣實現,讓咱們來看看
Javascript
中是如何實現閉包的以及有哪些特性。javascript
直接進入主題,上一段簡短的代碼:java
function outer(count) {
var temp = new Array(count)
function log() {
console.log(temp)
}
log()
function inner() {
console.log('done')
}
return inner
}
var o = {}
for(var i = 0; i < 1000000; i++) {
o["f"+i] = outer(i)
}
複製代碼
若是你不知道這段代碼可能帶來的問題,那麼這篇文章就值得你讀一讀。git
咱們先把上面的問題放一放,先讓咱們來看一看下面這段簡單的代碼:github
function outer() {
var b = 2
function inner() {
console.log(a, b)
}
return inner
}
var a = 1
var inner = outer()
inner()
複製代碼
在 JS引擎
中,是經過執行上下文棧來管理和執行代碼的。上述代碼的僞執行過程以下(本節內容主要參考冴羽大大的系列文章):bash
0、程序開始閉包
ECStack = []
複製代碼
一、建立全局上下文globalContext,並將其入棧函數
ECStack = [
globalContext
]
複製代碼
二、在執行以前初始化這個全局上下文工具
globalContext = {
VO: {
a: undefined,
inner: undefined,
outer: function outer() {...}
},
Scope: [globalContext]
}
複製代碼
初始化做用域鏈屬性 Scope
爲 [globalContext]
,此時代碼尚未執行,因爲變量提高的緣故, inner
和 a
變量爲 undefined
,須要注意的是,這個時候,outer
函數的做用域 [[scope]]
內部屬性已肯定(靜態做用域):ui
outer.[[scope]] = [
globalContext.VO
]
複製代碼
三、執行
globalContext
全局上下文spa
在執行過程當中,不斷改變 VO
,執行到 a = 1
語句,將 VO
中的 a
置爲 1
,執行到 inner = outer()
語句,執行 outer
函數,進入 outer
函數的執行上下文。
四、建立outerContext執行上下文,將其入棧
ECStack = [
outerContext,
globalContext
]
複製代碼
五、初始化
outerContext
執行上下文
outerContext = {
VO: {
b: undefined,
inner: function inner() {...}
},
Scope: [VO, globalContext.VO]
}
複製代碼
初始化做用域鏈屬性 Scope
爲 [VO].concat(outer.[[scope]])
即 [VO, globalContext.VO]
。 並在此時,肯定 inner
函數的 [[scope]]
屬性:
inner.[[scope]] = [
outerContext.VO,
globalContext.VO
]
複製代碼
六、執行
outerContext
上下文
執行語句 b = 2
,將 VO
中的 b
置爲 2
,最後返回 inner
。
七、
outerContext
執行完畢,出棧,繼續回到globalContext
執行餘下的代碼
ECStack = [
globalContext
]
複製代碼
繼續執行 inner = outer()
語句的賦值操做,將 outer
函數的返回結果賦給 inner
變量。
執行 inner()
語句,進入 inner
函數的執行上下文。
八、建立
innerContext
執行上下文,將其入棧
ECStack = [
innerContext,
globalContext
]
複製代碼
九、初始化
innerContext
執行上下文
innerContext = {
VO: {},
Scope: [VO, outerContext.VO, globalContext.VO]
}
複製代碼
初始化做用域鏈屬性 Scope
爲 [VO].concat(inner.[[scope]])
即 [VO, outerContext.VO, globalContext.VO]
。
十、執行
innerContext
上下文
執行語句 console.log(a, b)
,VO
中沒有變量 a
,往上查找到 outerContext.VO
,找到變量 a
,VO
中沒有變量 b
,依次往上查找到 globalContext.VO
,找到變量 b
。執行 console.log
函數,這裏一樣涉及到 變量console
的做用域鏈查找,console.log
函數的執行上下文切換,再也不贅述。
十一、
globalContext
執行完畢,出棧,程序結束
ECStack = []
複製代碼
在第7步中,outerContext
執行完畢後,雖然其已出棧並在隨後被垃圾回收機制回收,可是能夠看到 innerContext.Scope
仍有對 outerContext.VO
的引用。當 outerContext
被回收後,outerContext.VO
並不會被回收,以下圖:
這就使得咱們在執行 inner
函數時仍能夠經過其做用域鏈訪問到已執行完畢的 outer
函數中的變量,這就是閉包。
經過執行上下文和做用域鏈相關知識,咱們引出了閉包的概念,讓咱們繼續。
在第5步中,咱們說到,初始化 outerContext
的過程當中,同時肯定了 inner
函數的做用域屬性 [[scope]]
爲 [outerContext.VO, globalContext.VO]
,這實際上是不許確的。
咱們稍微改動下加上兩句代碼:
function outer() {
var b = 2
var c = new Array(100000).join('*')
var d = 3
function inner() {
console.log(a, b)
}
return inner
}
var a = 1
var inner = outer()
inner()
複製代碼
聰明的你會發現,變量 c
和 d
在inner中並不會用到,若是按照如上所述,將 inner
函數的 [[scope]]
屬性置爲 [outerContext.VO, globalContext.VO]
,那麼變量 c
(準確的說應該是變量 c
指向的那塊內存,下同)只能一直等到 inner
函數執行完畢後纔會被銷燬,若是 inner
函數一直不執行的話,new Array(100000).join('*')
所佔用的內存一直沒法被釋放。
那麼,你可能會想,咱們在肯定 inner
函數 [[scope]]
屬性的時候,只引用 inner
函數體內用到的變量不就行了嗎?實際上,JS引擎
和你同樣聰明,就是這麼幹的,在 Chrome
調試工具下:
能夠看到,並無對變量 c
的引用,咱們能夠認爲 inner
函數 [[scope]]
屬性爲:
inner.[[scope]] = [
Closure(outerContext.VO),
globalContext.VO
]
複製代碼
這裏,咱們用 Closure
這樣一個函數來表示獲得內部函數體中(包括內部函數中的內部函數,一直下去...)引用外部函數變量的集合,即閉包。
讓咱們繼續前進的腳步,把上面的代碼再稍微改動下:
function outer() {
var b = 2
var c = new Array(100000).join('*')
var d = 3
function log() {
console.log(c)
}
function inner() {
console.log(a, b)
}
log()
return inner
}
var a = 1
var inner = outer()
inner()
複製代碼
這裏,咱們只是加了一個 log
函數,並將變量 c
打印出來。對於 inner
函數來講,並無什麼改變,果然如此嗎?咱們看下 Chrome
調試工具下做用域和閉包相關信息。
outer函數執行以前:
outer函數執行完成:
咦,咱們能夠看到 inner
函數中的閉包中居然包含了變量 c
!可是 inner
函數中並無用到 c
啊,你可能隱隱發現了什麼,是的,咱們在 log
函數中引用了變量 c
,這居然會影響到 inner
函數的閉包。
在前文中,咱們說到肯定 inner
函數 [[scope]]
屬性時,會經過 Closure
函數獲得 inner
函數體內引用到的全部閉包變量集合,那有多個內部函數呢?
其實,JS引擎
會經過 Clousre
函數獲得 outer
函數下全部內部函數體中用到的閉包變量集合 Closure(outerContext.VO)
,而且全部的內部函數的 [[scope]]
屬性都引用這個共同的閉包,因此:
inner.[[scope]] = [
Closure(outerContext.VO),
globalContext.VO
]
log.[[scope]] = [
Closure(outerContext.VO),
globalContext.VO
]
Closure(outerContext.VO) = { b, c }
複製代碼
讓咱們來看看 log
函數的閉包信息,一樣也有變量 b
:
這裏,你可能會有疑問,變量 a
哪裏去了,其實變量 a
在 globalContext
下。
讀到這裏,細心的你會發現,這和文章開頭給出的代碼幾乎一毛同樣啊,那究竟會帶來什麼問題呢,我想你應該知道了:內存泄露!
讓咱們回到文章開頭的那段代碼,返回的 inner
函數中,一直引用着 temp
變量,在 inner
函數不執行的狀況下,temp
變量一直沒法被垃圾回收。
咱們再稍微改下代碼:
function outer(count) {
var temp = new Array(count)
function log() {
console.log(temp)
}
log()
function inner() {
var message = 'done'
return function innermost() {
console.log(message)
}
}
return inner()
}
var o = {}
for(var i = 0; i < 1000000; i++) {
o["f"+i] = outer(i)
}
複製代碼
這裏,咱們在 inner
函數裏面又包了一層,那最終返回的 innermost
還有對 temp
變量的引用嗎?
按照前面關於執行上下文相關內容的邏輯分析下去,實際上是有的。innermost
的 [[scope]]
屬性以下:
innermost.[[scope]] = [
Closure(innerContext.VO): { message },
Closure(outerContext.VO): { temp },
globalContext
]
複製代碼
固然,你可能會說,只要 inner
函數執行完成後,這些內存就會被回收掉。OK,那咱們再來看一個更經典的例子:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
複製代碼
unused
函數引用了 originalThing
,因爲共享閉包的特性,theThing.someMethod
函數的閉包中也包含了對 originalThing
的引用,而 originalThing
是上一個 theThing
,也就是說下一個 theThing
引用者上一個 theThing
,造成了一個鏈。並隨着 setInterval
的執行,這個鏈愈來愈長,最終致使內存泄露,以下:
若是把間隔時間改小點,分分鐘 out of memory
。
這個例子來源於這裏,建議你們都點進去讀一讀(我記得以前有小夥伴翻譯了這篇文章的,一時找不到了,有知道中文翻譯連接的小夥伴在評論裏貼一下哈)。
Real Local Variable
vs Context Variable
Real Local Variable
,直譯過來就是真正的局部變量,在這裏變量 d
就是 Real Local Variable
,在C++層面,它能夠直接分配在棧上,隨着 inner
函數執行完畢的出棧操做而被當即回收掉,不須要後面垃圾回收機制的干預。
Context Variable
,上下文環境變量或者稱之爲閉包變量,在這裏變量 b
就是 Context Variable
, 在C++層面,它必定分配在堆上,儘管這裏它是一個基本類型。
那變量 c
呢,你能夠認爲它是一個 Real Local Variable
,只是在棧上存的是指向這個 new Array()
的內存地址,而 new Array()
的實際內容是存在堆上的。
內存分佈以下:
經過上面的分析,咱們在不少文章中常常看到的 基本類型分佈在棧上,引用類型分佈在堆上
這句話明顯是錯誤的,對於被閉包引用的變量,無論其是什麼類型,確定是分配在堆上的。
eval
與閉包前文中已經提到,JS引擎
會分析全部內部函數體中引用了哪些外部函數的變量,可是對於 eval
的直接調用是沒法分析的。由於沒法預料到 eval
中可能會訪問那些變量,因此會把外部函數中的全部變量都囊括進來。
function outer() {
var b = 2
var c = new Array(100000).join('*')
var d = 3
function inner() {
eval("console.log(1)")
}
return inner
}
var a = 1
var inner = outer()
inner()
複製代碼
JS引擎
心裏OS是這樣的:eval
這傢伙什麼事情都乾的出來,大家(局部變量)通通不許走!
若是,你在層層嵌套的函數下面來一個 eval
,那麼 eval
所在函數的全部父級函數中的變量都沒法被釋放掉,想一想就可怕...
那對於 eval
的間接調用呢?
function outer() {
var b = 2
var c = new Array(100000).join('*')
var d = 3
function inner() {
(0, eval)("console.log(a)") // 輸出1
}
return inner
}
var a = 1
var inner = outer()
inner()
複製代碼
這時 JS引擎
心裏OS又是這樣的:eval
是誰,不認識,大家(局部變量)都回家收衣服吧...
其實,對於 eval
和 function
的組合還有各類姿式,好比:
function outer() {
var b = 2
var c = new Array(100000).join('*')
var d = 3
return eval("(function() { console.log(a) })")
// return (0,eval)("(function() { console.log(a) })")
// return (function(){ return eval("(function(){ console.log(a) })") })()
// ...
// 更多姿式留待各位本身去發掘和嘗試,逃...
}
var a = 1
var inner = outer()
inner()
複製代碼
到這裏就寫完了,但願各位對閉包有一個新的認識和看法。
最後歡迎各路大佬們啪啪打臉...