在复习JavaScript基础的时候,我写了一份演示闭包内存泄漏的Demo,但在运行时,并没有发现内存的明显变化:
<button id="add">创建数组对象</button>
<br />
<button id="des">销毁数组对象</button>
<script>
const add = document.querySelector('#add')
const des = document.querySelector('#des')
function createArray() {
var arr = new Array(1024 * 1024).fill(1);
return function () {
console.log('create done');
}
}
let array = []
//批量创建闭包
add.addEventListener('click', () => {
for (let i = 0; i < 100; i++) {
array.push(createArray())
}
console.log(array,array.length);
})
//释放创建的闭包
des.addEventListener('click', () => {
array = []
console.log('销毁成功');
})
`</script>`
按照闭包的原理来说,createArray
函数执行后,返回的函数,已经捕获到了函数定义时的父级作用域,虽然我没有使用父级作用域中的变量,按照闭包的规则,这仍然是一个闭包
但实际上在浏览器运行代码时,内存并没有发生明显变化。
经过查询,我发现,虽然词法分析时候确实形成了闭包,但因为现代的Javascript引擎对闭包的实现进行了优化。
具体来说,只有当返回的函数实际使用了父级作用域中的变量时,才会保留这些变量在内存中的引用。
这种优化减少了不必要的内存占用,但并不改变闭包本身的概念。
所以,这实际上是一种引擎优化。
在V8引擎中被称之为:逃逸分析(Escape Analysis) 和 上下文优化
逃逸分析(Escape Analysis): "如果一个变量不会被嵌套函数访问,它被视为"不逃逸",可以分配到栈上或完全忽略。"
上下文优化: "在生成字节码的过程中,V8 会决定哪些变量需要分配到**上下文对象(Context Object)**中。"
了解了大概的原因,我们就可以修改一下这个测试例:
...
function createArray() {
var arr = new Array(1024 * 1024).fill(1);
return function () {
arr;
console.log('create done');
}
}
...
只需要在返回的function中引用到需要使用的作用域中的变量,这个闭包就可以在实际环境中生效了
可以看到,闭包造成的内存泄漏还是很严重的
但我又发现了一个问题,这个销毁数组的方法array = []
并不能断开闭包函数引用,GC并没有触发清理内存操作
这时我意识到闭包是存于数组内部的,而改变array的指向只是改变对数组的引用指向,而闭包的函数环境还是被先前的数组引用着,且这个引用是一个强引用(Strong Reference)。
强引用是指在代码中明确地持有对对象的引用,使对象不能被垃圾回收。
只要存在强引用指向一个对象,该对象就会一直存在于内存中,垃圾回收器不会将其回收。JavaScript 的垃圾回收机制(GC)会检测是否有任何活动的引用指向内存。如果闭包内仍然引用了外部变量,即使你更改
array
的指向,这些变量也无法被回收。
于是我写了一个循环,依次将数组中的值全部置为null
array.forEach((_,index)=>array[index]=null)
array = []
这样就能真正清空数组。
但此时我又发现一个更简单的方法:设置array的length值为0,这种方法也可以清空数组中的全部内容
将
length
设置为 0,不仅仅是改变数组的长度,而是直接移除数组中所有的元素。
array.length = 0