本章继《JavaScript进阶-执行上下文栈和变量对象(一周一更)》之后继续深入学习JS
的基础知识.
上面我们已经介绍了很多关于JS
中执行上下文以及变量对象的知识, 而现在我要讲解的是JS
的内存空间.
这一章你会学习到:
-
三种数据结构: 堆(heap)、栈(stack)、队列(queue)
-
变量的存放
-
内存空间管理
JS
中三种重要的数据结构, 如图:
(图片来源前端九五六-Javascript 内存空间管理)
其实在《JavaScript执行上下文》中我就已经提到了执行栈, 让我们一起来回顾一下:
栈的特点: 后进先出(LIFO)的结构.
(LIFO
: last-in, first-out
,类似于向乒乓球桶中放球,最先放入的球最后取出)
这里还是贴上一张网图方便大家理解的好:
栈中的数据就像是一个个乒乓球, 最先进去的最后出来.
注
这里所说的进栈和出栈不是指赋值算进, 使用算出. 而是指赋值算进, 被清理算出, 而且位于同一函数作用域下的变量, 应该是在栈的同一层.
所谓的变量存储于栈内存中的栈,传统意义上说指的是由内存自动创建分配的空间,例如函数的参数值与局部变量,只是其操作方式类似于栈操作,所以叫栈内存。
比如函数调用其实就相当于栈的形式:
例子🌰:
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
格式的存储是无序的, 所以没有先后顺序, 所以它是一种绝对公平的数据结构.
如图所示:
队列数据结构不同于堆, 队列是一种**先进先出(FIFO)**的数据结构.
它也是**事件循环(Event Loop)**的基础结构.
如图所示:
最先进入队列的任务最先出来, 类似于你排队买票, 排在前面的人先买.
通过上面的介绍我们知道了, 内存中有堆了栈, 那么JS
变量具体是存放在哪里呢?
- 基本数据类型保存在栈内存中;
- 引用数据类型保存在堆内存中.
- 基本数据类型6种:
Undefined、Null、Boolean、Number、String、Symbol
, 由于他们在内存中分别占有固定大小的空间, 通过按值来访问. - 引用数据类型: 也就是
Object
对象, 它的存储分为访问地址和实际存放的地方; 访问地址是存储在栈中的, 当查询引用类型变量的时候, 会先从栈中读取内存地址(也就是访问地址), 然后再通过地址找到堆中的值.因此, 这种我们也把它叫为引用访问.
一张图方便你理解🤔
在计算机的数据结构中,栈比堆的运算速度快,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
. - 案例三中, 开始时,
obj1
和obj2
指向的都是同一堆内存对象{a: 1}
, 在第三步将obj1
赋值为null
仅仅只是改变了栈中obj1
的内存地址,将它变为了基本数据类型null
, 并不会影响堆内存对象. 同样的, 你要将obj1
不赋值为null
, 而是赋值为{b: 2}
, 对obj2
也还是没有影响.
在上面我们说了那么多的栈内存, 堆内存, 那么在JS
中, 是怎样管理这些内存空间的呢?
首先, 同样的, 内存空间也是有属于自己的生命周期, 它主要分为三个阶段:
- 分配你所需的内存;
- 使用分配到的内存(读、写);
- 不需要的时候将其释放、归还.
我们可以用个例子来看一下看.
案例一🌰:
var a = 1; // 在内存中给数值变量分配空间
alart(a + 2); // 使用内存
a = null; // 使用完后, 释放内存空间
上面三步分别对应着三个阶段. 当然, a = null
这个操作是我们手动将a
的内存空间释放. 若没有这个过程, JS
会自己帮我做一些释放内存的工作吗? 答案当然是肯定的.
JS
有自动垃圾收集机制, 听着这个机制的名字我想大家就知道它是做什么的了, 没错就是字面意思, 它会找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。
在自动垃圾收集机制中, 最常用的就是通过标记清除的算法来找到哪些对象不再继续使用. 其实上面我说的将a = null
手动释放内存其实是不准确的. 因为使用a = null
仅仅只是做了一个释放引用的操作, 让a
原本对应的值失去引用, 脱离执行环境, 这个值会在下一次垃圾收集器执行操作的时候被找到并释放.
还有一点, 在局部作用域中, 当函数执行完毕了之后, 局部变量就没有存在下去的必要了, 此时垃圾收集器知道这类变量是需要回收的, 所以很容易判断.
但是全局变量什么时候需要释放内存空间则很难判断,因此在我们的开发中,原则上应该避免使用全局变量。
掌握好JS
中的内存空间的基础知识, 才能避免在实际开发中产生的一系列性能问题.
参考文章:
知识无价, 支持原创