我穿越了,文章写于2019-11-24。
前言
" 相信接触过Java、Python等后端语言的同学来说,内存空间是一个老生常谈的话题,但是作为前端开发的同学来说,这个并不会被经常提及,并且在日常编码中并不会注意一些细节问题。比如面试中常会问到的深拷贝与浅拷贝、按值传递和按引用传递等,如果从内存空间的角度去理解,会发现对你JS又有了更深的理解。 "
01 内存生命周期
在JS中,一般可以将内存分为两种:栈(包含池)、堆。其声明周期可以大概分为三个阶段:
- 内存分配(声明)
- 内存读写(使用)
- 内存回收(销毁)

02 栈内存与基础数据类型
JS基础数据类型:Number、String、Boolean、Null、undefined、symbol、bigInt(自行了解)。
以上这些值都有固定大小,一般都保存在栈内存中,由系统分配固定的内存空间大小。我们在操作的时候直接操作内存中的值,即也就是在传递中是按值传递,遵循后进先出的原则进行读取。
需要强调的是,JS在执行过程中执行流进入一个函数时,会生成一个当前函数的执行环境(EC),并且推入到栈内存中,即我们所说的环境栈。所以JS在执行过程中消耗的是栈内存。
03 堆内存与引用数据类型
JS引用数据类型:Object、Array、Date、RegExp等。
引用数据类型的值时保存在堆内存中的对象,其内存空间大小是动态分配的。需要注意的是,JavaScript中不能直接访问堆内存中的位置,即不能直接操作对内存对象。而我们在平时操作引用数据类型的时候,其实操作的是这个对象的内存地址。相对应的,这个地址会被保存在栈内存中,形成一个映射关系。
举个例子:
js
var a = { b: 1 };
var c = 100;
function fn(a1, c1) {
a1.b = 1000;
a1 = null;
c1 = 200;
}
fn(a, c);
console.log(a);
console.log(c);
这是一道简单的题目,但是很好的表达了上面的堆栈原理。
答案是:{b: 1000}和100。
当传递a
的时候,实际上传递的是当前a
对象的引用(地址)给了a1
,其存储在栈内存中,执行a1.b = 1000
进行修改的时,会通过这个地址去操作堆内存的对象,将b
的值改为1000
;接着a1
赋值为null
,那么这时候a1
就不会再指向堆内存,但是a
的引用还是存在,所以对象变为修改后的{b: 1000}
,这就是所谓的按引用传递。
而当传递实参c
给形参c1
的过程中,会把100
直接复制新的一份给了c1
,此时修改c1
为200
后,实际上操作的就是c1
这个变量,而不会影响原来的c
,即该过程是按值传递。
04 内存回收
在JavaScript
中,自带有垃圾回收机制。其工作原理是会在每个固定时间执行一次内存释放操作,它内部有一套具体的回收机制,通过这个机制去找那些不再使用的值,回收对应内存。
标记清除法
这个是目前JavaScript最常用的垃圾收集方式。
请看下面代码
js
var a = 100;
console.log(a + 10086);
a = null;
可以理解为当JS执行流进入这个环境的时候,会首先将a
标记为"进入环境",当a
离开环境时(a = null)
,则将其标记为"离开环境"。在垃圾收集器运行时,回收标记为"离开环境"的变量,释放内存。
引用计数法
这个是早期Netscape浏览器下一种不太常见的垃圾收集策略,由于这种方式不能回收循环引用的变量,导致对应内存不能被及时回收。后面放弃了这种方式,转而采用了标记清除来实现。
引用计数算法定义"内存不再使用"的标准很简单,就是看一个变量的引用次数是否为0。如果为0,说明该变量已经不再需要了。
综合以上,我们在日常开发中需要注意当变量不需要再使用的时候,请将其内存及时归还(赋值为null),特别是全局变量、定时器、DOM节点、闭包等等。