栈空间和堆空间:数据是如何存储的?

1,427 阅读4分钟

前言

JavaScript 中的数据是如何存储在内存中的。虽然 JavaScript 并不需要直接去管理内存,但是在实际项目中为了能避开一些不必要的坑,你还是需要了解数据在内存中的存储方式。

示例

首先,我们先看下面这两段代码:

function foo() {
  var a = 1
  var b = a
  a = 2
  console.log(a)
  console.log(b)
}
foo()

执行上面这段代码,打印出来 a 的值是 2b 的值是 1,这没什么难理解的。

function foo() {
  var a = {name:"极客时间"}
  var b = a
  a.name = "极客邦" 
  console.log(a)
  console.log(b)
}
foo()

执行第二段代码,你会发现,仅仅改变了 aname 的属性值,但是最终 ab 打印出来的值都是 {name:"极客邦"}。这就和我们预期的不一致了,因为我们想改变的仅仅是 a 的内容,但 b 的内容也同时被改变了。

要彻底弄清楚这个问题,我们就得先从“JavaScript 是什么类型的语言”讲起。

JavaScript 是什么类型的语言?

支持隐式类型转换的语言称为 弱类型语言,不支持隐式类型转换的语言称为 强类型语言。在这点上,C 和 JavaScript 都是弱类型语言。

  • 弱类型,意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来。
  • 动态,意味着你可以使用同一个变量保存不同类型的数据。

JavaScript 数据类型包括原始类型和引用类型,之所以把它们区分为两种不同的类型,是因为它们在内存中存放的位置不一样。到底怎么个不一样法呢?接下来,我们就来讲解一下 JavaScript 的原始类型和引用类型到底是怎么储存的。

内存空间

在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间。其中的代码空间主要是存储可执行代码的,本文主要来说说栈空间和堆空间。

栈空间和堆空间

栈空间是用来存储执行上下文的。为了搞清楚栈空间是如何存储数据的,我们还是先看下面这段代码:

function foo() {
    var a = "极客时间"
    var b = a
    var c = {name:"极客时间"}
    var d = c
}
foo()

当 JavaScript 执行一段代码时,需要先编译,并创建执行上下文,然后再按照顺序执行代码。那么下面我们来看看,当执行到第 3 行代码时,其调用栈的状态,你可以参考下面这张调用栈状态图:

从图中可以看出来,当执行到第 3 行时,变量 a 和变量 b 的值都被保存在执行上下文中,而执行上下文又被压入到栈中,所以你也可以认为变量 a 和变量 b 的值都是存放在栈中的。

接下来执行第 4 行代码,由于 JavaScript 引擎判断右边的值是一个引用类型,这时候处理的情况就不一样了,JavaScript 引擎并不是直接将该对象存放到变量环境中,而是将它分配到堆空间里面,分配后该对象会有一个在“堆”中的地址,然后再将该数据的地址写进 c 的变量值,最终分配好内存的示意图如下所示:

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

通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

我们还是回到示例代码那里,看看它最后一步将变量 c 赋值给变量 d 是怎么执行的?

在 JavaScript 中,原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。所以 d = c 的操作就是把 c 的引用地址赋值给 d,你可以参考下图:

从图中你可以看到,变量 c 和变量 d 都指向了同一个堆中的对象,所以这就很好地解释了文章开头的那个问题,通过 a 修改 name 的值,变量 b 的值也跟着改变,归根结底它们是同一个对象。

最后

以上就是 JavaScript 中数据的存储方式,如果觉得对你有帮助的话,不要忘了点赞哟~