JavaScript系列之記憶體空間
對於很多沒經驗的前端開發來說,覺得JS反正有垃圾回收機制,很容易忽視記憶體空間的管理,這其實是一個大錯誤。
直到最近,看了阮一峰老師關於JS記憶體洩漏的文章,才發現自己以前寫的程式碼,存在許多記憶體洩漏的問題,再者,因為忽略對記憶體空間的學習,導致後面很多進階概念很模糊,比如閉包、作用域鏈,比如深拷貝與淺拷貝的區別等等。
這裡先介紹記憶體空間,後續還會通過別的文章來介紹深淺拷貝和記憶體洩漏。
記憶體空間管理
JavaScript的記憶體生命週期:
1. 分配你所需要的記憶體 2. 使用分配到的記憶體(讀、寫) 3. 不需要時將其釋放、歸還 複製程式碼
為了便於理解,我們使用一個簡單的例子來解釋這個週期。
var a = 10;// 在記憶體中給數值變數分配空間 alert(a + 90);// 使用分配到的記憶體 a = null; // 使用完畢之後,釋放記憶體空間 複製程式碼
在JS中,每一個數據都需要一個記憶體空間。記憶體空間又被分為兩種, 棧記憶體(stack) 與 堆記憶體(heap) 。
棧與堆
棧(stack)是有序的,主要存放一些 基本型別的變數和物件的地址 ,每個區塊按照一定次序存放(後進先出),它們都是直接按值儲存在棧中的,每種型別的資料佔用的記憶體空間的大小也是確定的,並由系統自動分配和自動釋放。
因此,這樣帶來的好處就是,記憶體可以及時得到回收,相對於堆來說,更加容易管理記憶體空間,且定址速度也更快。

堆(heap)是沒有特別的順序的,資料可以任意存放,多用於**複雜資料型別(引用型別)**分配空間,例如陣列物件、object物件。
其實這樣說也不太準確,因為,引用型別資料的地址是儲存於棧中的,當我們想要訪問引用型別的值的時候,需要先從棧中獲得想要訪問物件的地址,然後,再通過地址指向找出堆中的所需資料。就好比書架上的書,雖然已經按順序放好了,但我們只要知道書的名字,就可以對應的取下來。

變數的存放
首先,我們來看一下程式碼:
//原始型別都放在棧(stack)裡 //引用型別都放在堆(heap)裡 var a = 10; var b = 'lzm'; var c = true; var d = { n: 22 }; //地址假設為0x0012ff7f,不代表實際地址 var e = { n: 22 }; //重新開闢一段記憶體空間,地址假設為0x0012ff8c console.log(e==d); //false var obj = new Object(); //地址假設為0x0012ff9d var arr = ['a','b','c']; //地址假設為0x0012ff6e 複製程式碼
為什麼console.log(e==d)的結果為false?可以用下面的記憶體圖解釋:

變數a,b,c為基本資料型別,它們的值,直接存放在棧中,d,e,obj,arr為複合資料型別,他們的引用變數及地址儲存在棧中,指向於儲存在堆中的實際物件。我們是無法直接操縱堆中的資料的,也就是說我們無法直接操縱物件,我們只能通過棧中對物件的引用來操作物件,就像我們通過遙控機操作電視一樣,區別在於這臺電視本身並沒有控制按鈕。
變數d,e雖然指向存在堆記憶體中物件內容的值是相等的,但是它們來自棧記憶體中變數地址不相同,導致console.log(e==d)的結果為false。
這裡就回到了最初的疑問,為什麼原始型別值要放在棧中,而引用型別值要放在堆中,為什麼要分開放置呢?單列一種記憶體豈不是更省事嗎?那接下來,援引這篇文章裡邊的解釋:
記住一句話:能量是守衡的,無非是時間換空間,空間換時間的問題。堆比棧大,棧比堆的運算速度快,物件是一個複雜的結構,並且可以自由擴充套件,如:陣列可以無限擴充,物件可以自由新增屬性。將他們放在堆中是為了不影響棧的效率。而是通過引用的方式查詢到堆中的實際物件再進行操作。相對於簡單資料型別而言,簡單資料型別就比較穩定,並且它只佔據很小的記憶體。不將簡單資料型別放在堆是因為通過引用到堆中查詢實際物件是要花費時間的,而這個綜合成本遠大於直接從棧中取得實際值的成本。所以簡單資料型別的值直接存放在棧中。
比較摳細節的面試題
下面的幾道是關於記憶體空間的面試題,雖然不是特別的難,但比較扣細節你稍不注意就錯了,我的建議還是老老實實畫個記憶體圖再自信的給出正確答案吧。
第一題:
var a = 1 var b = a b = 2 請問 a 顯示是幾? 複製程式碼

上圖中可以看出,答案為:1。在棧記憶體中的資料發生複製行為時,系統會自動為新的變數分配一個新值。var b = a執行之後,a與b雖然值都等於1,但是他們其實已經是相互獨立互不影響的值了。
第二題:
var a = {name: 'a'} var b = a b = {name: 'b'} 請問現在 a.name 是多少? 複製程式碼

上圖中可以看出,答案為:"a"。因為b ={name:'b'}後相當於重新在堆記憶體中分配記憶體給物件{name:'b'},同時棧記憶體中變數b的指向地址也隨之變化,變數a不受影響。
第三題:
var a = {name: 'a'} var b = a b.name = 'b' 請問現在 a.name 是多少? 複製程式碼

上圖中可以看出,答案為:"b"。我們通過var b = a執行一次複製引用型別的操作。引用型別的複製同樣也會為新的變數自動分配一個新的值儲存在棧記憶體中,但不同的是,這個新的值,僅僅只是引用型別的一個地址指標。當地址指標相同時,儘管他們相互獨立,但是在堆記憶體中訪問到的具體物件實際上是同一個,因此b.name ='b'使堆記憶體中物件的value值變化,a.name的值也隨之變化。
第四題:
var a = {name: 'a'} var b = a b = null 請問現在 a 是什麼? 複製程式碼

上圖中可以看出,答案為:{name: "a"}。因為null為基本型別,存在棧記憶體當中。因此棧記憶體中的變數b由之前指向物件的一個地址轉變為null,變數a的地址還是指向原先的物件。
最後來個圖總結一下:

以上都是通過記憶體圖來解釋關於記憶體空間的知識,如有不合理的地方,希望指正一下~後續還會增加記憶體洩漏以及深淺拷貝的文章,敬請期待!
本人Github連結如下,歡迎各位Star