JavaScript的內存模型

引言

在咱們的前端平常工做中,無時無刻不在進行着變量的聲明和賦值,你是否也曾碰到過變量聲明報錯或變量被污染的問題,若是你跟筆者同樣碰到過,那麼咱們應該暫時停下來好好思考問題發生的緣由以及如何採起相應的補救措施。固然排查問題最好的方式就是深刻其底層細節,瞭解在JavaScript中的內存分配方式。只有咱們對底層細節有必定的瞭解以後,才能垂手可得地化解在寫代碼過程當中遇到的各類問題。本文基於JavaScript的內存模型繼續衍生出letconst的差別性對比,若文中有錯誤的地方,還請指出。javascript

一、內存是什麼

在講解JavaScript中的內存模型以前,咱們先從硬件層面來簡單瞭解下內存是什麼。前端

內存是計算機中重要的部件之一,它是外存與CPU進行溝通的橋樑。計算機中全部程序的運行都是在內存中進行的,所以內存的性能對計算機的影響很是大。內存(Memory)也被稱爲內存儲器主存儲器,其做用是用於暫時存放CPU中的運算數據,以及與硬盤等外部存儲器交換的數據。只要計算機在運行中,CPU就會把須要運算的數據調到內存中進行運算,當運算完成後CPU再將結果傳送出來,內存的運行也決定了計算機的穩定運行。java

內存條是計算機組成結構中的關鍵部分,其自己是一個很是精密的部件,內部包含了上億個電子元器件,它們很小,達到了納米級別。這些元器件,實際上也就是電路,電路的電壓會發生變化,但只有兩種可能,要麼0V(低電平),要麼5V(高電平),0V是斷電,用0來表示,5V是通電,用1來表示,所以一個元器件包含了兩個狀態0和1,即表示一位(bit)。可是做爲人類,咱們並不擅長使用bit來思考和計算,所以咱們會將它們劃分紅更大的組,例如8位表示1個byte(字節),16位表示2個byte(字節),32位表示4個byte(字節)。有不少東西都是存儲在內存中的,好比咱們的程序代碼,程序中所聲明的變量以及操做系統的代碼等。git

二、內存的生命週期

瞭解了內存的基本概念後,咱們來簡單聊聊內存的生命週期。JavaScript做爲一門高級編程語言,不像其餘語言(例如C語言)中須要開發人員手動地去管理內存,系統會自動爲你分配內存。可是不管是哪一種編程語言,內存的生命週期都主要分爲三個階段:github

  • 分配內存:由操做系統來分配內存,供程序使用。在JavaScript中,這一步由操做系統來自動分配,無需開發人員手動操做。
  • 使用內存:程序得到操做系統所分配的內存以後,在內存中發生讀和寫操做。
  • 釋放內存:程序使用完內存以後,會將這部份內存釋放出來供其餘程序使用。在JavaScript中,這一步一樣不須要開發人員手動操做,由操做系統自動釋放。

咱們知道,在JavaScript中的數據類型分爲基本數據類型引用數據類型,其中基本數據類型包括StringNumberBooleanNullUndefined,ES6中新增的Symbol以及最新的BigInt,除了這些之外,其餘的均爲引用數據類型,例如ArrayDateFunctionRegExpErrorObject等。那麼這兩種數據類型的其中一個區別就是,基本數據類型的內存大小都是固定的,而引用數據類型的內存大小都是動態不固定的,可能會隨時發生變化。所以在內存分配階段這兩種數據類型會有必定的差別。編程

編譯器在編譯代碼時,對於基本數據類型,因爲其空間大小固定,編譯器在檢查時會提早計算它們須要的內存大小,並插入與操做系統交互的代碼,向操做系統申請存儲變量所需的堆棧字節數,而後將申請到的內存分配給調用堆棧中的程序,稱爲靜態內存分配。例如在調用函數時,函數中的變量所需的內存會被添加到現有的內存之上,當函數執行完畢後,這部份內存又會之後進先出(LIFO)的順序被移除。可是對於引用數據類型,其空間大小是動態的,在編譯階段沒法直接肯定其須要多少內存,所以不能在堆棧上爲其分配內存,相反,須要在運行時向操做系統申請適當的內存,而且這部份內存是在堆空間進行分配的,稱爲動態內存分配。靜態內存分配和動態內存分配的區別以下表所示:數組

靜態內存分配 動態內存分配
編譯階段可肯定大小 編譯階段沒法肯定大小
在編譯時執行 在運行時執行
分配給堆棧 分配給堆
順序分配,後進先出(LIFO) 無序分配

三、JavaScript中的內存分配

在咱們的前端開發平常工做中,幾乎天天都在作着變量的聲明和賦值,這些變量最終都會被存放到內存中,因此咱們仍是有必要了解一下在JavaScript中的內存分配方式,這裏使用基本數據類型和引用數據類型來分別講述一下內存的分配過程,幫助咱們理解JavaScript的底層細節。
首先咱們從一個簡單的基本數據類型的賦值開始,代碼以下:編程語言

let num = 1;

當JavaScript引擎在執行到這行代碼時,會執行以下操做:ide

  • 爲變量num建立一個惟一標識符(identifier),該標識符用於與棧內存中的地址A1造成映射關係。
  • 在棧內存中爲其分配一個地址A1
  • 將值1存儲到分配的地址。

示例圖以下:

一般咱們說num變量的值等於1,但其實嚴格意義上來說,num變量的值等於棧內存中存放對應值的內存地址(如圖中的A1)。接下來咱們建立一個新的變量newNum並將num賦值給它:函數

let newNum = num;

通過以上賦值以後,一般說newNum的值爲1,一樣從嚴格意義上來說的話是指newNumnum指向同一個內存地址A1,以下圖所示:

若是接下來咱們執行如下操做,看會發生什麼:

num = num + 1;

咱們對num變量進行自增加,很顯然num變量的值爲2。因爲newNumnum指向同一個內存地址A1,那麼此時newNum的值是否也爲2呢,在回答這個問題以前,咱們先來看一下當前內存地址發生的變化:

在上圖中咱們能夠發現,num變量的內存地址發生了改變,由原來的A1變爲A2,這是由於在JS中的基本數據類型都是不可變的,一旦修改,只會爲其分配新的內存地址並將修改後的新值存入到新的地址中,所以回答上面的那個問題,newNum的值保持不變,依舊爲1,由於它的內存地址沒有發生改變。再看以下示例:

let str = 'ab';
str = str + 'c';

由於字符串也是屬於基本數據類型,基本數據類型都是不可變的,因此即便上述代碼中只是簡單的將c拼接到了原來的字符串ab後面,可是依舊會爲其分配新的內存地址,變量str最終會指向這個新的內存地址,以下圖所示:

瞭解了基本數據類型的內存分配方式以後,接下來咱們來了解下引用數據類型的內存分配方式。一樣咱們從一個簡單的引用數據類型的賦值開始:

let arr = [];

當JavaScript引擎在執行到這行代碼時,會執行以下操做:

  • 爲變量arr建立一個惟一標識符(identifier),該標識符用於與棧內存中的地址A3造成映射關係。
  • 在棧內存中爲其分配一個地址A3
  • 棧內存中存儲在堆中分配的內存地址的值H1
  • 在堆中存儲分配的值空數組[]

示例圖以下:

在JavaScript引擎(例如Chrome和Node的V8引擎)中主要是由兩個部件組成,一個叫內存堆(Memory Heap),一個叫調用堆棧(Call Stack)。其中調用堆棧除了函數調用以外,主要用於存放基本數據類型的值,而引用數據類型的值通常都存放在內存堆中,堆中存放的數據都是無序的而且能夠動態地增加,因此很是適合用於存儲數組和對象。

四、letconst的差別性對比

在瞭解完以上兩種數據類型的內存分配方式後,咱們這裏對letconst的使用方式進行一下對比,一般來講,咱們建議在寫代碼的過程當中能使用const的地方儘可能減小使用let,這樣能夠在某種程度上避免變量被無故修改而引起的一系列問題。以下代碼:

let num = 1;
num = num + 1;
let arr = [];
arr.push(1);
arr.push(2);
arr.push(3);

在上述代碼中,變量num由於使用let的方式聲明,因此容許其被修改,由於基本類型的值是不可變的,因此會爲num變量分配新的內存地址。對於arr變量,這裏一樣使用let方式進行聲明,表示容許其修改,可是對於push操做其實並無修改arr變量的內存地址,只是將新的值推入了堆內存的數組中,因此此處建議修改成使用const進行聲明。

筆者的觀點是:將修改理解爲修改內存地址,若容許修改內存地址,則使用let進行聲明,不然使用const進行聲明。

以下示例:

const num = 1;
num = num + 1;

由在上一小節中瞭解到的基本數據類型的內存分配方式,咱們知道爲變量num在棧內存中分配了一個地址來保存對應的值。

可是這裏咱們是使用const的方式來進行聲明的,當咱們從新爲變量num進行賦值時,JS嘗試爲其分配新的內存地址,那麼這裏也就是拋出錯誤的地方,由於咱們明確不容許對其進行修改。

所以在控制檯中咱們會看到對應的報錯信息。

再看以下示例:

const arr = [];

對於引用數據類型,咱們知道會在棧內存上爲其分配內存地址,存儲的是堆中的內存地址的值。

咱們作以下操做:

arr.push(1);
arr.push(2);
arr.push(3);


執行push操做其實是將新值推入堆中的數組,內存地址並無發生改變。這也就是爲何雖然使用const聲明變量,可是依舊沒有報錯的緣由。可是若是咱們使用以下方式:

arr = 1;
arr = undefined;
arr = null;
arr = [];
arr = {};

這些方式都會修改原數組的內存地址,const聲明是不容許修改內存地址的,因此很明顯會拋出錯誤。所以這裏也是建議默認狀況下使用const聲明變量,除非須要修改內存地址,const聲明的變量必須在聲明時進行初始化,也方便了其餘前端人員能一眼看出哪些變量是不可變的。

五、總結

在本篇中主要總結了一下JavaScript中的內存模型,並針對基本數據類型和引用數據類型分別講述了其在JavaScript中的內存分配方式,而後對letconst這兩種在代碼中的變量聲明方式進行對比以瞭解其中的差別性,下篇基於內存模型繼續講解JavaScript引擎中的垃圾回收機制以及在寫代碼過程當中的幾種有效避免內存泄漏的方式,和你們一塊兒瞭解JavaScript的底層細節。

六、交流

若以爲筆者的文章對你有幫助的話,不妨關注下筆者的公衆號,每週都會原創和整理一些前端技術乾貨,關注公衆號後能夠邀你入羣,咱們一塊兒交流前端,相互學習,共同進步。

文章已同步更新至Github博客,若覺文章尚可,歡迎前往star!

你的一個點贊,值得讓我付出更多的努力!

逆境中成長,只有不斷地學習,才能成爲更好的本身,與君共勉!

相關文章
相關標籤/搜索