译谈一谈JavaScript的内存模型

12次阅读

共计 4131 个字符,预计需要花费 11 分钟才能阅读完成。

  • 原文地址: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 的底层细节对咱们程序员有什么益处?

本文的纲要如下:

  1. JS 根本类型的变量申明和赋值
  2. JS 的内存模型:调用栈和堆
  3. JS 援用类型的变量申明和赋值
  4. Let vs const

JS 根本类型的变量申明和赋值

咱们先从一个简略的例子讲起:申明一个名为 muNumber 的变量,并初始化赋值为 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 = myNumber + 1

myNumber 天然会“等于”24,不过 newVarmyNumber 指向的可是同一块内存空间啊,newVar 是否也会“等于”24 呢?

并不会。在 JS 中,根本数据类型是 不可扭转的,在“myNumber + 1”被解析为“24”的时候,JS 实际上将会在内存中重新分配一块新的空间用于寄存 24 这个值,而 myNumber 将会转而指向这个新的内存空间的地址。

再看一个类型的例子:

let myString = 'abc'  
myString = myString + 'd'

JS 初学者可能会认为,无论字符串 abc 寄存在内存的哪个中央,这个操作都会将字符 d 拼接在字符串前面。这种想法是谬误的。别忘了,在 JS 中字符串也是根本类型。当 abcd 拼接的时候,在内存中会重新分配一块新的空间用于寄存 abcd 这个字符串,而 myString 将会转而指向这个新的内存空间的地址(同时,abc 仍然位于原先的内存空间中)。

接下来咱们看一下根本类型的内存调配产生在哪里。


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

简略了解,能够认为 JS 的内存模型蕴含两个不同的区域,一个是调用栈,一个是堆。

除了函数调用之外,调用栈同时也用于寄存根本类型的数据。以上一大节的代码为例,在申明变量后,调用栈能够粗略示意如下图:

在下面这张图中,我对内存地址进行了形象,以显示每个变量的值,但请记住,(正如之前所说的)变量始终指向某一块保留着某个值的内存空间。这是了解 let vs const 这一大节的要害。

再来看一下堆。

堆是援用类型变量寄存的中央。堆绝对于栈的一个要害区别就在于,堆能够寄存动静增长的无序数据 —— 尤其是数组和对象。


JS 援用类型的变量申明和赋值

在变量申明与赋值这方面,援用类型变量与根本类型变量的行为表现有很大的差别。

咱们同样从一个简略的例子讲起。上面申明一个名为 myArray 的变量并初始化为一个空数组:

let myArray = []

当你申明一个变量 myArray 并通过援用类型数据(比方 [])为它赋值的时候,在内存中的操作是这样的:

  1. 为变量创立一个惟一的标识符(myArray
  2. 在堆内存中调配一块空间(将在运行时实现调配)
  3. 这个空间寄存着此前所赋的值(空数组 []
  4. 在栈内存中调配一块空间
  5. 这个空间寄存着指向被调配的堆空间的地址

咱们能够对 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 课程或者一本书中,都会提倡咱们多应用 constlet,少应用 var,但他们基本上都没有解释这其中的原因。很多初学者会纳闷为什么有些用 const 申明的变量在“批改”的时候的确会报错,而有些变量却不会。我可能了解,正是这种反直觉的体验让他们更喜爱随处都应用 let,毕竟谁也不想踩坑嘛。

不过,这并不是咱们举荐的形式。Google 作为一家领有顶尖程序员的公司,它的 JavaScript 格调指南中就有这么一段话:用 const 或者 let 申明所有的局部变量。除非一个变量有从新赋值的须要,否则默认应用 const 进行申明。绝不允许应用 var关键字 (起源)。

尽管他们没有指出个中原因,不过我认为有上面这些理由:

  1. 事后防止未来可能产生的 bug
  2. const 申明的变量在申明的时候就必须进行初始化,这会疏导开发者关注这些变量在作用域中的体现,最终有助于促成更好的内存治理与性能体现。
  3. 带来更好的可读性,任何接管代码的人都能晓得,哪些变量是不可批改的(就 JS 而言),哪些变量是能够从新赋值的。

心愿本文可能帮忙你了解应用 const 或者 let 申明变量的个中原因以及利用场景。

参考:

  1. Google JS Style Guide
  2. Learning JavaScript: Call By Sharing, Parameter Passing
  3. How JavaScript works: memory management + how to handle 4 common memory leaks

交换

目前专一于前端畛域和交互设计畛域的学习,酷爱分享和交换。感兴趣的敌人能够关注公众号,一起学习和提高。

正文完
 0