Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

什么变量是存储在堆/栈? #85

Open
sisterAn opened this issue Apr 12, 2021 · 0 comments
Open

什么变量是存储在堆/栈? #85

sisterAn opened this issue Apr 12, 2021 · 0 comments

Comments

@sisterAn
Copy link
Owner

sisterAn commented Apr 12, 2021

什么变量保存在堆/栈中?

看到这个问题,第一反应表示很简单,基本类型保存在栈中,引用类型保存到堆中✌️✌️✌️,但仅仅就如此简单吗?我们接下来详细看一看

JS 数据类型

我们知道 JS 就是动态语言,因为在声明变量之前并不需要确认其数据类型,所以 JS 的变量是没有数据类型的,值才有数据类型,变量可以随时持有任何类型的数据

JS 值有 8 种数据类型:

  • Boolean:有 truefalse
  • Undefined:没有被赋值的变量或变量被提升时的,都会有个默认值 undefined
  • Null:只有一个值 null
  • Number:数字类型
  • BigInt(ES10):表示大于 253 - 1 的整数
  • String:字符类型
  • Symbol(ES6)
  • Object:对象类型

其中前 7 种数据类型称为基本类型,把最后一个对象类型称为引用类型

JS中的变量存储机制

JS 内存空间分为栈(stack)空间、堆(heap)空间、代码空间。其中代码空间用于存放可执行代码。

栈空间

栈是内存中一块用于存储局部变量和函数参数的线性结构,遵循着先进后出 (LIFO / Last In First Out) 的原则。栈由内存中占据一片连续的存储空间,出栈与入栈仅仅是指针在内存中的上下移动而已。

JS 的栈空间就是我们所说的调用栈,是用来存储执行上下文的,包含变量空间与词法环境,var、function保存在变量环境,let、const 声明的变量保存在词法环境中。

var a = 1
function add(a) {
  var b = 2
  let c = 3
  return a + b + c
}

// 函数调用
add(a)

这段代码很简单,就是创建了一个 add 函数,然后调用了它。

下面我们就一步步的介绍整个函数调用执行的过程。

在执行这段代码之前,JavaScript 引擎会先创建一个全局执行上下文,包含所有已声明的函数与变量:

从图中可以看出,代码中的全局变量 a 及函数 add 保存在变量环境中。

执行上下文准备好后,开始执行全局代码,首先执行 a = 1 的赋值操作,

赋值完成后 a 的值由 undefined 变为 1,然后执行 add 函数,JavaScript 判断出这是一个函数调用,然后执行以下操作:

  • 首先,从全局执行上下文中,取出 add 函数代码
  • 其次,对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码,并将执行上下文压入栈中

  • 然后,执行代码,返回结果,并将 add 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。

至此,整个函数调用执行结束了。

上面需要注意的是:函数(add)中存放在栈区的数据,在函数调用结束后,就已经自动的出栈,换句话说:栈中的变量在函数调用结束后,就会自动回收。

所以,通常栈空间都不会设置太大,而基本类型在内存中占有固定大小的空间,所以它们的值保存在栈空间,我们通过 按值访问 。它们也不需要手动管理,函数调时创建,调用结束则消失。

堆数据结构是一种树状结构。它的存取数据的方式与书架和书非常相似。我们只需要知道书的名字就可以直接取出书了,并不需要把上面的书取出来。

在栈中存储不了的数据比如对象就会被存储在堆中,在栈中只是保留了对象在堆中的地址,也就是对象的引用 ,对于这种,我们把它叫做 按引用访问

举个例子帮助理解一下:

var a = 1
function foo() {
  var b = 2
  var c = { name: 'an' } // 引用类型
}

// 函数调用
foo()

所以,堆空间通常很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间

JS中的变量存储机制与闭包

对以上总结一下,JS 内存空间分为栈(stack)空间、堆(heap)空间、代码空间。其中代码空间用于存放可执行代码

  • 基本类型:保存在栈内存中,因为这些类型在存中分别占有固定大小的空间,通过按值来访问。
  • 引用类型:保存在堆内存中,因为这种值的大小不固定,因此不能把它们保存到栈内存中,但内存地址大小的固定的,因此保存在堆内存中,在栈内存中存放的只是该对象的访问地址。当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。

闭包

那么闭包喃?既然基本类型变量存储在栈中,栈中数据在函数执行完成后就会被自动销毁,那执行函数之后为什么闭包还能引用到函数内的变量?

function foo() {
  let num = 1 // 创建局部变量 num 和局部函数 bar
  function bar() { // bar()是函数内部方法,是一个闭包
      num++
      console.log(num) // 使用了外部函数声明的变量,内部函数可以访问外部函数的变量
  }
  return bar // bar 被外部函数作为返回值返回了,返回的是一个闭包
}

// 测试
let test = foo()
test() // 2
test() // 3

在执行完函数 foo 后,foo 中的变量 num 应该被弹出销毁,为什么还能继续使用喃?

这说明闭包中的变量没有保存在栈中,而是保存到了堆中:

console.dir(test)

所以 JS 引擎判断当前是一个闭包时,就会在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JS 是无法访问的),用来保存 num 变量

注意,即使不返回函数(闭包没有被返回):

function foo() {
  let num = 1 // 创建局部变量 num 和局部函数 bar
  function bar() { // bar()是函数内部方法,是一个闭包
      num++ 
      console.log(num) // 使用了外部函数声明的变量,内部函数可以访问外部函数的变量
  }
  bar() // 2
  bar() // 3
  console.dir(bar)
}

foo()

总结

JS 就是动态语言,因为在声明变量之前并不需要确认其数据类型,所以 JS 的变量是没有数据类型的,值才有数据类型,变量可以随时持有任何类型的数据, JS 值有 8 种数据类型,它们可以分为两大类——基本类型和引用类型。其中,基本类型的数据是存放在栈中,引用类型的数据是存放在堆中的。堆中的数据是通过引用和变量关联起来的。

闭包除外,JS 闭包中的变量值并不保存中栈内存中,而是保存在堆内存中。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant