【譯】談一談JavaScript的內存模型

// 聲明一些變量並進行初始化
var a = 5
let b = 'xy'
const c = true

// 從新賦值
a = 6
b = b + 'z'
c = false // TypeError: Assignment to constant variable

對咱們程序員來講,聲明變量、進行初始化和賦值幾乎是天天都在作的一件事情。不過,這些操做本質上作了什麼事情呢?JavaScript 是如何在內部對這些進行處理的?更重要的是,瞭解 JavaScript 的底層細節對咱們程序員有什麼好處?javascript

本文的大綱以下:html

  1. JS 基本類型的變量聲明和賦值
  2. JS 的內存模型:調用棧和堆
  3. JS 引用類型的變量聲明和賦值
  4. Let vs const

JS 基本類型的變量聲明和賦值

咱們先從一個簡單的例子講起:聲明一個名爲 muNumber 的變量,並初始化賦值爲 23。前端

let myNumber = 23

當執行這一行代碼的時候,JS 將會 ......java

  1. 爲變量建立一個惟一的標識符( myNumber
  2. 在棧內存中分配一塊空間(將在運行時完成分配)
  3. 將值 23 保存在這個分配出去的空間中

咱們習慣的說法是「myNumber 等於23」,但更嚴謹的說法應該是,myNumber 等於保存着值 23 的那個內存空間的地址。這二者的區別很關鍵,須要搞清楚。git

若是咱們建立一個新變量 newVar 並將 myNumber 賦值給它 ......程序員

let newVar = myNumber

...... 因爲 myNumber 實際上等於內存地址 「0012CCGWH80」,所以這一操做會使得 newVar 也等於 「0012CCGWH80」,也就是等於保存着值 23 的那個內存地址。最終,咱們可能會習慣說「newVar 如今等於 23 了」。github

那麼,若是我這樣作會發生什麼呢?數組

myNumber = myNumber + 1

myNumber 天然會「等於」 24,不過 newVarmyNumber 指向的但是同一塊內存空間啊,newVar 是否也會「等於」 24 呢?session

並不會。在 JS 中,基本數據類型是不可改變的,在 「myNumber + 1」 被解析爲 「24」 的時候,JS 實際上將會在內存中從新分配一塊新的空間用於存放 24 這個值,而 myNumber 將會轉而指向這個新的內存空間的地址。ide

再看一個類型的例子:

let myString = 'abc'  
myString = myString + 'd'

JS 初學者可能會認爲,不管字符串 abc 存放在內存的哪一個地方,這個操做都會將字符 d 拼接在字符串後面。這種想法是錯誤的。別忘了,在 JS 中字符串也是基本類型。當 abcd 拼接的時候,在內存中會從新分配一塊新的空間用於存放 abcd 這個字符串,而 myString 將會轉而指向這個新的內存空間的地址(同時,abc 依然位於原先的內存空間中)。

接下來咱們看一下基本類型的內存分配發生在哪裏。


JS 的內存模型:調用棧和堆

簡單理解,能夠認爲 JS 的內存模型包含兩個不一樣的區域,一個是調用棧,一個是堆。

除了函數調用以外,調用棧同時也用於存放基本類型的數據。以上一小節的代碼爲例,在聲明變量後,調用棧能夠粗略表示以下圖:

在上面這張圖中,我對內存地址進行了抽象,以顯示每一個變量的值,但請記住,(正如以前所說的)變量始終指向某一塊保存着某個值的內存空間。這是理解 let vs const 這一小節的關鍵。

再來看一下堆。

堆是引用類型變量存放的地方。堆相對於棧的一個關鍵區別就在於,堆能夠存放動態增加的無序數據 —— 尤爲是數組和對象。


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

在變量聲明與賦值這方面,引用類型變量與基本類型變量的行爲表現有很大的差別。

咱們一樣從一個簡單的例子講起。下面聲明一個名爲 myArray 的變量並初始化爲一個空數組:

let myArray = []

當你聲明一個變量 myArray 並經過引用類型數據(好比 [])爲它賦值的時候,在內存中的操做是這樣的:

  1. 爲變量建立一個惟一的標識符(myArray
  2. 在堆內存中分配一塊空間(將在運行時完成分配)
  3. 這個空間存放着此前所賦的值(空數組 []
  4. 在棧內存中分配一塊空間
  5. 這個空間存放着指向被分配的堆空間的地址

咱們能夠對 myArray 進行各類數組操做:

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


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 變量是正確的,畢竟 sum 變量的值確實會改變;不過,用 let 聲明 numbers 是錯誤的。而錯誤的根源在於,這些人認爲往數組中添加元素是在改變它的值。

所謂的「改變」,實際上指的是內存地址的改變let 聲明的變量容許咱們修改內存地址,而 const 則不容許。

const importantID = 489  
importantID = 100 // TypeError: Assignment to constant variable

咱們研究一下這裏爲何會報錯。

當聲明 importantID 變量以後,某一塊內存空間被分配出去,用於存放 489 這個值。牢記咱們以前所說的,變量 importantID 歷來只等於某一個內存地址。

當把 100 賦值給 importantID 的時候,因爲 100 是基本類型的值,內存中會分配一塊新的空間用於存放 100。以後,JS 試圖將這塊新空間的地址賦值給 importantID,此時就會報錯。這其實正是咱們指望的結果,由於咱們根本就不想對這個很是重要的 ID 進行改動 .......

這樣就說得通了,用 let 聲明數組是錯誤的(不合適的),應該用 const 才行。這對初學者來講確實比較困惑,畢竟這徹底不符合直覺啊!初學者會認爲,既然是數組確定須要有所改動,而 const 聲明的常量明明是不可改動的啊,那爲什麼還要用 const ?不過,你必須得記住:所謂的「改變」指的是內存地址的改變。咱們再來深刻理解一下,爲何在這裏使用 const 徹底沒問題,而且絕對是更好的選擇。

const myArray = []

在聲明 myArray 以後,調用棧會分配一塊內存空間,它所存放的值是指向堆中某個被分配內存空間的地址。而堆中的這個空間纔是實際上存放空數組的地方。看下面的圖理解一下:

若是咱們進行這些操做:

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

這將會往堆中的數組添加元素。不過,myArray 的內存地址但是至始至終都沒改變的。這也就解釋了爲何 myArray 是用 const 聲明的,可是對它(數組)的修改卻不會報錯。由於,myArray 始終等於內存地址 「0458AFCZX91」,該地址指向的空間存放着另外一個內存地址 「22VVCX011」,而這第二個地址指向的空間則真正存放着堆中的數組。

若是咱們這麼作,則會報錯:

myArray = 3

由於 3 是基本類型的值,這麼作會在內存中分配一塊新的空間用於存放 3,同時會修改 myArray 的值,使其等於這塊新空間的地址。而因爲 myArray 是用 const 聲明的,這樣修改就必然會報錯。

下面這樣作一樣會報錯:

myArray = ['a']

因爲 [‘a’] 是一個新的引用類型的數組,所以在棧中會分配一塊新的空間來存放堆中的某個空間地址,堆中這塊空間則用於存放[‘a’] 。以後咱們試圖把新的內存地址賦值給 myArray,這樣顯然也是會報錯的。

對於用 const 聲明的對象,它和數組的表現也是同樣的。由於對象也是引用類型的數據,能夠添加鍵,更新值,諸如此類。

const myObj = {}  
myObj['newKey'] = 'someValue' // this will not throw an error

知道這些有什麼用?

GitHubStack Overflow 年度開發者調查報告) 的相關數據顯示,JavaScript 是排名第一的語言。精通這門語言併成爲一名「JS 大師」多是咱們求之不得的。在任何一門像樣的 JS 課程或者一本書中,都會倡導咱們多使用 constlet,少使用 var,但他們基本上都沒有解釋這其中的原因。不少初學者會疑惑爲何有些用 const 聲明的變量在「修改」的時候確實會報錯,而有些變量卻不會。我可以理解,正是這種反直覺的體驗讓他們更喜歡隨處都使用 let,畢竟誰也不想踩坑嘛。

不過,這並非咱們推薦的方式。Google 做爲一家擁有頂尖程序員的公司,它的 JavaScript 風格指南中就有這麼一段話:用 const 或者 let 聲明全部的局部變量。除非一個變量有從新賦值的須要,不然默認使用 const 進行聲明。毫不容許使用 var關鍵字 (來源)。

雖然他們沒有指出箇中原因,不過我認爲有下面這些理由:

  1. 預先避免未來可能產生的 bug
  2. const 聲明的變量在聲明的時候就必須進行初始化,這會引導開發者關注這些變量在做用域中的表現,最終有助於促進更好的內存管理與性能表現。
  3. 帶來更好的可讀性,任何接管代碼的人都能知道,哪些變量是不可修改的(就 JS 而言),哪些變量是能夠從新賦值的。

但願本文可以幫助你理解使用 const 或者 let 聲明變量的箇中原因以及應用場景。

參考:

  1. Google JS Style Guide
  2. Learning JavaScript: Call By Sharing, Parameter Passing
  3. How JavaScript works: memory management + how to handle 4 common memory leaks

交流

目前專一於前端領域和交互設計領域的學習,熱愛分享和交流。感興趣的朋友能夠關注公衆號,一塊兒學習和進步。

Snipaste_2020-07-09_16-09-39.png

相關文章
相關標籤/搜索