天天看點

JavaScript 是如何工作的:JavaScript 的記憶體模型

摘要: 從記憶體角度了解 let 和 const 的意義。

Fundebug

經授權轉載,版權歸原作者所有。

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

如果你錯過了前面的章節,可以在這裡找到它們:

  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; //  類型錯誤:不可對常量指派           

作為程式員,聲明變量、初始化變量(或不初始化變量)以及稍後為它們配置設定新值是我們每天都要做的事情。

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

下面,我打算介紹以下内容:

  • JS 原始資料類型的變量聲明和指派
  • JavaScript 記憶體模型:調用堆棧和堆
  • JS 引用類型的變量聲明和指派
  • let

    vs

    const

讓我們從一個簡單的例子開始。下面,我們聲明一個名為

myNumber

的變量,并用值

23

初始化它。

let myNumber = 23;           

當執行此代碼時,JS 将執行:

  1. 為變量(

    myNumber

    )建立唯一辨別符(identifier)。
  2. 在記憶體中配置設定一個位址(在運作時配置設定)。
  3. 将值

    23

    存儲在配置設定的位址。
JavaScript 是如何工作的:JavaScript 的記憶體模型

雖然我們通俗地說,

“myNumber 等于 23”

,更專業地說,

myNumber

等于儲存值 23 的記憶體位址,這是一個值得了解的重要差別。

如果我們要建立一個名為

newVar

的新變量并把

myNumber

指派給它。

let newVar = myNumber;           

因為

myNumber

在技術上實際是等于 “

0012CCGWH80

”,是以

newVar

也等于 “

0012CCGWH80

”,這是儲存值為

23

的記憶體位址。通俗地說就是

newVar

現在的值為

23

JavaScript 是如何工作的:JavaScript 的記憶體模型

myNumber

等于記憶體位址

0012CCGWH80

,是以将它指派給

newVar

就等于将

0012CCGWH80

指派給

newVar

現在,如果我這樣做會發生什麼:

myNumber = myNumber + 1;           

myNumber

的值肯定是 24。但是

newVar

的值是否也為 24 呢?,因為它們指向相同的記憶體位址?

答案是否定的。由于 JS 中的原始資料類型是不可變的,當

myNumber + 1

解析為

24

時,JS 将在記憶體中配置設定一個新位址,将

24

作為其值存儲,

myNumber

将指向新位址。

JavaScript 是如何工作的:JavaScript 的記憶體模型

這是另一個例子:

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

雖然一個初級 JS 程式員可能會說,字母

d

隻是簡單在原來存放

adbc

記憶體位址上的值,從技術上講,這是錯的。當

abc

d

拼接時,因為字元串也是 JS 中的基本資料類型,不可變的,是以需要配置設定一個新的記憶體位址,

abcd

存儲在這個新的記憶體位址中,

myString

指向這個新的記憶體位址。

JavaScript 是如何工作的:JavaScript 的記憶體模型

下一步是了解原始資料類型的記憶體配置設定位置。

JS 記憶體模型可以了解為有兩個不同的區域:調用堆棧(call stack)和堆(heap)。

JavaScript 是如何工作的:JavaScript 的記憶體模型

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

JavaScript 是如何工作的:JavaScript 的記憶體模型

在上圖中,我抽象出了記憶體位址以顯示每個變量的值。 但是,不要忘記實際上變量指向記憶體位址,然後儲存一個值。 這将是了解

let vs. const

一節的關鍵。

堆是存儲引用類型的地方。跟調用堆棧主要的差別在于,堆可以存儲無序的資料,這些資料可以動态地增長,非常适合數組和對象。

myArray

的變量,并用一個空數組初始化它。

let myArray = [];           

當你聲明變量“

myArray

”并為其指定非原始資料類型(如“[]”)時,以下是在記憶體中發生的情況:

  1. 為變量建立唯一辨別符(“

    myArray

    ”)
  2. 在記憶體中配置設定一個位址(将在運作時配置設定)
  3. 存儲在堆上配置設定的記憶體位址的值(将在運作時配置設定)
  4. 堆上的記憶體位址存儲配置設定的值(空數組[])
JavaScript 是如何工作的:JavaScript 的記憶體模型
JavaScript 是如何工作的:JavaScript 的記憶體模型

從這裡,我們可以

push

,

pop

,或對數組做任何我們想做的。

myArray.push("first");
myArray.push("second");
myArray.push("third");
myArray.push("fourth");
myArray.pop();           
JavaScript 是如何工作的:JavaScript 的記憶體模型

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

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

看作等于記憶體位址。

JavaScript 是如何工作的:JavaScript 的記憶體模型

當将

100

配置設定給

importantID

時,因為

100

是一個原始資料類型,是以會配置設定一個新的記憶體位址,并将

100

的值存儲這裡。

然後 JS 嘗試将新的記憶體位址配置設定給

importantID

,這就是抛出錯誤的地方,這也是我們想要的行為,因為我們不想改變這個

importantID

的值。

JavaScript 是如何工作的:JavaScript 的記憶體模型

當你将

100

importantID

時,實際上是在嘗試配置設定存儲

100

的新記憶體位址,這是不允許的,因為

importantID

是用

const

聲明的。

如上所述,假設的初級 JS 程式員使用

let

錯誤地聲明了他們的數組。相反,他們應該用

const

聲明它。這在一開始看起來可能令人困惑,我承認這一點也不直覺。

初學者會認為數組隻有在我們可以改變的情況下才有用,

const

使數組不可變,那麼為什麼要使用它呢? 請記住:“改變”是指改變記憶體位址。讓我們深入探讨一下為什麼使用const聲明數組是完全可以的。

const myArray = [];           

在聲明

myArray

時,将在調用堆棧上配置設定記憶體位址,該值是在堆上配置設定的記憶體位址。堆上存儲的值是實際的空數組。想象一下,它是這樣的:

JavaScript 是如何工作的:JavaScript 的記憶體模型
JavaScript 是如何工作的:JavaScript 的記憶體模型

如果我們這麼做:

myArray.push(1);
myArray.push(2);
myArray.push(3);
myArray.push(4);
myArray.push(5);           
JavaScript 是如何工作的:JavaScript 的記憶體模型

執行

push

操作實際是将數字放入堆中存在的數組。而

myArray

的記憶體位址沒有改變。這就是為什麼雖然使用

const

聲明了 myArray,但沒有抛出任何錯誤。

myArray

仍然等于

0458AFCZX91

,它的值是另一個記憶體位址

22VVCX011

,它在堆上有一個數組的值。

如果我們這樣做,就會抛出一個錯誤:

myArray = 3;           

由于

3

是一個原始資料類型,是以生成一個新的調用堆棧上的記憶體位址,其值為

3

,然後我們将嘗試将新的記憶體位址配置設定給

myArray

,由于 myArray 是用 const 聲明的,是以這是不允許的。

JavaScript 是如何工作的:JavaScript 的記憶體模型

另一個會抛出錯誤的例子:

myArray = ["a"];           

[a]

是一個新的引用類型的數組,是以将配置設定調用堆棧上的一個新記憶體位址,并存儲堆上的一個記憶體位址的值,其它值為

[a]

。然後,我們嘗試将調用堆棧記憶體位址配置設定給

myArray

,這會抛出一個錯誤。

JavaScript 是如何工作的:JavaScript 的記憶體模型

對于使用

const

聲明的對象(如數組),由于對象是引用類型,是以可以添加鍵,更新值等等。

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

為什麼這些知識對我們有用呢

JavaScript 是世界上排名第一的程式設計語言(

根據 GitHub 和 Stack Overflow 的年度開發人員調查

)。 掌握并成為“JS 忍者”是我們所有人都渴望成為的人。

任何品質好的的 JS 課程或書籍都提倡使用

let, const

來代替

var

,但他們并不一定說出原因。 對于初學者來說,為什麼某些 const 變量在“改變”其值時會抛出錯誤而其他 const變量卻沒有。 對我來說這是有道理的,為什麼這些程式員預設使用let到處避免麻煩。

但是,不建議這樣做。谷歌擁有世界上最好的一些程式員,在他們的 JavaScript 風格指南中說,使用 const 或 let 聲明所有本地變量。預設情況下使用 const,除非需要重新配置設定變量,不使用 var 關鍵字(

原文

)。

雖然他們沒有明确說明原因,但據我所知,有幾個原因

  • 先發制人地限制未來的 bug。
  • 使用

    const

    聲明的變量必須在聲明時初始化,這迫使程式員經常在範圍方面更仔細地放置它們。這最終會導緻更好的記憶體管理和性能。
  • 要通過代碼與任何可能遇到它的人交流,哪些變量是不可變的(就 JS 而言),哪些變量可以重新配置設定。

希望上面的解釋能幫助你開始明白為什麼或者什麼時候應該在代碼中使用 let 和 const 。

關于Fundebug

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

免費試用

繼續閱讀