面试官会抛出类似 “闭包中与内存泄漏之间的关系”?
面试官很多时候都不是为了就事论事,而是准备深挖 JS 的内存管理、垃圾回收机制、以及内存泄漏成因等等具有相当深度的问题。
栈内存与堆内存
基本类型被放在 JS 的栈内存里存储;引用类型被放在 JS 的堆内存里存储。
引用计数法
当我们用一个变量指向了一个值,那么就创建了一个针对这个值的 “引用”:
const students = ['baomboo', '小竹子'];
大家知道赋值表达式是从右向左读的。这行代码首先是开辟了一块内存,把右侧这个数组塞了进去,此时这个数组就占据了一块内存。随后 students 变量指向它,这就是创建了一个指向该数组的 “引用”。 在引用计数法的机制下,内存中的每一个值都会对应一个引用计数。当垃圾收集器感知到某个值的引用计数为 0 时,就判断它 “没用” 了,随即这块内存就会被释放。
引用计数法缺陷的地方?
function recovery() {
var recoveryObj1 = {};
var recoveryObj2 = {};
recoveryObj1.target = recoveryObj2;
recoveryObj2.target = recoveryObj1;
}
recovery()
函数作用域的生命非常短暂,当函数(recovery())执行完之后,作用域内的变量也会全部被视作 “垃圾” 进而移除。但如果用了引用计数法,那么即使 recovery 执行完毕,recoveryObj1 和 recoveryObj2 还是会活得好好的 —— 因为 recoveryObj2 的引用计数为 1(recoveryObj1.target),而 recoveryObj1 的引用计数也为 1 (recoveryObj2.target)。 所以引用计数法无法甄别循环引用场景下的 “垃圾回收”。
标记清除法
考虑到引用计数法存在严重的局限性,标记清除法则是现代浏览器的标准垃圾回收算法。 这个算法有两个阶段,分别是标记阶段和清除阶段:
- 标记阶段:垃圾收集器会先找到根对象,在浏览器里,根对象是 Window;在 Node 里,根对象是 Global。从根对象出发,垃圾收集器会扫描所有可以通过根对象触及的变量,这些对象会被标记为 “可抵达”。
- 清除阶段: 没有被标记为 “可抵达” 的变量,就会被认为是不需要的变量,这波变量会被清除。
当recovery执行完,从根对象 Window 出发,recoveryObj1 和 recoveryObj2 都会被识别为不可抵达的对象,它们会按照预期被清除掉。这样一来,循环引用的问题,就被标记清除干脆地解决掉了。
闭包与内存泄漏
该释放的变量(内存垃圾)没有被释放,仍然霸占着原有的内存不松手,导致内存占用不断攀高,带来性能恶化、系统崩溃等一系列问题,这种现象就叫内存泄漏。
在面试过程中,内存泄漏这个点会被一些面试官通过闭包来引出。内存泄漏其实并不是啥高深的命题,导致内存泄露的,往往是低级错误。说得直接点,那就是代码功夫不到家。
事实上,单纯由闭包导致的内存泄漏,极少极少(除非你的操作极其不规范,但那就不是闭包的问题了,是代码写得有问题)。
内存泄漏场景
1、“手滑” 导致的全局变量function() { name: 'bamboo' },name而非var name;
2、忘记清除的 setInterval 和 setTimeout;
3、清除不当的 DOM
...