我對於 JavaScript 的內存模型一直都比較困惑,很想了解在操作變量的時候,JS 是如何工作的。如果你和我有同樣的困惑,希望這篇文章能給你一些啟發。
譯文,喜歡原文的可以直接拉到底部
當我們聲明變量、初始化變量、更改變量值的時候,到底會發生什么?JavaScript 是如何實現這些基本的功能?最重要的是,我們如何才能理解這些基礎知識?
本文將覆蓋以下 4 個方面:
- JavaScript 原始數據類型的變量聲明和賦值
- JavaScript 內存模型:調用棧和堆
- JavaScript 引用類型的變量聲明和賦值
- Let VS. const
JavaScript 原始數據類型的變量聲明和賦值
從一個簡單的栗子開始。首先我們聲明一個叫myNumber
的變量,賦值為 23。
let myNumber = 23
執行這段代碼的時候,JavaScript 會...
- 為你的變量(myNumber)創建一個唯一標識符。
- 為變量分配一個內存地址(運行時)。
- 在分配的地址中存儲一個值(23)。
通常我們會說:“myNumber 等於 23”,但從技術上講,myNumber
等於一個內存地址,那兒保存着一個大小為 23 的值。理解這段話十分關鍵。
如果我們創建一個 newVar
的新變量,然后把 myNumber
賦值給它:
let newVar = myNumber
因為 myNumber
實際上等於“0012CCGWH80”,那么newVar
也等於“0012CCGWH80”,這個內存地址保存的值為 23。最終實現了“newVal 等於 23”的效果。
如果我們這樣做又會發生什么呢?
myNumber = myNumber + 1
顯然,myNumber
的值為 24,那么對於指向相同內存地址的newVar
,它是否也等於 24?
答案當然是否定的!因為 JavaScript 的基本數據類型是不可變的,myNumber + 1
的結果是 24,JavaScript 會分配一個新的內存地址來存儲這個值,然后將myNumber
指向這個新地址。
圖3
再舉一個例子:
let myString = 'abc'
myString = myString + 'd'
JS 新手可能認為,字符串abc
已經存在於內存里,所以字母d
只是追加到它的后面。從技術上講,這是錯誤的。由於原始數據類型的不變性,當abc
與d
結合時,JS 會分配一個新的內存地址來保存這個值(abcd
),接着myString
指向新的地址。
圖4
JavaScript 的內存模型:調用棧和堆
JS 的內存模型可以簡單的理解為兩個不同的區域:調用棧和堆。
圖5
棧用來保存原始數據以及函數調用,可以粗略的用下圖表示。
圖6
上圖中,我抽象的在調用棧中顯示每個變量的值。但請記住,變量實際指向的是內存地址,那里保存着對應的值。這是理解let vs. cont
的關鍵。
關於堆內存。
堆保存着所有非原始類型的數據。它和棧最大的區別是,堆可以保存無序、能夠動態增刪的數據——對於對象和數組來說,這是完美的存儲空間。
JavaScript 非原始數據類型的變量聲明和賦值
還是從一個簡單的栗子開始。下面,我們聲明一個叫myArray
的變量,並初始化一個空數組。
let myArray = []
當 JS 引擎執行上面的代碼,內存會發生如下變化:
- 為變量(myArray)創建一個唯一標識符。
- 在棧中給變量分配一個地址a(運行時)。
- 在堆中分配一個地址b,用來存儲值 [](運行時)。
- 地址a所存儲的值為地址b
圖7
圖8
現在,我們可以對數組做任何操作了。
myArray.push('first')
myArray.push('second')
myArray.push('third')
myArray.pop()
圖9
Let vs. const
我們應該優先使用const
而不是let
,除非變量會被改變。
我們必須清楚的知道——“改變”到底是什么意思。
值發生了變化,這是對“改變”的一種錯誤理解。一些 JS 程序員會寫下這樣的代碼:
let sum = 0
sum = 1 + 2
let numbers = []
numbers.push(1)
numbers.push(2)
這段代碼正確的使用let
聲明變量sum
,因為值被改變了。然而卻錯誤的使用let
來聲明變量numbers
,因為他們認為給數組 push 一些數據后,數組的值被改變了。
“改變”的正確解釋是——內存地址變了。let
允許你改變內存地址,const
則不允許。
const importantId = 489
importantId = 100 // TypeError: Assignment to constant variable
一起看看這到底發生了什么。
當聲明importantId
時,JS 引擎為其分配一個內存地址,並存儲一個大小為 489 的值。切記,變量importantId
等於這個內存地址。
圖10
當把 100 賦值給importantId
時,因為 100 是原始類型,此時會分配一個用來存儲 100 的內存地址。然后 JS 嘗試將新的內存地址賦值給importantId
,此時就會發生錯誤。這是我們想要的結果,因為我們不想改變一個非常重要的 ID。
圖11
對於新手來說,由於不清楚“改變”的真是含義,在使用 const 聲明變量可能會有些困惑,所以他們默認使用 let 來避免麻煩。
然而,這並不是推薦的做法。Google 在他們的 JavaScript 風格指南中寫道:“使用 const 或 let 聲明所有變量。除非變量會被重新賦值,否則優先使用 const。一定不要使用 var”。
他們沒有明確說明為什么要這樣做,但我認為這樣做有以下好處:
- 減少未來的bug。
- 使用 const 聲明變量時必須初始化,這會強迫程序員更加小心的處理變量作用域,帶來更好的內存管理和性能。
- 更好的可讀性,哪些變量是不變的,哪些會被重新賦值,一目了然。
bye...