JavaScript進階-記憶體空間詳解(雙十一過後的一更)

LinDaiDai_霖呆呆發表於2019-11-17

前言

本章繼《JavaScript進階-執行上下文棧和變數物件(一週一更)》之後繼續深入學習JS的基礎知識.

上面我們已經介紹了很多關於JS中執行上下文以及變數物件的知識, 而現在我要講解的是JS的記憶體空間.

這一章你會學習到:

  • 三種資料結構: 堆(heap)、棧(stack)、佇列(queue)

  • 變數的存放

  • 記憶體空間管理

三種資料結構

JS中三種重要的資料結構, 如圖:

img1

(圖片來源前端九五六-Javascript 記憶體空間管理)

棧資料結構

其實在《JavaScript執行上下文》中我就已經提到了執行棧, 讓我們一起來回顧一下:

棧的特點: 後進先出(LIFO)的結構.

LIFO: last-in, first-out,類似於向乒乓球桶中放球,最先放入的球最後取出)

這裡還是貼上一張網圖方便大家理解的好:

img2

棧中的資料就像是一個個乒乓球, 最先進去的最後出來.

注⚠️

這裡所說的進棧出棧不是指賦值算進, 使用算出. 而是指賦值算進, 被清理算出, 而且位於同一函式作用域下的變數, 應該是在棧的同一層.

所謂的變數儲存於棧記憶體中的棧,傳統意義上說指的是由記憶體自動建立分配的空間,例如函式的引數值與區域性變數,只是其操作方式類似於棧操作,所以叫棧記憶體。

比如函式呼叫其實就相當於棧的形式:

例子?:

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格式的儲存是無序的, 所以沒有先後順序, 所以它是一種絕對公平的資料結構.

如圖所示:

img3

佇列資料結構

佇列資料結構不同於堆, 佇列是一種先進先出(FIFO) 的資料結構.

它也是事件迴圈(Event Loop) 的基礎結構.

如圖所示:

img4

最先進入佇列的任務最先出來, 類似於你排隊買票, 排在前面的人先買.

變數的存放

通過上面的介紹我們知道了, 記憶體中有堆了棧, 那麼JS變數具體是存放在哪裡呢?

  • 基本資料型別儲存在記憶體中;
  • 引用資料型別儲存在記憶體中.
  1. 基本資料型別6種: Undefined、Null、Boolean、Number、String、Symbol,(若是算上BigInt則有7種) 由於他們在記憶體中分別佔有固定大小的空間, 通過按值來訪問.
  2. 引用資料型別: 也就是Object物件, 它的儲存分為訪問地址實際存放的地方; 訪問地址是儲存在中的, 當查詢引用型別變數的時候, 會先從中讀取記憶體地址(也就是訪問地址), 然後再通過地址找到中的值.因此, 這種我們也把它叫為引用訪問.

一張圖方便你理解?

img5

在計算機的資料結構中,棧比堆的運算速度快,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.
  • 案例三中, 開始時, obj1obj2指向的都是同一堆記憶體物件{a: 1}, 在第三步將obj1賦值為null僅僅只是改變了棧中obj1的記憶體地址,將它變為了基本資料型別null, 並不會影響堆記憶體物件. 同樣的, 你要將obj1不賦值為null, 而是賦值為{b: 2}, 對obj2也還是沒有影響.

閉包中的變數

問❓: 是不是所有的變數都遵循: “基本資料型別儲存在棧中, 物件資料型別儲存在堆中” 的規律呢?

其實並不是完全正確的, 比如 閉包中的變數 就並不是儲存在棧中, 而是儲存於堆中.

因為如果變數存在棧中,那函式呼叫完棧頂空間銷燬,閉包變數就會被清除掉了, 那還談和閉包呢?

記憶體空間管理

在上面我們說了那麼多的棧記憶體, 堆記憶體, 那麼在JS中, 是怎樣管理這些記憶體空間的呢?

首先, 同樣的, 記憶體空間也是有屬於自己的生命週期, 它主要分為三個階段:

  1. 分配你所需的記憶體;
  2. 使用分配到的記憶體(讀、寫);
  3. 不需要的時候將其釋放、歸還.

我們可以用個例子來看一下看.

案例一?:

var a = 1; // 在記憶體中給數值變數分配空間
alart(a + 2); // 使用記憶體
a = null; // 使用完後, 釋放記憶體空間
複製程式碼

上面三步分別對應著三個階段. 當然, a = null這個操作是我們手動將a的記憶體空間釋放. 若沒有這個過程, JS會自己幫我做一些釋放記憶體的工作嗎? 答案當然是肯定的.

JS有自動垃圾收集機制, 聽著這個機制的名字我想大家就知道它是做什麼的了, 沒錯就是字面意思, 它會找出那些不再繼續使用的值,然後釋放其佔用的記憶體。垃圾收集器會每隔固定的時間段就執行一次釋放操作。

在自動垃圾收集機制中, 最常用的就是通過標記清除的演算法來找到哪些物件不再繼續使用. 其實上面我說的將a = null手動釋放記憶體其實是不準確的. 因為使用a = null僅僅只是做了一個釋放引用的操作, 讓a原本對應的值失去引用, 脫離執行環境, 這個值會在下一次垃圾收集器執行操作的時候被找到並釋放.

還有一點, 在區域性作用域中, 當函式執行完畢了之後, 區域性變數就沒有存在下去的必要了, 此時垃圾收集器知道這類變數是需要回收的, 所以很容易判斷.

但是全域性變數什麼時候需要釋放記憶體空間則很難判斷,因此在我們的開發中,原則上應該避免使用全域性變數。

後語

掌握好JS中的記憶體空間的基礎知識, 才能避免在實際開發中產生的一系列效能問題.

參考文章:

木易楊前端進階-JavaScript深入之記憶體空間詳細圖解

前端基礎進階(一):記憶體空間詳細圖解

前端九五六-Javascript 記憶體空間管理

關於js中 “棧空間的先進後出,後進先出” 的疑問?

知識無價, 支援原創

相關文章