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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...