前言
本章繼《JavaScript進階-執行上下文棧和變數物件(一週一更)》之後繼續深入學習JS
的基礎知識.
上面我們已經介紹了很多關於JS
中執行上下文以及變數物件的知識, 而現在我要講解的是JS
的記憶體空間.
這一章你會學習到:
-
三種資料結構: 堆(heap)、棧(stack)、佇列(queue)
-
變數的存放
-
記憶體空間管理
三種資料結構
JS
中三種重要的資料結構, 如圖:
(圖片來源前端九五六-Javascript 記憶體空間管理)
棧資料結構
其實在《JavaScript執行上下文》中我就已經提到了執行棧, 讓我們一起來回顧一下:
棧的特點: 後進先出(LIFO)的結構.
(LIFO
: last-in, first-out
,類似於向乒乓球桶中放球,最先放入的球最後取出)
這裡還是貼上一張網圖方便大家理解的好:
棧中的資料就像是一個個乒乓球, 最先進去的最後出來.
注⚠️
這裡所說的進棧和出棧不是指賦值算進, 使用算出. 而是指賦值算進, 被清理算出, 而且位於同一函式作用域下的變數, 應該是在棧的同一層.
所謂的變數儲存於棧記憶體中的棧,傳統意義上說指的是由記憶體自動建立分配的空間,例如函式的引數值與區域性變數,只是其操作方式類似於棧操作,所以叫棧記憶體。
比如函式呼叫其實就相當於棧的形式:
例子?:
function fn1() {
console.log(1)
fn2()
}
function fn2() {
console.log(2)
fn3()
}
function fn3() {
console.log(3)
}
fn1()
複製程式碼
如上, 宣告的順序是1, 2, 3
, 但是釋放的順序是為3, 2, 1
.
這裡釋放按照這個順序是因為 3
最先執行完, 所以最先被釋放.
堆資料結構
一種樹狀結構。好比 JSON
格式中的資料,你有 key
,我有對應的 value
, 就立馬返給你。
因為我們知道JSON
格式的儲存是無序的, 所以沒有先後順序, 所以它是一種絕對公平的資料結構.
如圖所示:
佇列資料結構
佇列資料結構不同於堆, 佇列是一種先進先出(FIFO) 的資料結構.
它也是事件迴圈(Event Loop) 的基礎結構.
如圖所示:
最先進入佇列的任務最先出來, 類似於你排隊買票, 排在前面的人先買.
變數的存放
通過上面的介紹我們知道了, 記憶體中有堆了棧, 那麼JS
變數具體是存放在哪裡呢?
- 基本資料型別儲存在棧記憶體中;
- 引用資料型別儲存在堆記憶體中.
- 基本資料型別6種:
Undefined、Null、Boolean、Number、String、Symbol
,(若是算上BigInt
則有7種) 由於他們在記憶體中分別佔有固定大小的空間, 通過按值來訪問. - 引用資料型別: 也就是
Object
物件, 它的儲存分為訪問地址和實際存放的地方; 訪問地址是儲存在棧中的, 當查詢引用型別變數的時候, 會先從棧中讀取記憶體地址(也就是訪問地址), 然後再通過地址找到堆中的值.因此, 這種我們也把它叫為引用訪問.
一張圖方便你理解?
在計算機的資料結構中,棧比堆的運算速度快,Object是一個複雜的結構且可以擴充套件:陣列可擴充,物件可新增屬性,都可以增刪改查。將他們放在堆中是為了不影響棧的效率。而是通過引用的方式查詢到堆中的實際物件再進行操作。所以查詢引用型別值的時候先去棧查詢再去堆查詢。
變數存放案例
要是你讀完了上面的堆疊儲存介紹還有點模糊的話, 我們不妨來看幾個案例.
案例一?:
var a = 1;
var b = a;
b = 2;
console.log(a); // a = ?
複製程式碼
案例二?:
var obj1 = { a: 1, b: 2 };
var obj2 = obj1;
obj2.a = 3;
console.log(obj1.a); // obj1.a = ?
複製程式碼
案例三?:
var obj1 = { a: 1 };
var obj2 = obj1;
obj1 = null;
console.log(obj2); // obj2 = ?
複製程式碼
上面三個案例的答案分別對應的是: 1、3、{ a: 1 }
.
- 案例一中,
a和b
都是基本資料型別, 它們的值分別儲存在各自獨立的棧空間中, 是互不影響的, 所以修改了b
的值後a
還是不變.var b = a
的操作, 你可以理解為單純的b
賦值了值1
, 而後a和b
沒有任何關係了. - 案例二中, 建立
obj1
的時候, 在棧中儲存了一個名為obj1
的變數, 同時開闢了一個堆記憶體用於存放了{a: 1, b: 2}
物件,obj1
中存放的就是指向這個堆記憶體物件的地址. 因此obj2
進行賦值的時候拷貝的只是obj1
中的地址, 實際上它們指向的都是堆記憶體的物件.在第三步改變這個物件的值的時候, 也相當於同時改變了obj1
. - 案例三中, 開始時,
obj1
和obj2
指向的都是同一堆記憶體物件{a: 1}
, 在第三步將obj1
賦值為null
僅僅只是改變了棧中obj1
的記憶體地址,將它變為了基本資料型別null
, 並不會影響堆記憶體物件. 同樣的, 你要將obj1
不賦值為null
, 而是賦值為{b: 2}
, 對obj2
也還是沒有影響.
閉包中的變數
問❓: 是不是所有的變數都遵循: “基本資料型別儲存在棧中, 物件資料型別儲存在堆中” 的規律呢?
其實並不是完全正確的, 比如 閉包中的變數 就並不是儲存在棧中, 而是儲存於堆中.
因為如果變數存在棧中,那函式呼叫完棧頂空間銷燬,閉包變數就會被清除掉了, 那還談和閉包呢?
記憶體空間管理
在上面我們說了那麼多的棧記憶體, 堆記憶體, 那麼在JS
中, 是怎樣管理這些記憶體空間的呢?
首先, 同樣的, 記憶體空間也是有屬於自己的生命週期, 它主要分為三個階段:
- 分配你所需的記憶體;
- 使用分配到的記憶體(讀、寫);
- 不需要的時候將其釋放、歸還.
我們可以用個例子來看一下看.
案例一?:
var a = 1; // 在記憶體中給數值變數分配空間
alart(a + 2); // 使用記憶體
a = null; // 使用完後, 釋放記憶體空間
複製程式碼
上面三步分別對應著三個階段. 當然, a = null
這個操作是我們手動將a
的記憶體空間釋放. 若沒有這個過程, JS
會自己幫我做一些釋放記憶體的工作嗎? 答案當然是肯定的.
JS
有自動垃圾收集機制, 聽著這個機制的名字我想大家就知道它是做什麼的了, 沒錯就是字面意思, 它會找出那些不再繼續使用的值,然後釋放其佔用的記憶體。垃圾收集器會每隔固定的時間段就執行一次釋放操作。
在自動垃圾收集機制中, 最常用的就是通過標記清除的演算法來找到哪些物件不再繼續使用. 其實上面我說的將a = null
手動釋放記憶體其實是不準確的. 因為使用a = null
僅僅只是做了一個釋放引用的操作, 讓a
原本對應的值失去引用, 脫離執行環境, 這個值會在下一次垃圾收集器執行操作的時候被找到並釋放.
還有一點, 在區域性作用域中, 當函式執行完畢了之後, 區域性變數就沒有存在下去的必要了, 此時垃圾收集器知道這類變數是需要回收的, 所以很容易判斷.
但是全域性變數什麼時候需要釋放記憶體空間則很難判斷,因此在我們的開發中,原則上應該避免使用全域性變數。
後語
掌握好JS
中的記憶體空間的基礎知識, 才能避免在實際開發中產生的一系列效能問題.
參考文章:
木易楊前端進階-JavaScript深入之記憶體空間詳細圖解
知識無價, 支援原創