今天突然想到一个问题,let的块级作用域,以及闭包的变量引用功能很有意思(这脑洞咋联想到一起的,囧)。。闭包的使用会影响浏览器的GC过程。那么:
- JS 对象什么时候会被自动回收?
- 如何使用正确使用闭包,并避免内存泄漏?
先看一个经典例子,循环异步打印问题(没耐心的直接跳最后一个实例(^▽^))
// 想异步打印1到5
for(var i=1; i<=5;i++) {
setTimeout(function(){
console.log("print: " + i);
}, i*1000)
}
// 结果
print: 6
print: 6
print: 6
print: 6
print: 6
由于是异步调用打印函数,所以等调用这个函数时,循环已经结束,i变成了6,所以连着打印5个6。
第二种情况,如果用let 来声明i,let 和var 相比至少有如下特性:
- let声明的变量拥有块级作用域
- 形如for (let x...)的循环在每次迭代时都为x创建新的绑定(深度复制)
// 1到5
for(let i=1; i<=5;i++) {
setTimeout(function(){
console.log("print: " + i);
}, i*1000)
}
// 结果
print: 1
print: 2
print: 3
print: 4
print: 5
这种情况下直接通过let, 实际上给每一次回调函数的注册,创建了一个闭包,所以打印正常。
第三种情况,通过手动创建闭包也可以实现类似效果。每次循环内,返回一个函数引用当时的变量 i,这样实际上是重新分配了内存来存储i 的值,而不是单纯的引用内存地址。 尼玛内存蹭蹭往上涨,不过这么点数据完全不用担心
// 1到5
for(var i=1; i<=5;i++) {
setTimeout((function(){
var b = i; //install timer的时候引用 i 并且return 一个函数
return function(){
console.log("print: " + b);
}
})(), i*1000)
}
这个例子很好地说明了闭包对内部变量内存地址的保留作用(循环1w次就深度复制了1w份i )。但闭包和全局变量的不当使用可能会导致内存泄漏,内存居高不下甚至标签页直接挂掉。
JS 变量在浏览器内存中是否被GC 回收要看这个变量所在作用域的生命周期和变量是否被别人引用:
- 如果是函数内部声明的变量,并且没有任何外部变量引用,则函数执行完就销毁。如果有引用,则该内部变量会一直游离于内存中
JS 对象(引用类型)是存储在内存堆heap中,可以通过Chrome Debug Tool的 Profile 工具生成Heap SnapShot 来查看。
最后看一个活生生的实例,不出意外分分钟内存占用1G:
function Test()
{
this.obj= {};
this.index = 1;
this.timer = null;
var cache = []; // 内部变量,内存隐患...
this.timer = window.setInterval(() =>{
this.index += 1;
this.obj = {
val: '_timerxxxxxbbbbxx_' + this.index,
junk: [...cache]
};
cache.push(this.obj);
}, 1);
console.warn("create Test instance..");
}
test = new Test(); // JS对象开启定时器不断分配内存
啰嗦几句,这个例子的关键在于内部变量cache被外部的异步函数(定时器)引用。 如果不清除定时器,只是把Test类的实例手动设为null,也无济于事,cache还会继续占用内存。
Test.prototype.destroy = function(){
clearInterval(this.timer);
}
function d() {
// 取消定时器并销毁Test 实例
test.destroy();
test = null;
console.warn("destroyed test instance..");
}
清除内部变量cache的引用后,内存堆大小立刻下降了40MB.
总结:
- 函数内部不用的局部变量及时清理,在清理时要考虑ta的所有引用函数。
- 非得引用局部变量,请用非匿名函数,否则难以销毁引用。
个人见解,说得不对之处欢迎评论指正 : )
参考文章: