JavaScript 是如何工做的:JavaScript 的內存模型

摘要: 從內存角度理解 let 和 const 的意義。javascript

Fundebug經受權轉載,版權歸原做者全部。html

這是專門探索 JavaScript 及其所構建的組件的系列文章的第 21 篇。前端

若是你錯過了前面的章節,能夠在這裏找到它們:java

  1. JavaScript 是如何工做的:引擎,運行時和調用堆棧的概述!
  2. JavaScript 是如何工做的:深刻 V8 引擎&編寫優化代碼的 5 個技巧!
  3. JavaScript 是如何工做的:內存管理+如何處理 4 個常見的內存泄漏!
  4. JavaScript 是如何工做的:事件循環和異步編程的崛起+ 5 種使用 async/await 更好地編碼方式!
  5. JavaScript 是如何工做的:深刻探索 websocket 和 HTTP/2 與 SSE +如何選擇正確的路徑!
  6. JavaScript 是如何工做的:與 WebAssembly 比較 及其使用場景!
  7. JavaScript 是如何工做的:Web Workers 的構建塊+ 5 個使用他們的場景!
  8. JavaScript 是如何工做的:Service Worker 的生命週期及使用場景!
  9. JavaScript 是如何工做的:Web 推送通知的機制!
  10. JavaScript 是如何工做的:使用 MutationObserver 跟蹤 DOM 的變化!
  11. JavaScript 是如何工做的:渲染引擎和優化其性能的技巧!
  12. JavaScript 是如何工做的:深刻網絡層 + 如何優化性能和安全!
  13. JavaScript 是如何工做的:CSS 和 JS 動畫底層原理及如何優化它們的性能!
  14. JavaScript 是如何工做的:解析、抽象語法樹(AST)+ 提高編譯速度 5 個技巧!
  15. JavaScript 是如何工做的:深刻類和繼承內部原理+Babel 和 TypeScript 之間轉換!
  16. JavaScript 是如何工做的:存儲引擎+如何選擇合適的存儲 API!
  17. JavaScript 是如何工做的:Shadow DOM 的內部結構+如何編寫獨立的組件!
  18. JavaScript 是如何工做的:WebRTC 和對等網絡的機制!
  19. JavaScript 是如何工做的:編寫本身的 Web 開發框架 + React 及其虛擬 DOM 原理!
  20. JavaScript 是如何工做的:模塊的構建以及對應的打包工具
// 聲明一些變量並初始化它們
var a = 5;
let b = "xy";
const c = true;

// 分配新值
a = 6;
b = b + "z";
c = false; //  類型錯誤:不可對常量賦值

做爲程序員,聲明變量、初始化變量(或不初始化變量)以及稍後爲它們分配新值是咱們天天都要作的事情。git

可是當這樣作的時候會發生什麼呢? JavaScript 如何在內部處理這些基本功能? 更重要的是,做爲程序員,理解 JavaScript 的底層細節對咱們有什麼好處。程序員

下面,我打算介紹如下內容:github

  • JS 原始數據類型的變量聲明和賦值
  • JavaScript 內存模型:調用堆棧和堆
  • JS 引用類型的變量聲明和賦值
  • let vs const

JS 原始數據類型的變量聲明和賦值

讓咱們從一個簡單的例子開始。下面,咱們聲明一個名爲myNumber的變量,並用值23初始化它。web

let myNumber = 23;

當執行此代碼時,JS 將執行:編程

  1. 爲變量(myNumber)建立惟一標識符(identifier)。
  2. 在內存中分配一個地址(在運行時分配)。
  3. 將值 23 存儲在分配的地址。

雖然咱們通俗地說,「myNumber 等於 23」,更專業地說,myNumber 等於保存值 23 的內存地址,這是一個值得理解的重要區別。小程序

若是咱們要建立一個名爲 newVar 的新變量並把 myNumber 賦值給它。

let newVar = myNumber;

由於 myNumber 在技術上實際是等於 「0012CCGWH80」,因此 newVar 也等於 「0012CCGWH80」,這是保存值爲23的內存地址。通俗地說就是 newVar 如今的值爲 23

由於 myNumber 等於內存地址 0012CCGWH80,因此將它賦值給 newVar 就等於將0012CCGWH80 賦值給 newVar

如今,若是我這樣作會發生什麼:

myNumber = myNumber + 1;

myNumber的值確定是 24。可是newVar的值是否也爲 24 呢?,由於它們指向相同的內存地址?

答案是否認的。因爲 JS 中的原始數據類型是不可變的,當 myNumber + 1 解析爲24時,JS 將在內存中分配一個新地址,將24做爲其值存儲,myNumber將指向新地址。

這是另外一個例子:

let myString = "abc";
myString = myString + "d";

雖然一個初級 JS 程序員可能會說,字母d只是簡單在原來存放adbc內存地址上的值,從技術上講,這是錯的。當 abcd 拼接時,由於字符串也是 JS 中的基本數據類型,不可變的,因此須要分配一個新的內存地址,abcd 存儲在這個新的內存地址中,myString 指向這個新的內存地址。

下一步是瞭解原始數據類型的內存分配位置。

JavaScript 內存模型:調用堆棧和堆

JS 內存模型能夠理解爲有兩個不一樣的區域:調用堆棧(call stack)堆(heap)

調用堆棧是存放原始數據類型的地方(除了函數調用以外)。上一節中聲明變量後調用堆棧的粗略表示以下。

在上圖中,我抽象出了內存地址以顯示每一個變量的值。 可是,不要忘記實際上變量指向內存地址,而後保存一個值。 這將是理解 let vs. const 一節的關鍵。

是存儲引用類型的地方。跟調用堆棧主要的區別在於,堆能夠存儲無序的數據,這些數據能夠動態地增加,很是適合數組和對象。

JS 引用類型的變量聲明和賦值

讓咱們從一個簡單的例子開始。下面,咱們聲明一個名爲myArray的變量,並用一個空數組初始化它。

let myArray = [];

當你聲明變量「myArray」併爲其指定非原始數據類型(如「[]」)時,如下是在內存中發生的狀況:

  1. 爲變量建立惟一標識符(「myArray」)
  2. 在內存中分配一個地址(將在運行時分配)
  3. 存儲在堆上分配的內存地址的值(將在運行時分配)
  4. 堆上的內存地址存儲分配的值(空數組[])

從這裏,咱們能夠 push, pop,或對數組作任何咱們想作的。

myArray.push("first");
myArray.push("second");
myArray.push("third");
myArray.push("fourth");
myArray.pop();

代碼部署後可能存在的 BUG 無法實時知道,過後爲了解決這些 BUG,花了大量的時間進行 log 調試,這邊順便給你們推薦一個好用的 BUG 監控工具 Fundebug

let vs const

通常來講,咱們應該儘量多地使用const,只有當咱們知道某個變量將發生改變時才使用let

讓咱們明確一下咱們所說的**「改變」**是什麼意思。

let sum = 0;
sum = 1 + 2 + 3 + 4 + 5;
let numbers = [];
numbers.push(1);
numbers.push(2);
numbers.push(3);
numbers.push(4);
numbers.push(5);

這個程序員使用let正確地聲明瞭sum,由於他們知道值會改變。可是,這個程序員使用let錯誤地聲明瞭數組 numbers ,由於他將把東西推入數組理解爲改變數組的值

解釋**「改變」**的正確方法是更改內存地址let 容許你更改內存地址。const 不容許你更改內存地址。

const importantID = 489;
importantID = 100; // 類型錯誤:賦值給常量變量

讓咱們想象一下這裏發生了什麼。

當聲明importantID時,分配了一個內存地址,並存儲489的值。記住,將變量importantID看做等於內存地址。

當將100分配給importantID時,由於100是一個原始數據類型,因此會分配一個新的內存地址,並將100的值存儲這裏。

而後 JS 嘗試將新的內存地址分配給 importantID,這就是拋出錯誤的地方,這也是咱們想要的行爲,由於咱們不想改變這個 importantID的值。

當你將100分配給importantID時,其實是在嘗試分配存儲100的新內存地址,這是不容許的,由於importantID是用const聲明的。

如上所述,假設的初級 JS 程序員使用let錯誤地聲明瞭他們的數組。相反,他們應該用const聲明它。這在一開始看起來可能使人困惑,我認可這一點也不直觀。

初學者會認爲數組只有在咱們能夠改變的狀況下才有用,const 使數組不可變,那麼爲何要使用它呢? 請記住:「改變」是指改變內存地址。讓咱們深刻探討一下爲何使用const聲明數組是徹底能夠的。

const myArray = [];

在聲明 myArray 時,將在調用堆棧上分配內存地址,該值是在堆上分配的內存地址。堆上存儲的值是實際的空數組。想象一下,它是這樣的:

若是咱們這麼作:

myArray.push(1);
myArray.push(2);
myArray.push(3);
myArray.push(4);
myArray.push(5);

執行 push 操做實際是將數字放入堆中存在的數組。而 myArray 的內存地址沒有改變。這就是爲何雖然使用const聲明瞭 myArray,但沒有拋出任何錯誤。

myArray 仍然等於 0458AFCZX91,它的值是另外一個內存地址22VVCX011,它在堆上有一個數組的值。

若是咱們這樣作,就會拋出一個錯誤:

myArray = 3;

因爲 3 是一個原始數據類型,所以生成一個新的調用堆棧上的內存地址,其值爲 3,而後咱們將嘗試將新的內存地址分配給 myArray,因爲 myArray 是用 const 聲明的,因此這是不容許的。

另外一個會拋出錯誤的例子:

myArray = ["a"];

因爲[a]是一個新的引用類型的數組,所以將分配調用堆棧上的一個新內存地址,並存儲上的一個內存地址的值,其它值爲 [a]。而後,咱們嘗試將調用堆棧內存地址分配給 myArray,這會拋出一個錯誤。

對於使用const聲明的對象(如數組),因爲對象是引用類型,所以能夠添加鍵,更新值等等。

const myObj = {};
myObj["newKey"] = "someValue"; // 這不會拋出錯誤

爲何這些知識對咱們有用呢

JavaScript 是世界上排名第一的編程語言(根據 GitHub 和 Stack Overflow 的年度開發人員調查)。 掌握併成爲「JS 忍者」是咱們全部人都渴望成爲的人。

任何質量好的的 JS 課程或書籍都提倡使用let, const 來代替 var,但他們並不必定說出緣由。 對於初學者來講,爲何某些 const 變量在「改變」其值時會拋出錯誤而其餘 const變量卻沒有。 對我來講這是有道理的,爲何這些程序員默認使用let處處避免麻煩。

可是,不建議這樣作。谷歌擁有世界上最好的一些程序員,在他們的 JavaScript 風格指南中說,使用 constlet 聲明全部本地變量。默認狀況下使用 const,除非須要從新分配變量,不使用 var 關鍵字(原文)。

雖然他們沒有明確說明緣由,但據我所知,有幾個緣由

  • 先發制人地限制將來的 bug。
  • 使用 const 聲明的變量必須在聲明時初始化,這迫使程序員常常在範圍方面更仔細地放置它們。這最終會致使更好的內存管理和性能。
  • 要經過代碼與任何可能遇到它的人交流,哪些變量是不可變的(就 JS 而言),哪些變量能夠從新分配。

但願上面的解釋能幫助你開始明白爲何或者何時應該在代碼中使用 letconst

關於Fundebug

Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟件、百姓網等衆多品牌企業。歡迎你們免費試用

版權聲明

轉載時請註明做者Fundebug以及本文地址: https://blog.fundebug.com/2019/04/15/javascritpt-memory-mechanism/

相關文章
相關標籤/搜索