引言
JS的内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈中)
其中栈存放变量,堆存放复杂对象,池存放常量,所以也叫常量池
一、栈内存
1、访问顺序
栈是一种后进先出(先进后出)(LIFO,last-in-first-out)的数据结构,栈内存是内存中用于存放临时变量的一片内存块。
它是一种特殊的列表,栈内的元素只能通过列表的一端访问,这一端称为栈顶,另一端称为栈底。
2、存储数据
一般来说,栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null、Symbol 以及对象变量的指针
为方便大家理解,这里我们通过类比乒乓球盒子来分析栈的存取方式。
image.png
这种乒乓球的存放方式与栈中存取数据的方式如出一辙。
处于盒子中最顶层的乒乓球 5,它一定是最后被放进去,但可以最先被使用。
而我们想要使用底层的乒乓球 1,就必须将上面的 4 个乒乓球取出来,让乒乓球1处于盒子顶层。
这就是栈空间先进后出,后进先出的特点。
为了更好的搞懂基本数据类型变量与栈内存,我们结合以下例子与图解进行理解:
let num1 = 1;
let num2 = 1;
image.png
PS: 需要注意的是闭包中的基本数据类型变量不保存在栈内存中,而是保存在堆内存中。这个问题,我们后文再说
二、堆内存
1、访问顺序
堆内存的存储不同于栈,虽然他们都是内存中的一片空间,但是堆内存存储变量时没有什么规律可言。它只会用一块足够大的空间来存储变量
2、存储数据
堆内存主要负责像对象Object这种变量类型的存储,堆内存存储的对象类型数据对于大小这方面,一般都是未知的,(这大概也是为什么null作为一个object类型的变量却存储在栈内存中的原因)
为了更好的搞懂变量对象与堆内存,我们结合以下例子与图解进行理解。
// 基本数据类型-栈内存
let a1 = 0;
// 基本数据类型-栈内存
let a2 = 'this is string';
// 基本数据类型-栈内存
let a3 = null;
// 对象的指针存放在栈内存中,指针指向的对象存放在堆内存中
let b = { m: 20 };
// 数组的指针存放在栈内存中,指针指向的数组存放在堆内存中
let c = [1, 2, 3];
image.png
因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量中获取了该对象的地址指针, 然后再从堆内存中取得我们需要的数据。
从内存角度来看变量复制
基本数据类型的复制
let a = 20;
let b = a;
b = 30;
console.log(a); // 此时a的值是多少,是 30?还是 20?
答案是:20
在这个例子中,a、b 都是基本类型,它们的值是存储在栈内存中的,a、b 分别有各自独立的栈空间, 所以修改了 b 的值以后,a 的值并不会发生变化
从下图可以清晰的看到变量是如何复制并修改的
image.png
引用数据类型的复制
let m = { a: 10, b: 20 };
let n = m;
n.a = 15;
console.log(m.a) //此时m.a的值是多少,是10?还是15?
答案是:15
在这个例子中,m、n都是引用类型,栈内存中存放地址指向堆内存中的对象,引用类型的复制会为新的变量自动分配一个新的值保存在变量中,但只是引用类型的一个地址指针而已,实际指向的是同一个对象,所以修改 n.a 的值后,相应的 m.a 也就发生了改变。
从下图可以清晰的看到变量是如何复制并修改的。
image.png
栈内存和堆内存的优缺点
在JS中,基本数据类型变量大小固定,并且操作简单容易,所以把它们放入栈中存储。引用类型变量大小不固定,所以把它们分配给堆中,让他们申请空间的时候自己确定大小,这样把它们分开存储能够使得程序运行起来占用的内存最小。
栈内存由于它的特点,所以它的系统效率较高。堆内存需要分配空间和地址,还要把地址存到栈中,所以效率低于栈。
栈内存和堆内存的垃圾回收
栈内存中变量一般在它的当前执行环境结束就会被销毁被垃圾回收制回收, 而堆内存中的变量则不会,因为不确定其他的地方是不是还有一些对它的引用。 堆内存中的变量只有在所有对它的引用都结束的时候才会被回收。
闭包与堆内存
闭包中的变量并不保存中栈内存中,而是保存在堆内存中。 这也就解释了函数调用之后之后为什么闭包还能引用到函数内的变量。
我们先来看什么是闭包:
function A() {
let a = 1;
function B() {
console.log(a);
}
return B;
}
let res = A();
函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
函数 A 弹出调用栈后,函数 A 中的变量这时候是存储在堆上的,所以函数B依旧能引用到函数A中的变量。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。
网友评论