- 原文地址:JavaScript’s Memory Model
- 原文作者:Ethan Nam
- 译者:Chor
// 声明一些变量并进行初始化
var a = 5
let b = 'xy'
const c = true
// 重新赋值
a = 6
b = b + 'z'
c = false // TypeError: Assignment to constant variable
对我们程序员来说,声明变量、进行初始化和赋值几乎是每天都在做的一件事情。不过,这些操作本质上做了什么事情呢?JavaScript 是如何在内部对这些进行处理的?更重要的是,了解 JavaScript 的底层细节对我们程序员有什么好处?
本文的大纲如下:
- JS 基本类型的变量声明和赋值
- JS 的内存模型:调用栈和堆
- JS 引用类型的变量声明和赋值
- Let vs const
JS 基本类型的变量声明和赋值
我们先从一个简单的例子讲起:声明一个名为 muNumber
的变量,并初始化赋值为 23。
let myNumber = 23
当执行这一行代码的时候,JS 将会 …
- 为变量创建一个唯一的标识符(
myNumber
) - 在栈内存中分配一块空间(将在运行时完成分配)
- 将值 23 保存在这个分配出去的空间中
我们习惯的说法是“myNumber
等于23”,但更严谨的说法应该是,myNumber
等于保存着值 23 的那个内存空间的地址。这两者的区别很关键,需要搞清楚。
如果我们创建一个新变量 newVar
并将 myNumber
赋值给它 …
let newVar = myNumber
… 由于 myNumber
实际上等于内存地址 “0012CCGWH80”,因此这一操作会使得 newVar
也等于 “0012CCGWH80”,也就是等于保存着值 23 的那个内存地址。最终,我们可能会习惯说“newVar
现在等于 23 了”。
那么,如果我这样做会发生什么呢?
myNumber = myNumber + 1
myNumber
自然会“等于” 24,不过 newVar
和 myNumber
指向的可是同一块内存空间啊,newVar
是否也会“等于” 24 呢?
并不会。在 JS 中,基本数据类型是不可改变的,在 “myNumber + 1” 被解析为 “24” 的时候,JS 实际上将会在内存中重新分配一块新的空间用于存放 24 这个值,而 myNumber
将会转而指向这个新的内存空间的地址。
再看一个类型的例子:
let myString = 'abc'
myString = myString + 'd'
JS 初学者可能会认为,无论字符串 abc
存放在内存的哪个地方,这个操作都会将字符 d
拼接在字符串后面。这种想法是错误的。别忘了,在 JS 中字符串也是基本类型。当 abc
与 d
拼接的时候,在内存中会重新分配一块新的空间用于存放 abcd
这个字符串,而 myString
将会转而指向这个新的内存空间的地址(同时,abc
依然位于原先的内存空间中)。
接下来我们看一下基本类型的内存分配发生在哪里。
JS 的内存模型:调用栈和堆
简单理解,可以认为 JS 的内存模型包含两个不同的区域,一个是调用栈,一个是堆。
除了函数调用之外,调用栈同时也用于存放基本类型的数据。以上一小节的代码为例,在声明变量后,调用栈可以粗略表示如下图:
在上面这张图中,我对内存地址进行了抽象,以显示每个变量的值,但请记住,(正如之前所说的)变量始终指向某一块保存着某个值的内存空间。这是理解 let vs const 这一小节的关键。
再来看一下堆。
堆是引用类型变量存放的地方。堆相对于栈的一个关键区别就在于,堆可以存放动态增长的无序数据 —— 尤其是数组和对象。
JS 引用类型的变量声明和赋值
在变量声明与赋值这方面,引用类型变量与基本类型变量的行为表现有很大的差异。
我们同样从一个简单的例子讲起。下面声明一个名为 myArray
的变量并初始化为一个空数组:
let myArray = []
当你声明一个变量 myArray
并通过引用类型数据(比如 []
)为它赋值的时候,在内存中的操作是这样的:
- 为变量创建一个唯一的标识符(
myArray
) - 在堆内存中分配一块空间(将在运行时完成分配)
- 这个空间存放着此前所赋的值(空数组
[]
) - 在栈内存中分配一块空间
- 这个空间存放着指向被分配的堆空间的地址
我们可以对 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
知道这些有什么用?
GitHub 和 Stack Overflow 年度开发者调查报告) 的相关数据显示,JavaScript 是排名第一的语言。精通这门语言并成为一名“JS 大师”可能是我们梦寐以求的。在任何一门像样的 JS 课程或者一本书中,都会倡导我们多使用 const
和 let
,少使用 var
,但他们基本上都没有解释这其中的缘由。很多初学者会疑惑为什么有些用 const
声明的变量在“修改”的时候确实会报错,而有些变量却不会。我能够理解,正是这种反直觉的体验让他们更喜欢随处都使用 let
,毕竟谁也不想踩坑嘛。
不过,这并不是我们推荐的方式。Google 作为一家拥有顶尖程序员的公司,它的 JavaScript 风格指南中就有这么一段话:用 const
或者 let
声明所有的局部变量。除非一个变量有重新赋值的需要,否则默认使用 const
进行声明。绝不允许使用 var
关键字 (来源)。
虽然他们没有指出个中缘由,不过我认为有下面这些理由:
- 预先避免将来可能产生的 bug
- 用
const
声明的变量在声明的时候就必须进行初始化,这会引导开发者关注这些变量在作用域中的表现,最终有助于促进更好的内存管理与性能表现。 - 带来更好的可读性,任何接管代码的人都能知道,哪些变量是不可修改的(就 JS 而言),哪些变量是可以重新赋值的。
希望本文能够帮助你理解使用 const
或者 let
声明变量的个中缘由以及应用场景。
参考: