JavaScript 是如何工做的:JavaScript 的内存模型

阿里云最近在作活动,低至2折,有兴趣能够看看:
https://promotion.aliyun.com/...

为了保证的可读性,本文采用意译而非直译。html

这是专门探索 JavaScript 及其所构建的组件的系列文章的第 21 篇。前端

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!git

若是你错过了前面的章节,能够在这里找到它们:程序员

  1. JavaScript 是如何工做的:引擎,运行时和调用堆栈的概述!
  2. JavaScript 是如何工做的:深刻V8引擎&编写优化代码的5个技巧!
  3. JavaScript 是如何工做的:内存管理+如何处理4个常见的内存泄漏!
  4. JavaScript 是如何工做的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!
  5. JavaScript 是如何工做的:深刻探索 websocket 和HTTP/2与SSE +如何选择正确的路径!
  6. JavaScript 是如何工做的:与 WebAssembly比较 及其使用场景!
  7. JavaScript 是如何工做的:Web Workers的构建块+ 5个使用他们的场景!
  8. JavaScript 是如何工做的:Service Worker 的生命周期及使用场景!
  9. JavaScript 是如何工做的:Web 推送通知的机制!
  10. JavaScript 是如何工做的:使用 MutationObserver 跟踪 DOM 的变化!
  11. JavaScript 是如何工做的:渲染引擎和优化其性能的技巧!
  12. JavaScript 是如何工做的:深刻网络层 + 如何优化性能和安全!
  13. JavaScript 是如何工做的:CSS 和 JS 动画底层原理及如何优化它们的性能!
  14. JavaScript 是如何工做的:解析、抽象语法树(AST)+ 提高编译速度5个技巧!
  15. JavaScript 是如何工做的:深刻类和继承内部原理+Babel和 TypeScript 之间转换!
  16. JavaScript 是如何工做的:存储引擎+如何选择合适的存储API!
  17. JavaScript 是如何工做的:Shadow DOM 的内部结构+如何编写独立的组件!
  18. JavaScript 是如何工做的:WebRTC 和对等网络的机制!
  19. JavaScript 是如何工做的:编写本身的 Web 开发框架 + React 及其虚拟 DOM 原理!
  20. JavaScript 是如何工做的:模块的构建以及对应的打包工具
// 声明一些变量并初始化它们
var a = 5
let b = 'xy'
const c = true

// 分配新值
a = 6
b = b + 'z'
c = false //  类型错误:不可对常量赋值

做为程序员,声明变量、初始化变量(或不初始化变量)以及稍后为它们分配新值是咱们天天都要作的事情。github

可是当这样作的时候会发生什么呢? JavaScript 如何在内部处理这些基本功能? 更重要的是,做为程序员,理解 JavaScript 的底层细节对咱们有什么好处。web

下面,我打算介绍如下内容:编程

  • JS 原始数据类型的变量声明和赋值
  • JavaScript内存模型:调用堆栈和堆
  • JS 引用类型的变量声明和赋值
  • let vs const

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

让咱们从一个简单的例子开始。下面,咱们声明一个名为myNumber的变量,并用值23初始化它。segmentfault

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

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,便可看到福利,你懂的。

clipboard.png

相关文章
相关标签/搜索