JS栈和堆:数据是如何存储的

发布时间:2024年01月10日

背景

JS有多种数据类型:数字型,字符串型,数组型等,虽然 JavaScript 并不需要直接去管理内存,但是实际项目中为了能避开一些不必要的坑,还是需要了解数据在内存中的存储方式。例如下面两段示例代码:

function foo() {
  var a = 1
  var b = a
  a = 2
  console.log(a)
  console.log(b)
}
foo()
// 执行结果:
// 2
// 1
function foo() {
  var a = {name: 'yy'}
  var b = a
  a.name = 'qq'
  console.log(a)
  console.log(b)
}
foo()
// 执行结果:
// {name: 'qq'}
// {name: 'qq'}

第一段代码没什么难以理解的,但是如果你对第二段代码感到迷惑,想要彻底弄清楚这个问题,就需要先从 JavaScript 这种语言说起。

JavaScript 是什么类型的语言

每种编程语言都具有内建的数据类型,但不同语言它们的数据类型常有不同之处,使用方式也很不一样。比如 C 语言在定义变量之前,就需要确定变量的类型,我们将这种称为静态语言。相反地,像 JavaScript 这种在运行过程中需要检查数据类型的语言称为动态语言

虽然 C 语言是静态语言,但是在 C 语言中我们可以把其他类型数据赋予给一个声明好的变量,比如将 int 型变量赋值给 bool 型变量,因为在赋值过程中,C 编译器会把 int 型的变量悄悄转换为 bool 型的变量,通常将这种偷偷转换的操作称为隐式类型转换,而支持隐式类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类型语言。这这点上,C 和 JavaScript 都是弱类型语言。

在这里插入图片描述

JavaScript 的数据类型

现在我们知道,JavaScript 是一种弱类型、动态语言。意味着:

  • 弱类型:你不需要告诉 JavaScript 引擎这个变量是什么数据类型,JavaScript 引擎在运行代码时会自己计算出来
  • 动态:你可以使用同一个变量保存不同类型的数据(变量没有数据类型,值才有数据类型)

JavaScript 中的数据类型一共有 8 种,它们分别是:

类型描述
Booleantruefalse 两个值
Nullnull
Undefined一个没有被赋值的变量会有个默认值 undefined,变量提升时的默认值也是 undefined
Number数字型
BigIntJavaScript 中的任意精度整数,可以安全存储和操作大整数,即使超出 Number 能够表示的安全整数范围。(Number 数据类型大于或等于 2 的 1024 次方的数值 JavaScript 无法表示,会返回 infinity,ES2020 引入一种新的数据类型 BigInt 来解决这个问题)
String字符串
Symbol符号类型是唯一的并且是不可修改的,通常用来作为 Object 的 key
Object在 JavaScript 中,对象可以看作是一组属性的集合

需要注意的是:

  • 使用 typeof 检测 null 时,返回的是 object,这是当初 JavaScript 语言的一个 Bug,为了兼容老的代码所以一直保留至今
  • Object 类型比较特殊,它是由上述 7 种类型组成的一个包含了 key,value键值对的数据类型
  • 我们把前面 7 中数据类型称为原始类型,最后一个对象类型称为引用类型,因为它们在内存中存放的位置不一样。

内存空间

在 JavaScript 执行过程中,只要有三种类型内存空间,分别是代码空间栈空间堆空间。其中的代码空间主要是存储可执行代码。今天主要来介绍栈空间和堆空间。

栈空间和堆空间

这里的栈空间就是在 JS 调用栈 文中反复提及的调用栈,先来看下面这段示例代码:

function foo() {
  var a = 'yy'
  var b = a
  var c = {name: 'qq'}
  var d = c
}
foo()

JS 调用栈 这篇文章中讲解过,当执行一段代码时,需要先编译并创建执行上下文,然后再按照顺序执行代码。下图是执行到 b = a 这行代码时其调用栈的状态图,可以参考:

在这里插入图片描述

此时,变量 a 和 变量 b 的值都被保存在执行上下文中,而执行上下文又被压入栈中,所以也可以认为变量 a 和变量 b 的值都是存放在栈中的。

接下来继续执行 c = {name: 'qq'} 这行代码,由于 JavaScript 引擎判断右边的值是一个引用类型,此时 JavaScript 引擎不是直接将该对象存放在变量环境中,而是将它分配到堆空间里,分配后该对象会有一个在“堆”中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的示例图如下:

在这里插入图片描述

从上图可以清晰观察到,对象类型时存放在堆空间的,栈空间只保留了对象的引用地址,当 JavaScript 需要访问该数据时,是通过栈中的引用地址来访问的。

为何一定要分“堆”和“栈”两个存储空间呢?所有数据直接都存放在“栈”中可以么?
不可以。因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了所有数据都存放在栈空间中,会影响到上下文切换的效率,进而影响到整个程序的执行效率。
例如文中的 foo 函数执行结束了,JavaScript 引擎需要离开当前的执行上下文,只需要将指针下移到全局上下文的地址就行了,foo 函数执行上下文栈区空间全部回收。所以,通常情况下,栈空间都不会设置太大。

在 JavaScript 中,赋值操作和其他语言有很大的不同,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。所以 d = c 这行代码的操作就是把 c 的引用地址赋值给了 d,具体可参考下图:

在这里插入图片描述

从图中看出,变量 c 和变量 d 都指向了同一个堆中的对象。这就很好解释了在文章开头的示例代码 2 中,通过 a 修改 name 的值,变量 b 的值也会跟着改变,因为归根到底它们是同一个对象。

再谈闭包

在知道了作用域内的原始数据类型会被存储到栈空间,引用类型会被存储到堆空间,基于这两点的认知,进一步探讨下闭包的内存模型。

关于什么是闭包,可以参考文章 JS作用域链和闭包 这篇文章。

还是以这段代码为例:

function foo() {
  var myname = 'yy'
  let test1 = 1
  const test2 = 2
  var innerbar = {
    getName: function() {
      console.log(test1)
      return myname
    },
    setName: function(newName) {
      myname = newName
    }
  }
  return innerbar
}
var bar = foo()
bar.setName('qq')
bar.getName()
console.log(bar.getName())

由于变量 mynametest1test2 都是原始类型数据,所以在执行 foo 函数时,它们会被压入到调用栈中;当 foo 函数执行结束后,调用栈中的 foo 函数的执行上下文会被销毁,其内部变量 mynametest1test2 也应该一同被销毁。但是根据 JS作用域链和闭包 文中的分析,由于 foo 函数产生了闭包,所以变量 mynametest1 并没有被销毁,而是保存在内存中,现在我们站在内存模型的角度来分析这段代码的执行流程:

文章来源:https://blog.csdn.net/weixin_43443341/article/details/135342494
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。