JavaScript的記憶體模型

来源:https://www.cnblogs.com/tangshiwei/archive/2019/12/11/12020478.html
-Advertisement-
Play Games

引言 在我們的前端日常工作中,無時無刻不在進行著變數的聲明和賦值,你是否也曾碰到過變數聲明報錯或變數被污染的問題,如果你跟筆者一樣碰到過,那麼我們應該暫時停下來好好思考問題發生的原因以及如何採取相應的補救措施。當然排查問題最好的方式就是深入其底層細節,瞭解在JavaScript中的記憶體分配方式。只有 ...


引言

在我們的前端日常工作中,無時無刻不在進行著變數的聲明和賦值,你是否也曾碰到過變數聲明報錯或變數被污染的問題,如果你跟筆者一樣碰到過,那麼我們應該暫時停下來好好思考問題發生的原因以及如何採取相應的補救措施。當然排查問題最好的方式就是深入其底層細節,瞭解在JavaScript中的記憶體分配方式。只有我們對底層細節有一定的瞭解之後,才能輕而易舉地化解在寫代碼過程中遇到的各種問題。本文基於JavaScript的記憶體模型繼續衍生出letconst的差異性對比,若文中有錯誤的地方,還請指出。

1、記憶體是什麼

在講解JavaScript中的記憶體模型之前,我們先從硬體層面來簡單瞭解下記憶體是什麼。

記憶體是電腦中重要的部件之一,它是外存與CPU進行溝通的橋梁。電腦中所有程式的運行都是在記憶體中進行的,因此記憶體的性能對電腦的影響非常大。記憶體(Memory)也被稱為記憶體儲器主存儲器,其作用是用於暫時存放CPU中的運算數據,以及與硬碟等外部存儲器交換的數據。只要電腦在運行中,CPU就會把需要運算的數據調到記憶體中進行運算,當運算完成後CPU再將結果傳送出來,記憶體的運行也決定了電腦的穩定運行。

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

2、記憶體的生命周期

瞭解了記憶體的基本概念後,我們來簡單聊聊記憶體的生命周期。JavaScript作為一門高級編程語言,不像其他語言(例如C語言)中需要開發人員手動地去管理記憶體,系統會自動為你分配記憶體。但是無論是哪種編程語言,記憶體的生命周期都主要分為三個階段:

  • 分配記憶體:由操作系統來分配記憶體,供程式使用。在JavaScript中,這一步由操作系統來自動分配,無需開發人員手動操作。
  • 使用記憶體:程式獲得操作系統所分配的記憶體之後,在記憶體中發生讀和寫操作。
  • 釋放記憶體:程式使用完記憶體之後,會將這部分記憶體釋放出來供其他程式使用。在JavaScript中,這一步同樣不需要開發人員手動操作,由操作系統自動釋放。

我們知道,在JavaScript中的數據類型分為基本數據類型引用數據類型,其中基本數據類型包括StringNumberBooleanNullUndefined,ES6中新增的Symbol以及最新的BigInt,除了這些以外,其他的均為引用數據類型,例如ArrayDateFunctionRegExpErrorObject等。那麼這兩種數據類型的其中一個區別就是,基本數據類型的記憶體大小都是固定的,而引用數據類型的記憶體大小都是動態不固定的,可能會隨時發生變化。因此在記憶體分配階段這兩種數據類型會有一定的差異。

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

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

3、JavaScript中的記憶體分配

在我們的前端開發日常工作中,幾乎每天都在做著變數的聲明和賦值,這些變數最終都會被存放到記憶體中,所以我們還是有必要瞭解一下在JavaScript中的記憶體分配方式,這裡使用基本數據類型和引用數據類型來分別講述一下記憶體的分配過程,幫助我們理解JavaScript的底層細節。
首先我們從一個簡單的基本數據類型的賦值開始,代碼如下:

let num = 1;

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

  • 為變數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)。其中調用堆棧除了函數調用之外,主要用於存放基本數據類型的值,而引用數據類型的值一般都存放在記憶體堆中,堆中存放的數據都是無序的並且可以動態地增長,所以非常適合用於存儲數組和對象。

4、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聲明的變數必須在聲明時進行初始化,也方便了其他前端人員能一眼看出哪些變數是不可變的。

5、總結

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

6、交流

若覺得筆者的文章對你有幫助的話,不妨關註下筆者的公眾號,每周都會原創和整理一些前端技術乾貨,關註公眾號後可以邀你入群,我們一起交流前端,相互學習,共同進步。

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

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

逆境中成長,只有不斷地學習,才能成為更好的自己,與君共勉!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 模擬百度搜索框 我的思路整理: 1. 註冊文本框抬起事件(onkeyup) 2. 處理函數: >創建臨時數組,迴圈遍歷文本框鍵入的文字內容和keywords數組,用keyWords[i].indexOf(text) == 0 來判斷,true就追加進臨時數組tempArr.push(keyWords ...
  • reselect是什麼? "reselect" 是配合 使用的一款輕量型的狀態選擇庫,目的在於當store中的state重新改變之後,使得局部未改變的狀態不會因為整體的state變化而全部重新渲染,功能有點類似於組件中的生命周期函數 ,但是它們並不是一個東西。下麵是官方的一些簡介: Selector ...
  • nvm Node.js version manager ,推薦使用它來安裝 node.js 。 "Mac 版項目地址" "Windows 版項目地址" windows 版安裝 進入 "下載頁" 找到安裝包: 跟著引導安裝即可。 輸入 查看是否安裝成功,成功的話如下圖所示。如果提示找不到命令, "請看 ...
  • 為同一個元素綁定多個不同事件指向同一個事件處理函數 1. 用了switch(e.type){} 來修改 2. break <input type="button" value="小蘇" id="btn" /> <script src="common.js"></script> <script> // ...
  • div拖拽效果 JQuery ...
  • 如何渲染幾萬條數據並不卡住界面? 如何在不卡住頁面的情況下渲染數據,也就是說不能一次性將幾萬條 都渲染出來,而應該一次渲染部分 DOM,那麼就可以通過 requestAnimationFrame 來 每 16 ms 刷新一次。 <!DOCTYPE html> <html lang="en"> <he ...
  • 事件冒泡: 多個元素嵌套, 有層次關係 ,這些元素都註冊了相同的事件, 如果裡面的元素的事件觸發了, 外面的元素的該事件自動的觸發了 事件有三個階段: 1.事件捕獲階段 :從外向內 2.事件目標階段 :最開始選擇的那個 3.事件冒泡階段 : 從裡向外 為元素綁定事件 addEventListener ...
  • zoning 中華人民共和國行政區劃:省級、地級、縣級、鄉級和村級 GitHub: Gitee: Demo: 來源 國家統計局 統計用區劃和城鄉劃分代碼 統計數據截止2018 10 31 於 2019 01 31發佈 使用 打開頁面 打開瀏覽器控制台(推薦Chrome、Firefox,請不要用IE系 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:本文代碼示例演示瞭如何在WPF中使用LiveCharts庫創建動態條形圖。通過創建數據模型、ViewModel和在XAML中使用`CartesianChart`控制項,你可以輕鬆實現圖表的數據綁定和動態更新。我將通過清晰的步驟指南包括詳細的中文註釋,幫助你快速理解並應用這一功能。 先上效果: 在 ...
  • openGauss(GaussDB ) openGauss是一款全面友好開放,攜手伙伴共同打造的企業級開源關係型資料庫。openGauss採用木蘭寬鬆許可證v2發行,提供面向多核架構的極致性能、全鏈路的業務、數據安全、基於AI的調優和高效運維的能力。openGauss深度融合華為在資料庫領域多年的研 ...
  • openGauss(GaussDB ) openGauss是一款全面友好開放,攜手伙伴共同打造的企業級開源關係型資料庫。openGauss採用木蘭寬鬆許可證v2發行,提供面向多核架構的極致性能、全鏈路的業務、數據安全、基於AI的調優和高效運維的能力。openGauss深度融合華為在資料庫領域多年的研 ...
  • 概述:本示例演示了在WPF應用程式中實現多語言支持的詳細步驟。通過資源字典和數據綁定,以及使用語言管理器類,應用程式能夠在運行時動態切換語言。這種方法使得多語言支持更加靈活,便於維護,同時提供清晰的代碼結構。 在WPF中實現多語言的一種常見方法是使用資源字典和數據綁定。以下是一個詳細的步驟和示例源代 ...
  • 描述(做一個簡單的記錄): 事件(event)的本質是一個委托;(聲明一個事件: public event TestDelegate eventTest;) 委托(delegate)可以理解為一個符合某種簽名的方法類型;比如:TestDelegate委托的返回數據類型為string,參數為 int和 ...
  • 1、AOT適合場景 Aot適合工具類型的項目使用,優點禁止反編 ,第一次啟動快,業務型項目或者反射多的項目不適合用AOT AOT更新記錄: 實實在在經過實踐的AOT ORM 5.1.4.117 +支持AOT 5.1.4.123 +支持CodeFirst和非同步方法 5.1.4.129-preview1 ...
  • 總說周知,UWP 是運行在沙盒裡面的,所有許可權都有嚴格限制,和沙盒外交互也需要特殊的通道,所以從根本杜絕了 UWP 毒瘤的存在。但是實際上 UWP 只是一個應用模型,本身是沒有什麼許可權管理的,許可權管理全靠 App Container 沙盒控制,如果我們脫離了這個沙盒,UWP 就會放飛自我了。那麼有沒... ...
  • 目錄條款17:讓介面容易被正確使用,不易被誤用(Make interfaces easy to use correctly and hard to use incorrectly)限制類型和值規定能做和不能做的事提供行為一致的介面條款19:設計class猶如設計type(Treat class de ...
  • title: 從零開始:Django項目的創建與配置指南 date: 2024/5/2 18:29:33 updated: 2024/5/2 18:29:33 categories: 後端開發 tags: Django WebDev Python ORM Security Deployment Op ...
  • 1、BOM對象 BOM:Broswer object model,即瀏覽器提供我們開發者在javascript用於操作瀏覽器的對象。 1.1、window對象 視窗方法 // BOM Browser object model 瀏覽器對象模型 // js中最大的一個對象.整個瀏覽器視窗出現的所有東西都 ...