以前寫了一篇文章瀏覽器是怎麼看閉包的,發現有些讀者對js內存分配與回收懵懵懂懂,理解文章的配圖有些困難,我想主要是由於配圖省略了一些細節。今天專門寫一篇關於js內存分配回收的文章,幫助你們理解js代碼的內存表示。原文備份在這裏javascript
先嘮叨些基本知識:html
咱們都知道的是,javascript中值類型是在變量所在的內存單元中存放的,而對於引用類型的對象,變量所在的內存單元存放的是堆空間中對象的內存地址。咱們還應該知道的是,函數在執行時,局部變量是在棧空間中建立,引用對象是在堆空間中建立的。java
咱們仍是從代碼入手:瀏覽器
var a = 'abc'
var b = 123
var c = true
var d = undefined
var e = null
var f = {
n: 'test'
}複製代碼
這段代碼咱們定義了六個全局變量,每一個變量賦予不一樣類型的值,咱們發現,a、b、c、d、e基本類型的值佔據一個內存單元,而變量f內存儲的是堆中對象的地址。以下圖表示:閉包
變量f中存儲的0x00012345是堆中對象的內存地址。函數
一切都很容易理解。細心的同窗也許會指出,null也是對象,經過typeof null 表達式獲得的結果是'object'。關於這個,我想說的是typeof null = 'object' 這個現象是歷史遺留bug。事實上null是空值,並非對象。post
js的類型值有1-3位是表示類型,其它位表示真實值。
000: object. The data is a reference to an object.
也就是說,000開頭的被認爲是指向對象引用。因爲js中的null是空指針,在大多數平臺上空指針的前兩位是0x00,再加上null的數值表示是0,因此null的前三位是0x000,js引擎會認爲它是指向對象的引用,這是一個歷史遺留bug。但事實上,null是空值。詳細解釋參見這裏。測試
說到null,咱們還要用圖形表示一下null所起到的做用。對於上面的代碼,咱們將引用類型f置爲null,該變量將再也不指向堆中對象。圖形表示以下:spa
你會發現,本來f指向堆對象的線消失了,堆中對應的對象再也不被f引用。3d
看到這裏,你也許會問:咦,那沒有任何對象指向那個堆對象了,它還佔據內存嗎?若是還佔據的話,豈不是佔着茅坑不那啥嗎?我想,若是能想到這一點,說明你是一個有追求的js開發者。
是的,本來堆空間中的那個對象確實沒有引用了,js引擎會在下一個垃圾回收節點將它回收掉。
爲了幫助你們更好的理解內存的分配與釋放,建議你們在看配圖的時候,必定要謹記箭頭的走向,認真看箭頭從哪一個對象出發,又是指向哪一個對象的。由於箭頭指向表明變量引用,而引用是垃圾回收器辨別內存垃圾的依據。什麼是垃圾呢?按照垃圾回收器的理解是,從根對象觸發,沿着箭頭指向,可以找到的對象,都不該該斷定爲垃圾。相反,從根對象觸發,沿着箭頭指向,不可以找到的對象,被斷定爲垃圾,將在下一個垃圾回收節點回收掉。
那麼,與之相伴的是內存泄漏,什麼叫內存泄漏呢?通俗一點講,就是某個對象已經不會再被咱們用到,可是垃圾回收器卻發現從根對象仍然可以找到它,因此不認爲它是垃圾,所以不會回收它,可是它確實對咱們沒有用處。這樣就形成了內存的浪費,這種現象稱做內存泄漏。
理解了斷定內存垃圾的方式以及內存泄漏,咱們就能夠經過畫圖的方式來檢驗代碼是否存在內存泄漏,代碼是否健壯。
上面代碼中,f指向的對象是一個簡單對象,只包含一個屬性,若是堆中的是一個複雜對象,又該如何表示呢?讓咱們繼續看代碼
var a = {
b: 123,
c: 'abc',
d: true,
e: null,
f: {
h: 'test',
j: {
k: 567
}
}
}複製代碼
咱們定義了一個全局變量a,指向堆內存中的一個複雜對象。以下圖:
全局定義的變量是常駐內存的,爲何常駐內存?咱們從垃圾回收的角度分析一下:
因此,全局定義的變量a所關聯的對象是常駐內存的。
再次思考一下,咱們如何讓垃圾回收器回收堆空間右側的那兩個對象呢?聰明的你也許想到了
a.f = null複製代碼
是的,將a.f的指針置爲null就能夠了。咱們從垃圾回收的角度分析一下,a.f = null這段代碼執行之後,f變量中存儲的值變成了null,再也不指向右側的兩個對象,按照咱們以前的方法,從根對象window開始,沿着箭頭尋找,找不到右側對象,因此右側兩個對象成爲內存垃圾,將被GC(垃圾回收器)回收掉。這就是當咱們爲一個變量賦值null以後,變量在內存中的變化。以下圖所示:
固然,咱們也能夠爲變量賦予其餘基礎類型的值,斷開和堆中對象的聯繫。
對於再複雜的對象,你們能夠觸類旁通。接下來,咱們看一下函數的定義:
function say() {
var a = '測試'
var b = {
c: 123
}
}複製代碼
能夠看到函數對象指針在全局變量區,函數自己在堆中存放,函數我只畫了了幾個常見的屬性。細心的你也許發現有個[[Socpes]]的屬性,之後講閉包的時候再對它做詳細介紹,這裏只大概介紹一下。
[[Scopes]]屬性是在函數建立的時候附加的屬性,表明該函數的做用域鏈。
繼續看一段代碼:
function say() {
var a = '測試'
var b = {
c: 123
}
}
say()複製代碼
很簡單,咱們定義一個函數,並執行它,變量的內存圖以下:
函數運行時,局部變量分配在棧空間,此時,外部window對象與棧空間中的變量沒有引用關係。局部變量a是值類型,在棧中存放,局部變量b是引用類型,棧中存放對象在堆中的內存地址。
函數運行結束後的圖示以下:
函數運行結束,局部變量因爲沒有外部引用,因此所有釋放,同時堆中的對象也失去了引用,成爲內存垃圾,被GC回收掉。
至此,關於js代碼的內存表示就先告一段落,經過畫圖的方式,但願你們能對程序的執行有感性的理解,也但願能幫助你們經過畫圖的方式去斷定內存垃圾。另外,你們在看下一篇文章瀏覽器是怎麼看閉包的的時候,會發現有一些細節沒有表示,你們不要太過於糾結,只需注意箭頭走向便可。