【譯】JavaScript 的記憶體模型

前端小智發表於2019-04-15

譯者:前端小智

原文:medium.com/@ethannam/j…

// 宣告一些變數並初始化它們
var a = 5
let b = 'xy'
const c = true

// 分配新值
a = 6
b = b + 'z'
c = false //  型別錯誤:不可對常量賦值
複製程式碼

作為程式設計師,宣告變數、初始化變數(或不初始化變數)以及稍後為它們分配新值是我們每天都要做的事情。

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

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

  • JS 原始資料型別的變數宣告和賦值

  • JavaScript記憶體模型:呼叫堆疊和堆

  • JS 引用型別的變數宣告和賦值

  • let vs const

JS 原始資料型別的變數宣告和賦值

讓我們從一個簡單的例子開始。下面,我們宣告一個名為myNumber的變數,並用值23初始化它。

let myNumber = 23
複製程式碼

當執行此程式碼時,JS將執行:

  1. 為變數(myNumber)建立唯一識別符號(identifier)。

  2. 在記憶體中分配一個地址(在執行時分配)。

  3. 將值 23 儲存在分配的地址。

clipboard.png

雖然我們通俗地說,“myNumber 等於 23”,更專業地說,myNumber 等於儲存值 23 的記憶體地址,這是一個值得理解的重要區別。

如果我們要建立一個名為 newVar 的新變數並把 myNumber 賦值給它。

let newVar = myNumber
複製程式碼

因為 myNumber 在技術上實際是等於 “0012CCGWH80”,所以 newVar 也等於 “0012CCGWH80”,這是儲存值為23的記憶體地址。通俗地說就是 newVar 現在的值為 23

clipboard.png

因為 myNumber 等於記憶體地址 0012CCGWH80,所以將它賦值給 newVar 就等於將0012CCGWH80 賦值給 newVar

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

myNumber = myNumber + 1
複製程式碼

myNumber的值肯定是 24。但是newVar的值是否也為 24 呢?,因為它們指向相同的記憶體地址?

答案是否定的。由於JS中的原始資料型別是不可變的,當 myNumber + 1 解析為24時,JS 將在記憶體中分配一個新地址,將24作為其值儲存,myNumber將指向新地址。

clipboard.png

這是另一個例子:

let myString = 'abc'
myString = myString + 'd'
複製程式碼

雖然一個初級 JS 程式設計師可能會說,字母d只是簡單在原來存放adbc記憶體地址上的值,從技術上講,這是錯的。當 abcd 拼接時,因為字串也是JS中的基本資料型別,不可變的,所以需要分配一個新的記憶體地址,abcd 儲存在這個新的記憶體地址中,myString 指向這個新的記憶體地址。

clipboard.png

下一步是瞭解原始資料型別的記憶體分配位置。

JavaScript 記憶體模型:呼叫堆疊和堆

JS 記憶體模型可以理解為有兩個不同的區域:呼叫堆疊(call stack)堆(heap)

clipboard.png

呼叫堆疊是存放原始資料型別的地方(除了函式呼叫之外)。上一節中宣告變數後呼叫堆疊的粗略表示如下。

clipboard.png

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

是儲存引用型別的地方。跟呼叫堆疊主要的區別在於,堆可以儲存無序的資料,這些資料可以動態地增長,非常適合陣列和物件。

JS 引用型別的變數宣告和賦值

讓我們從一個簡單的例子開始。下面,我們宣告一個名為myArray的變數,並用一個空陣列初始化它。

let myArray = []
複製程式碼

當你宣告變數“myArray”併為其指定非原始資料型別(如“[]”)時,以下是在記憶體中發生的情況:

  1. 為變數建立唯一識別符號(“myArray”)

  2. 在記憶體中分配一個地址(將在執行時分配)

  3. 儲存在堆上分配的記憶體地址的值(將在執行時分配)

  4. 堆上的記憶體地址儲存分配的值(空陣列[])

clipboard.png

clipboard.png

從這裡,我們可以 push, pop,或對陣列做任何我們想做的。

myArray.push("first")
myArray.push("second")
myArray.push("third")
myArray.push("fourth")
myArray.pop()
複製程式碼

clipboard.png

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看作等於記憶體地址。

clipboard.png

當將100分配給importantID時,因為100是一個原始資料型別,所以會分配一個新的記憶體地址,並將100的值儲存這裡。

然後 JS 嘗試將新的記憶體地址分配給 importantID,這就是丟擲錯誤的地方,這也是我們想要的行為,因為我們不想改變這個 importantID的值。

clipboard.png

當你將100分配給importantID時,實際上是在嘗試分配儲存100的新記憶體地址,這是不允許的,因為importantID是用const宣告的。

如上所述,假設的初級JS程式設計師使用let錯誤地宣告瞭他們的陣列。相反,他們應該用const宣告它。這在一開始看起來可能令人困惑,我承認這一點也不直觀。

初學者會認為陣列只有在我們可以改變的情況下才有用,const 使陣列不可變,那麼為什麼要使用它呢? 請記住:“改變”是指改變記憶體地址。讓我們深入探討一下為什麼使用const宣告陣列是完全可以的。

const myArray = []
複製程式碼

在宣告 myArray 時,將在呼叫堆疊上分配記憶體地址,該值是在堆上分配的記憶體地址。堆上儲存的值是實際的空陣列。想象一下,它是這樣的:

clipboard.png

clipboard.png

如果我們這麼做:

myArray.push(1)
myArray.push(2)
myArray.push(3)
myArray.push(4)
myArray.push(5)
複製程式碼

clipboard.png

執行 push 操作實際是將數字放入堆中存在的陣列。而 myArray 的記憶體地址沒有改變。這就是為什麼雖然使用const宣告瞭myArray,但沒有丟擲任何錯誤。

myArray 仍然等於 0458AFCZX91,它的值是另一個記憶體地址22VVCX011,它在堆上有一個陣列的值。

如果我們這樣做,就會丟擲一個錯誤:

myArray = 3
複製程式碼

由於 3 是一個原始資料型別,因此生成一個新的呼叫堆疊上的記憶體地址,其值為 3,然後我們將嘗試將新的記憶體地址分配給 myArray,由於myArray是用const宣告的,所以這是不允許的。

clipboard.png

另一個會丟擲錯誤的例子:

myArray = ['a']
複製程式碼

由於[a]是一個新的引用型別的陣列,因此將分配呼叫堆疊上的一個新記憶體地址,並儲存上的一個記憶體地址的值,其它值為 [a]。然後,我們嘗試將呼叫堆疊記憶體地址分配給 myArray,這會丟擲一個錯誤。

clipboard.png

對於使用const宣告的物件(如陣列),由於物件是引用型別,因此可以新增鍵,更新值等等。

const myObj = {}
myObj['newKey'] = 'someValue' // 這不會丟擲錯誤
複製程式碼

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

JavaScript 是世界上排名第一的程式語言(根據GitHub和Stack Overflow的年度開發人員調查)。 掌握併成為“JS忍者”是我們所有人都渴望成為的人。

任何質量好的的 JS 課程或書籍都提倡使用let, const 來代替 var,但他們並不一定說出原因。 對於初學者來說,為什麼某些 const 變數在“改變”其值時會丟擲錯誤而其他 const變數卻沒有。 對我來說這是有道理的,為什麼這些程式設計師預設使用let到處避免麻煩。

但是,不建議這樣做。谷歌擁有世界上最好的一些程式設計師,在他們的JavaScript風格指南中說,使用 constlet 宣告所有本地變數。預設情況下使用 const,除非需要重新分配變數,不使用 var 關鍵字(原文)。

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

  1. 先發制人地限制未來的 bug。
  2. 使用 const 宣告的變數必須在宣告時初始化,這迫使程式設計師經常在範圍方面更仔細地放置它們。這最終會導致更好的記憶體管理和效能。
  3. 要通過程式碼與任何可能遇到它的人交流,哪些變數是不可變的(就JS而言),哪些變數可以重新分配。

希望上面的解釋能幫助你開始明白為什麼或者什麼時候應該在程式碼中使用 letconst

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

你的點贊是我持續分享好東西的動力,歡迎點贊!

歡迎加入前端大家庭,裡面會經常分享一些技術資源。

clipboard.png

相關文章