閉包是如何產生的?你知道嗎?
大家好,我是前端西瓜哥。
這次從內存管理的角度來看看,閉包是怎么產生的。
我們知道,在調用函數時,其實會產生臨時的 調用棧。這些調用棧保存的是 執行上下本,并實際保存在 棧內存 中。
每執行一個函數,函數內的局部臨時變量會臨時保存起來。如果此時函數又調用了另一個函數,另一個函數下的局部變量也要保存下來,就這樣,我們產生了棧。
當一個函數執行完后,它對應的局部臨時變量就會被銷毀。
局部變量保存下來,是為了保護上下文現場。
舉例說明一下:
這里我們嵌套調用了 a、b、c 函數,會產生如下的調用棧。
基本類型的臨時變量,會直接保存到棧內存中,對于引用類型,則是在堆內存中生成,然后將地址拿到,保存到棧內存中。
引用類型為什么不直接放到棧內存中?因為棧內存不是很大,很容易就棧溢出,而引用類型通常很大。
閉包的產生
函數調用完成后,它內部聲明的臨時變量會被銷毀。理論上應該如此,但如果使用了閉包,可以會讓臨時變量一直保留不被銷毀。
例子:
執行過程為:
- 執行函數 createCounter 時,會創建一個空的上下文對象。
- 遇到內部函數 counter,會預掃描內部函數 counter 使用了 createCounter 下的哪些便利,最終掃描出 count 變量。于是在堆內存創建一個閉包 Closure (createCounter) 對象,將 count 加進去。otherVal 不會加到閉包對象上,因為它沒有被使用。
- 這個內部函數最后被返回,被引用,閉包就一直不會銷毀。
使用 DevTool 可以觀察到這個閉包對象:
所以,如果一個閉包返回的函數執行完后不用了,要設置為 null。否則它關聯的閉包對象會一直在那里占用內存。
多個內部函數共享一個閉包對象
另外,如果有多個內部函數,這些函數會共用同一個閉包對象。即使其中的一個內部函數不會返回,它也會給閉包對象加東西。
下面我們加了一個 printOtherVal 的內部函數,它并不返回,但還是會導致返回 counter 函數對應的閉包對象帶上了它不需要的 otherVal 變量。
這是 JS 引擎處理閉包策略問題,理論不應該有這樣奇怪的效果。
結尾
調用函數時,會產生調用棧,將當前函數上下文入棧,會保存基本類型變量。引用變量會在堆內存中創建,然后在棧內存中引用過來。
因為 JavaScript 中函數是第一公民,所以會有閉包的概念。當發現內部函數,會創建一個閉包對象,將其中使用到的外部函數變量保存到該閉包對象下。之后內部函數被調用時,就會從閉包里提取變量,如果找不到則從全局上下文提取。