JavaScript进阶-内存空间详解(双十一过后的一更)

1,280 阅读7分钟

前言

本章继《JavaScript进阶-执行上下文栈和变量对象(一周一更)》之后继续深入学习JS的基础知识.

上面我们已经介绍了很多关于JS中执行上下文以及变量对象的知识, 而现在我要讲解的是JS的内存空间.

这一章你会学习到:

  • 三种数据结构: 堆(heap)、栈(stack)、队列(queue)

  • 变量的存放

  • 内存空间管理

三种数据结构

JS中三种重要的数据结构, 如图:

img1

(图片来源前端九五六-Javascript 内存空间管理)

栈数据结构

其实在《JavaScript执行上下文》中我就已经提到了执行栈, 让我们一起来回顾一下:

栈的特点: 后进先出(LIFO)的结构.

LIFO: last-in, first-out,类似于向乒乓球桶中放球,最先放入的球最后取出)

这里还是贴上一张网图方便大家理解的好:

img2

栈中的数据就像是一个个乒乓球, 最先进去的最后出来.

注⚠️

这里所说的进栈出栈不是指赋值算进, 使用算出. 而是指赋值算进, 被清理算出, 而且位于同一函数作用域下的变量, 应该是在栈的同一层.

所谓的变量存储于栈内存中的栈,传统意义上说指的是由内存自动创建分配的空间,例如函数的参数值与局部变量,只是其操作方式类似于栈操作,所以叫栈内存。

比如函数调用其实就相当于栈的形式:

例子🌰:

function fn1() {
	console.log(1)
  fn2()
}
function fn2() {
	console.log(2)
	fn3()
}
function fn3() {
	console.log(3)
}
fn1()

如上, 声明的顺序是1, 2, 3 , 但是释放的顺序是为3, 2, 1 .

这里释放按照这个顺序是因为 3最先执行完, 所以最先被释放.

堆数据结构

一种树状结构。好比 JSON 格式中的数据,你有 key,我有对应的 value, 就立马返给你。

因为我们知道JSON格式的存储是无序的, 所以没有先后顺序, 所以它是一种绝对公平的数据结构.

如图所示:

img3

队列数据结构

队列数据结构不同于堆, 队列是一种先进先出(FIFO) 的数据结构.

它也是事件循环(Event Loop) 的基础结构.

如图所示:

img4

最先进入队列的任务最先出来, 类似于你排队买票, 排在前面的人先买.

变量的存放

通过上面的介绍我们知道了, 内存中有堆了栈, 那么JS变量具体是存放在哪里呢?

  • 基本数据类型保存在内存中;
  • 引用数据类型保存在内存中.
  1. 基本数据类型6种: Undefined、Null、Boolean、Number、String、Symbol,(若是算上BigInt则有7种) 由于他们在内存中分别占有固定大小的空间, 通过按值来访问.
  2. 引用数据类型: 也就是Object对象, 它的存储分为访问地址实际存放的地方; 访问地址是存储在中的, 当查询引用类型变量的时候, 会先从中读取内存地址(也就是访问地址), 然后再通过地址找到中的值.因此, 这种我们也把它叫为引用访问.

一张图方便你理解🤔

img5

在计算机的数据结构中,栈比堆的运算速度快,Object是一个复杂的结构且可以扩展:数组可扩充,对象可添加属性,都可以增删改查。将他们放在堆中是为了不影响栈的效率。而是通过引用的方式查找到堆中的实际对象再进行操作。所以查找引用类型值的时候先去查找再去查找。

变量存放案例

要是你读完了上面的堆栈存储介绍还有点模糊的话, 我们不妨来看几个案例.

案例一🌰:

var a = 1;
var b = a;
b = 2;
console.log(a); // a = ?

案例二🌰:

var obj1 = { a: 1, b: 2 };
var obj2 = obj1;
obj2.a = 3;
console.log(obj1.a); // obj1.a = ?

案例三🌰:

var obj1 = { a: 1 };
var obj2 = obj1;
obj1 = null;
console.log(obj2); // obj2 = ?

上面三个案例的答案分别对应的是: 1、3、{ a: 1 }.

  • 案例一中, a和b都是基本数据类型, 它们的值分别存储在各自独立的栈空间中, 是互不影响的, 所以修改了b的值后a还是不变. var b = a 的操作, 你可以理解为单纯的b赋值了值1, 而后a和b没有任何关系了.
  • 案例二中, 创建obj1的时候, 在栈中存储了一个名为obj1的变量, 同时开辟了一个堆内存用于存放了{a: 1, b: 2}对象, obj1中存放的就是指向这个堆内存对象的地址. 因此obj2进行赋值的时候拷贝的只是obj1中的地址, 实际上它们指向的都是堆内存的对象.在第三步改变这个对象的值的时候, 也相当于同时改变了obj1.
  • 案例三中, 开始时, obj1obj2指向的都是同一堆内存对象{a: 1}, 在第三步将obj1赋值为null仅仅只是改变了栈中obj1的内存地址,将它变为了基本数据类型null, 并不会影响堆内存对象. 同样的, 你要将obj1不赋值为null, 而是赋值为{b: 2}, 对obj2也还是没有影响.

闭包中的变量

问❓: 是不是所有的变量都遵循: “基本数据类型存储在栈中, 对象数据类型存储在堆中” 的规律呢?

其实并不是完全正确的, 比如 闭包中的变量 就并不是保存在栈中, 而是保存于堆中.

因为如果变量存在栈中,那函数调用完栈顶空间销毁,闭包变量就会被清除掉了, 那还谈和闭包呢?

内存空间管理

在上面我们说了那么多的栈内存, 堆内存, 那么在JS中, 是怎样管理这些内存空间的呢?

首先, 同样的, 内存空间也是有属于自己的生命周期, 它主要分为三个阶段:

  1. 分配你所需的内存;
  2. 使用分配到的内存(读、写);
  3. 不需要的时候将其释放、归还.

我们可以用个例子来看一下看.

案例一🌰:

var a = 1; // 在内存中给数值变量分配空间
alart(a + 2); // 使用内存
a = null; // 使用完后, 释放内存空间

上面三步分别对应着三个阶段. 当然, a = null这个操作是我们手动将a的内存空间释放. 若没有这个过程, JS会自己帮我做一些释放内存的工作吗? 答案当然是肯定的.

JS有自动垃圾收集机制, 听着这个机制的名字我想大家就知道它是做什么的了, 没错就是字面意思, 它会找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。

在自动垃圾收集机制中, 最常用的就是通过标记清除的算法来找到哪些对象不再继续使用. 其实上面我说的将a = null手动释放内存其实是不准确的. 因为使用a = null仅仅只是做了一个释放引用的操作, 让a原本对应的值失去引用, 脱离执行环境, 这个值会在下一次垃圾收集器执行操作的时候被找到并释放.

还有一点, 在局部作用域中, 当函数执行完毕了之后, 局部变量就没有存在下去的必要了, 此时垃圾收集器知道这类变量是需要回收的, 所以很容易判断.

但是全局变量什么时候需要释放内存空间则很难判断,因此在我们的开发中,原则上应该避免使用全局变量。

后语

掌握好JS中的内存空间的基础知识, 才能避免在实际开发中产生的一系列性能问题.

参考文章:

木易杨前端进阶-JavaScript深入之内存空间详细图解

前端基础进阶(一):内存空间详细图解

前端九五六-Javascript 内存空间管理

关于js中 “栈空间的先进后出,后进先出” 的疑问?

知识无价, 支持原创