// 声明一些变量并初始化它们 var a = 5 let b = 'xy' const c = true // 分配新值 a = 6 b = b + 'z' c = false // 类型错误:不可对常量赋值
作为程序员,声明变量、初始化变量(或不初始化变量)以及稍后为它们分配新值是我们每天都要做的事情。
但是当这样做的时候会发生什么呢? JavaScript 如何在内部处理这些基本功能? 更重要的是,作为程序员,理解 JavaScript 的底层细节对我们有什么好处。
下面,我打算介绍以下内容:
JS 原始数据类型的变量声明和赋值
JavaScript内存模型:调用堆栈和堆
JS 引用类型的变量声明和赋值
let
vsconst
JS 原始数据类型的变量声明和赋值
让我们从一个简单的例子开始。下面,我们声明一个名为myNumber
的变量,并用值23
初始化它。
let myNumber = 23
当执行此代码时,JS将执行:
为变量(
myNumber
)创建唯一标识符(identifier)。在内存中分配一个地址(在运行时分配)。
将值
23
存储在分配的地址。
虽然我们通俗地说,“myNumber 等于 23”
,更专业地说,myNumber
等于保存值 23 的内存地址,这是一个值得理解的重要区别。
如果我们要创建一个名为 newVar
的新变量并把 myNumber
赋值给它。
let newVar = myNumber
因为 myNumber
在技术上实际是等于 “0012CCGWH80
”,所以 newVar
也等于 “0012CCGWH80
”,这是保存值为23
的内存地址。通俗地说就是 newVar
现在的值为 23
。
因为 myNumber
等于内存地址 0012CCGWH80
,所以将它赋值给 newVar
就等于将0012CCGWH80
赋值给 newVar
。
现在,如果我这样做会发生什么:
myNumber = myNumber + 1
myNumber
的值肯定是 24。但是newVar
的值是否也为 24 呢?,因为它们指向相同的内存地址?
答案是否定的。由于JS中的原始数据类型是不可变的,当 myNumber + 1
解析为24
时,JS 将在内存中分配一个新地址,将24
作为其值存储,myNumber
将指向新地址。
这是另一个例子:
let myString = 'abc' myString = myString + 'd'
虽然一个初级 JS 程序员可能会说,字母d
只是简单在原来存放adbc
内存地址上的值,从技术上讲,这是错的。当 abc
与 d
拼接时,因为字符串也是JS中的基本数据类型,不可变的,所以需要分配一个新的内存地址,abcd
存储在这个新的内存地址中,myString
指向这个新的内存地址。
下一步是了解原始数据类型的内存分配位置。
JavaScript 内存模型:调用堆栈和堆
JS 内存模型可以理解为有两个不同的区域:调用堆栈(call stack)和堆(heap)。
调用堆栈是存放原始数据类型的地方(除了函数调用之外)。上一节中声明变量后调用堆栈的粗略表示如下。
在上图中,我抽象出了内存地址以显示每个变量的值。 但是,不要忘记实际上变量指向内存地址,然后保存一个值。 这将是理解 let vs. const
一节的关键。
堆是存储引用类型的地方。跟调用堆栈主要的区别在于,堆可以存储无序的数据,这些数据可以动态地增长,非常适合数组和对象。
JS 引用类型的变量声明和赋值
让我们从一个简单的例子开始。下面,我们声明一个名为myArray
的变量,并用一个空数组初始化它。
let myArray = []
当你声明变量“myArray
”并为其指定非原始数据类型(如“[]”)时,以下是在内存中发生的情况:
为变量创建唯一标识符(“
myArray
”)在内存中分配一个地址(将在运行时分配)
存储在堆上分配的内存地址的值(将在运行时分配)
堆上的内存地址存储分配的值(空数组[])
从这里,我们可以 push
, pop
,或对数组做任何我们想做的。
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
,因为他们知道值会改变。但是,这个程序员使用let
错误地声明了数组 numbers
,因为他将把东西推入数组理解为改变数组的值。
解释**“改变”**的正确方法是更改内存地址
。let
允许你更改内存地址。const
不允许你更改内存地址。
const importantID = 489 importantID = 100 // 类型错误:赋值给常量变量
让我们想象一下这里发生了什么。
当声明importantID
时,分配了一个内存地址,并存储489
的值。记住,将变量importantID
看作等于内存地址。
当将100
分配给importantID
时,因为100
是一个原始数据类型,所以会分配一个新的内存地址,并将100
的值存储这里。
然后 JS 尝试将新的内存地址分配给 importantID
,这就是抛出错误的地方,这也是我们想要的行为,因为我们不想改变这个 importantID
的值。
当你将100
分配给importantID
时,实际上是在尝试分配存储100
的新内存地址,这是不允许的,因为importantID
是用const
声明的。
如上所述,假设的初级JS程序员使用let
错误地声明了他们的数组。相反,他们应该用const
声明它。这在一开始看起来可能令人困惑,我承认这一点也不直观。
初学者会认为数组只有在我们可以改变的情况下才有用,const
使数组不可变,那么为什么要使用它呢? 请记住:“改变”是指改变内存地址。让我们深入探讨一下为什么使用const声明数组是完全可以的。
const myArray = []
在声明 myArray
时,将在调用堆栈上分配内存地址,该值是在堆上分配的内存地址。堆上存储的值是实际的空数组。想象一下,它是这样的:
如果我们这么做:
myArray.push(1) myArray.push(2) myArray.push(3) myArray.push(4) myArray.push(5)
执行 push
操作实际是将数字放入堆中存在的数组。而 myArray
的内存地址没有改变。这就是为什么虽然使用const
声明了myArray,但没有抛出任何错误。
myArray
仍然等于 0458AFCZX91
,它的值是另一个内存地址22VVCX011
,它在堆上有一个数组的值。
如果我们这样做,就会抛出一个错误:
myArray = 3
由于 3
是一个原始数据类型,因此生成一个新的调用堆栈上的内存地址,其值为 3
,然后我们将尝试将新的内存地址分配给 myArray
,由于myArray是用const声明的,所以这是不允许的。
另一个会抛出错误的例子:
myArray = ['a']
由于[a]
是一个新的引用类型的数组,因此将分配调用堆栈上的一个新内存地址,并存储堆上的一个内存地址的值,其它值为 [a]
。然后,我们尝试将调用堆栈内存地址分配给 myArray
,这会抛出一个错误。
对于使用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 。
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。