JS中的棧記憶體、堆記憶體

reslicma發表於2019-02-23

淺談JS中的堆疊

寫在正文之前:我個人寫部落格的靈感大部分源於我最近學到了什麼新的或者好玩的東西,寫下來加深記憶並且和大家分享,還有就是源於某些偏原理的東西,我喜歡去和大家分享和討論某些東西實現的原理,而不是它具體的實現,實現的方法太多了,原理雖有區別,但基本上大同小異。

引言:我們都熟知並且常用JS變數的宣告以及初始化(賦值),比如一行極其簡單的程式碼var str = '我是字串',那麼這行程式碼執行的時候發生了什麼呢?再比如var obj = {name: 'reslicma'}又發生了什麼?他們一樣嗎?請看正文。

JS中變數的型別

我們先行討論JS中變數的型別,因為JS中變數的具體儲存方式是取決於這個變數的型別的。JS中的變數共有兩大類:基本資料型別引用資料型別,我們下文說基本型就是基本資料型別,引用型就是引用資料型別。

基本資料型別(簡單資料型別)

JS中的基本型共有五種:string,number,Boolean,undefined,null。分別對應:字串型別,數字型別,布林型別,undefined(變數宣告未初始化),null(空物件或理解為空指標)。

引用資料型別

JS中的引用型:Array,Function,Object。但是實際上就是一種:Object型,沒錯,就是物件,畢竟Array,Function也是物件。JS一切皆物件這句話並不為過······

棧記憶體和堆記憶體

介紹完了基本型和引用型就可以真正的進入正題了。我們知道宣告一個變數並且給它賦值這樣的操作對於這兩種型別而言沒什麼區別,但是對這兩種型別的具體的操作卻大不相同。 在JS中,棧記憶體用於儲存基本型的變數值,堆記憶體用於儲存引用型的值。這是為什麼呢?因為JS這門語言和其他語言有一個不同之處:不允許直接訪問記憶體的位置,也就是說不能直接操作物件的記憶體空間,操作的是物件的引用而已,那麼引用可以理解為是一個指標,是一個具體的堆記憶體的地址。我寫下這幾行程式碼,並且附上一張圖,讓我們來詳細的看一下:

var str = `我是字串`,
    num = 1,
    bl = true,
    nu = null,
    un = undefined,
    obj = {
        name: 'reslicma'
    }
複製程式碼

在記憶體中就發生瞭如下圖這樣的事情:

堆疊記憶體圖
我們接下來詳細的分析它: 首先,我定義的前五個變數都是基本型,那麼他們都是儲存在棧記憶體當中,並且他們儲存的就是值本身,所以說訪問基本型變數就能訪問得到。而obj這個變數是個引用型,所以它在棧記憶體中只儲存了一個指標(或者理解為一個地址),比如上圖中的這個地址,那麼這個地址指向了堆記憶體中的一塊記憶體空間,這個空間才是真正儲存了這個obj物件的記憶體空間。

理解棧記憶體和堆記憶體

我們來說一下棧記憶體和堆記憶體具體的區別和聯絡。

棧記憶體就像一個線性的、規則的、大小基本固定的、有序的排列起來的一塊塊記憶體空間,就像我上圖畫的那樣,每個單元大小固定,規則有序的排列下來,就是棧。所以,在定義一個基本型變數的時候,發生的事情如下:向棧記憶體申請(注意是申請)一塊空間,然後把你宣告的變數名和這個變數的具體的值本身壓入這個申請好的小空間內。

堆記憶體就像一個不規則的、大小不固定的、無序的一塊塊記憶體空間,像上圖中我畫的堆記憶體圖中,大小不固定,並且每一塊堆記憶體都有一個自己的地址(指標),用來操作它們。這個地址很有意思,在你定義一個引用型變數的時候,向堆記憶體申請一塊空間,用於儲存這個具體的引用型的值(物件),同時JS會隨機分配給這塊堆記憶體的小空間一個地址,然後,把這個變數名和這個地址壓入申請好的棧空間內。這裡就是我詳細說明的地方,其實上圖就很形象的表達了這個流程。

棧記憶體和堆記憶體的優缺點

那麼為什麼JS要這樣區分棧記憶體和堆記憶體呢?在JS中,這些基本型變數大小固定,並且操作容易簡單,所以把它們放入棧中儲存。引用型變數大小不固定,所以把它們分配給堆中,讓他們申請空間的時候自己確定大小,這樣把它們分開儲存能夠使得程式執行起來佔用的記憶體最小。

棧記憶體由於它的特點,所以它的系統效率較高,堆記憶體需要分配空間和地址,還要把地址存到棧中,所以效率低於棧。

棧記憶體和堆記憶體的垃圾回收

我們知道JS是有垃圾回收制的(不詳細說),棧記憶體中基本型一般在它的當前執行環境結束就會被銷燬被垃圾回收制回收,而引用型別不會,因為不確定其他的地方是不是還有一些對它的引用,所以引用型只有在所有對它的引用都結束的時候才會被回收掉。

加深理解

注意:在JS中訪問變數時,是把變數名作為索引來尋找值的,無論是基本型還是引用型。也就是說,訪問變數的過程就是:通過變數名找到棧記憶體中儲存的具體的值,如果是基本型,直接就返回值,如果是引用型,返回一個指向堆記憶體的地址

案例1:基本型的複製:

var num1 = 1
var num2 = num1
// 修改num1的值
num1 = 2
console.log(num2) // 還是1,不會改變
複製程式碼

解析具體過程:首先在棧記憶體中壓入一個變數名為num1、值為1的一個變數。然後,第二行程式碼:賦值操作,先執行賦值運算子右邊的式子,所以通過變數名找到了num1的值1,然後把這個值1返回並且賦值給了num2這個變數,所以棧記憶體中就又壓入了一個變數名為num2、值為1的變數,這兩個變數的值1相等但不是同一個。所以,改變num1的值就只改變了num1棧記憶體中的值,對num2沒有任何影響。圖解:

基本型複製圖
案例2:引用型的複製:

var obj1 = {name: 'reslicma'}
var obj2 = obj1
obj1.name = '我被修改了'
console.log(obj2.name) // 我被修改了
複製程式碼

我們前面已經說過了,無論是基本型還是引用型都是按變數名訪問棧記憶體中的值,所以,第二句程式碼執行的時候,就相當於把通過obj1找到的那個記憶體地址賦值給了obj2這個變數,所以這兩個變數在棧記憶體中都是儲存的同一個地址、指標,指向的是同一塊堆記憶體中的空間,我修改了obj1的內容,那麼由於obj2也是指向這個內容,所以obj2的內容也會隨之改變。

記憶體圖解:

基本型複製圖
基本型複製圖

相關文章