阅读 1869

[译]助力年后跳槽涨薪,深入理解 JavaScript 内存模型


本文翻译自:JavaScript’s Memory Model


感谢原作者创作了这么好的文章


作为开发者,我们每天都在声明变量,初始化变量,并在以后分配新值。


// declare some variables and initialize them
var a = 5let b = 'xy'
const c = true// assign new values
a = 6
b = b + 'z'
c = false // TypeError: Assignment to constant variable复制代码

但是,
实际上
这样做的时候会发生什么?
特别是 JavaScript 如何在内部处理此类基本功能?
更重要的是,作为开发者,了解 JavaScript 的基本细节有什么好处?

下面,我打算介绍以下内容

  1. JS 原始数据类型的变量声明和赋值
  2. JavaScript 的内存模型:调用堆栈和堆
  3. JS 引用数据类型的变量声明和赋值
  4. Let 和 const

JS原始数据类型的变量声明和赋值

让我们从一个简单的例子开始。
下面,我们声明一个名为 myNumber 的变量,并将其初始化为 23 。

let myNumber = 23复制代码

执行此代码后,JS 将…

  1. 为变量创建一个唯一标识符(“myNumber”)。
  2. 在内存中分配一个地址(将在运行时分配)。
  3. 在分配的地址中存储一个值(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都简单地附加在字符串abc后面,但这在程序角度上是错误的。


当 “abc” 与 “d” 串联时,由于字符串也是 JS 中的原始数据类型,因此会分配一个新的内存地址,在其中存储 “abcd”,而 “myString” 指向此新的内存地址。


下一步将会了解对于原始数据类型内存分配在哪发生。

JavaScript的内存模型:调用堆栈和堆

就本博客而言,可以将JS内存模型理解为具有两个不同的区域:调用堆栈和堆。

调用堆栈是存储原始数据类型的地方
在上一节中声明变量之后,下面是调用堆栈的粗略表示。


在下面的插图中,我剔除了内存地址以显示每个变量的值。
但是,请不要忘记实际上该变量指向一个内存地址,然后该内存地址保存一个值。
这是理解 letconst 的关键。

那么堆呢。

堆是存储引用数据类型的位置。
关键区别在于堆可以存储可以动态增长的无序数据,这对于数组和对象来说是完美的。

JS引用数据类型的变量声明和赋值

与原始JS数据类型相比,非原始JS数据类型的行为有所不同。

让我们从一个简单的例子开始。
在下面,我们声明一个名为 “myArray” 的变量,并使用一个空数组对其进行初始化。

let myArray = []复制代码

当您声明变量 “myArray” 并为其分配非原始数据类型(如“ []”)时,这将在内存中发生:

  1. 为变量创建一个唯一标识符(“ myArray”)。
  2. 在内存中分配一个地址(将在运行时分配)。
  3. 存储在堆上分配的内存地址的值(将在运行时分配)。
  4. 堆上的内存地址存储分配的值(空数组[])。


从这里,我们可以调用 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,调用 push 更改其值。

现在我们来解释一下 “更改” 的正确含义

let

您可以更改内存地址。

const

不允许您更改内存地址。

const importantID = 489importantID = 100 // TypeError: Assignment to constant variable复制代码

让我们想象一下这里发生的事情。

声明 “mportantID” 时,将分配一个内存地址,并存储值 489。
请记住,将变量 “importantID” 视为等于内存地址。


当将 100 分配给 “importantID” 时,由于 100 是基本数据类型,因此会分配一个新的内存地址,并将 100 的值存储在那里。
然后,JS尝试将新的内存地址分配给 “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)复制代码

这会将数字推入堆中存在的数组。但是,“myArray”的内存地址未更改。这就是为什么尽管使用常量声明了 “myArray”,却未引发任何错误。“myArray” 仍等于 “0458AFCZX91”,其值为另一个内存地址 “22VVCX011”,其值为堆上数组的值。

如果执行以下操作,将引发错误:

myArray = 3复制代码

由于 3 是基本数据类型,因此将在调用堆栈上分配一个内存地址,并存储值 3,然后我们将尝试将新的内存地址分配给 myArray。
但是由于 myArray 是用 const 声明的,因此不允许这样做。


另一个将引发错误的示例:

myArray = ['a']复制代码

由于 ['a'] 是一个新的非原始数组,因此将在调用堆栈上分配一个新的内存地址,并存储堆上一个内存地址的值,存储在堆内存地址上的值将为['一种']。
然后,我们将尝试将调用堆栈内存地址分配给 myArray,这将引发错误。


对于用 const 声明的对象
(如数组),由于对象不是基本数据类型,因此您可以添加键,更新值等等。

const myObj = {} 
myObj['newKey'] ='someValue'//这不会引发错误复制代码

为什么这对我们有用

JavaScript 是全球排名第一的编程语言(根据 GitHub 和 
Stack Overflow 的年度开发人员调查

)。
发展掌握能力并成为 “JS Ninja” 是我们所有人都渴望成为的。任何体面的 JS 课程或书籍倡导const 让 va r放弃,但他们并不一定要说为什么。对于某些初学者而言,为什么某些 const
变量在 “更改” 其值时抛出错误而其他变量则没有,这是不直观的
对于我来说,为什么这些程序员随后默认使用在各处使用 let 以避免麻烦是很有意义的。

但是,不建议这样做。
拥有世界上最好的编码器的 Google 在其 JavaScript 编程风格指南中说:“使用constlet声明所有局部变量默认情况下使用const,除非需要重新分配变量。VAR
关键字不推荐使用”()。

尽管据我所知,他们没有明确说明原因,但有几个原因:

  1. 太过自由带来 bug。

  2. 用 const 声明的变量必须在声明时进行初始化,这迫使编码人员经常在范围方面考虑周全。这最终导致更好的内存管理和性能。
  3. 别人通过您的代码就像和你交流一样,哪些变量是不可变的,哪些变量可以重新分配。

我希望以上解释能帮助您开始理解:

const 或 Let

参考文献

  1. Google JS样式指南
  2. 学习JavaScript:通过共享调用,参数传递


最后

最近我录了几个大厂的笔试真题讲解,准备面试的可以关注一下:

哔哩哔哩2020校园招聘前端笔试卷(一):www.bilibili.com/video/av834…

哔哩哔哩2020校园招聘技术类笔试卷(二):www.bilibili.com/video/av834…

小米2019秋招前端开发笔试题(B):www.bilibili.com/video/av834…

阿里巴巴2017秋招前端笔试题(上篇):www.bilibili.com/video/av834…

如果喜欢我的文章,可以关注我的的微信公众号。


关注下面的标签,发现更多相似文章
评论