变量存储

由于不同引擎下变量存储方式会有些许不同,因此本文的结论都是依据的 V8 引擎。

我们通常认为:基本类型存储在栈中,这些类型的值会分配固定大小的内存;引用类型存储在堆中,值的大小不固定,栈中存的只是一个堆内存的引用地址。

举个例子:

let newVar = 23
let myNumber = 24
let myString = 'abcd'
let myArray = []

对应的存储略图:

如果我们将 newVar 赋给 myNumber 呢?

结果是并不会开辟新内存,而是两个变量指向同一个内存地址对应的值。

现在我们执行:

myNumber++

既然 newVarmyNumber 指向同样的内存地址,那么 myNumber 会不会随着 newVar 的改变而改变呢?答案是不!

由于 JS 中的原始数据类型是不可变的,当 myNumber ++ 执行后,JS 会在内存中分配一个新地址,将 24 作为其值存储,myNumber 将指向新地址。

在步入 ES6+ 时代之后,我们应该尽可能地使用 const,而只有在变量可能被改变的情况下使用 let

使用 const 声明的变量,其 call stack 中所对应的 值/堆内存地址 是不能发生变化的。但引用类型在堆中的值是可以发生变化的,只要保证引用地址指向不变即可。

栈是一种先进后出的数据结构,因此如果我们执行一个函数,其中声明的变量,在函数执行结束之后就会自动出栈。

那么这样就有一个问题:闭包中的变量是怎么存储的呢

假如有如下代码:

function b() {
  let c = 1
  return function() {
    return ++c
  }
}
const d = b()
d() // 2
d() // 3

如果说当函数 b 执行完就将内部声明的所有变量出栈,那么为什么后续执行 d 可以获得内部变量 c 的值呢?

我们不妨在控制台调试一下上面的代码:

可以看到,V8 将包含全局变量在内的所有变量放到了一个叫 Scope 的空间中。

在执行到第 14 行之前(执行完第 13 行),函数 b 被放入了全局 Global 对象中,变量 c 还没赋值,因此为 undefined

当执行到第 16 行之前(执行完第 15 行之后),Scope 中新增了一个 Closure(b) 也就是函数 b 的闭包,闭包中包含先前声明的变量 c

我们再多声明几个闭包内没有用到的变量:

function b() {
  let c = 1
  let e = 2
  let f = []
  return function() {
    return ++c
  }
}
const d = b()
d() // 2
d() // 3


 
 







在执行到 16 行后 17 行前,cef 都在 Local 中。

执行完第 17 行,我们发现,Closure只会保留闭包函数中用到的变量。那么问题来了,我们先前说基本类型都是存储在栈内存中,既然变量 ef 都出栈了,为什么 c 可以保留下来呢?

因此在这个情况下,基本类型也是存储在堆内存而不是栈内存中的。

并且,整个 Scope 也是存在堆内存而非栈内存中的,我们可以访问到这个对象:

function b() {
  let c = 1
  let e = 2
  let f = []
  return function() {
    return ++c
  }
}
const d = b()
console.dir(d)

由此我们可以得知,V8 在堆内存中使用一个特殊的 Scope 对象来保存变量。

但这并不能推翻基本变量存在栈内存中这个说法,我们可以将依照上述性质将变量分成三种:

  1. 局部变量
  2. 全局变量
  3. 被捕获变量

前面两个不用说,一个存储在 Local,一个存储在 Global 中。被捕获变量则不同于局部变量,虽然同在函数中声明,但函数 return 后仍有未执行的作用域使用到了这些变量,这些变量就叫做被捕获变量。

因此,除了局部变量中的基本类型值之外,其余的变量全部存储在堆中

例外:存储大小限制。

我们知道,栈中存储的值大小是固定的,由于操作系统对每组线程的栈内存有一定的限制,为适应线程各种操作系统,所以 Node.js 默认的栈大小为 984k。

我们假设调用栈最大为 984k,假如我在函数中声明了一个大小为 1MB 的字符串,调用栈放不下该怎么办呢?

因此,当这个字符串大到调用栈放不下的时候,就会被存到堆内存当中!在v8引擎中(很多别的编程语言也是这么做的),对值得驻留的字符串内存中相同的字符串只会保存一份,值得驻留的字符串指的是在有些场景下会重复出现的字符串,当两个变量保存相同的字符串时,它们实际上是保存了这个字符串在内存中的地址。这叫作字符串驻留

注意:v8内部有一个名为stringTable的hashmap缓存了所有字符串,在V8阅读我们的代码,转换抽象语法树时,每遇到一个字符串,会根据其特征换算为一个hash值,插入到hashmap中。在之后如果遇到了hash值一致的字符串,会优先从里面取出来进行比对,一致的话就不会生成新字符串类。这也是为什么我们不能直接用下标的方式修改字符串: V8 中的字符串都是不可变的

那么对于基本类型的存储方式来说:

  1. 字符串:存在堆里,栈中为引用地址,如果存在相同字符串,则引用地址相同。
  2. 数字:小整数存在栈中,其他类型存在堆中。
  3. 其他类型:引擎初始化时分配唯一地址,栈中的变量存的是唯一的引用。

参考文章