闭包
function f1() {
var a = 1
function f2() {
return a
}
return f2 // return为了让f2函数能够被使用
}
var b = f1()
b() // 1 b作为全局变量获取到了局部变量a的值
在上方代码中,函数 f2 被包括在函数 f1 内部,这时 f1 内部的所有局部变量,对 f2 都是可见的。
既然 f2 可以读取 f1 中的局部变量,那么只要把 f2 作为返回值,就可以在 f1 外部读取它的内部变量了。
上面代码中的 f2 函数,就是闭包。(通俗理解:闭包就是能够读取其他函数内部变量的函数。)
闭包可以简单理解成 “定义在一个函数内部的函数“ 。所以,在本质上,闭包是连接函数内外部的桥梁。
经典例1
function f() {
var a = 1 // 没有被闭包所引用,函数执行完毕被回收
console.log(++a)
}
f() // 2
f() // 2
// 变量a会在每次被调用时新创建
function f() {
var a = 1 // 被闭包所引用,不会被回收
return function () {
var b = 0
console.log(++a);
console.log(++b)
}
}
var f1 = f()
f1() // 2 1
f1() // 3 1
一般情况下,在函数 f 执行完后,就应该连同它里面的变量一同被销毁。
但是在这个例子中,匿名函数内部引用着 f 里的变量 a,所以变量 a 无法被销毁,而变量 b 是每次被调用时新创建的,所以每次 f1 执行完后它就把属于自己的变量(b)连同自己一起销毁,于是乎最后就剩下孤零零的 a,于是这里就产生了内存消耗的问题。
经典例2 定时器与闭包
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000)
} // 输出三次3
由于js是单线程的,所以在执行for循环的时候setTimeout被安排到任务队列中排队等待执行,等到setTimeout可以执行的时候,for循环已经结束,i的值也已经变为3,所以打印出来三个3。
解决办法:引入闭包来保存变量i,将setTimeout放入立即执行函数中,将for循环中的循环值i作为参数传递:
for (var i = 0; i < 3; i++) {
(function (i) {
setTimeout(() => {
console.log(i);
}, 1000)
}(i))
} // (1s later) 0 1 2
实现每隔1000毫秒分别依次输出数字:把时间1000改为1000*i
闭包的作用
- 可以在函数的外部访问到函数内部的变量,也就是「间接访问一个变量」。
- 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁。
- 防止命名冲突。
使用闭包可能会导致的问题「内存泄漏」:
内存泄露就是指用不到(访问不到)的变量,依然占据内存空间,不能被再次利用起来,增大了内存消耗,造成内存泄漏。
解决方法是可以在使用完变量后手动为它赋值为null。
有时会使用全局变量去存储临时信息,那么要记得使用完毕后手动赋值为 null,以回收内存。
常见的四种js内存泄漏:
1.全局变量
有时候我们无法避免使用全局变量,那么记得在使用完毕后手动释放它们,例如让变量指向null。
2 被遗忘的定时器或回调函数
不需要时记得清除定时器、移除监听事件
3 闭包 (解决方法:使用完变量后手动为它赋值为null
内存回收
在js中,垃圾回收器每隔一段时间就会找出那些不再使用的数据,并释放其所占用的内存空间。
以全局变量和局部变量来说,函数中的局部变量在函数执行结束后不再被需要,所以垃圾回收器会识别并释放它们(闭包可以让这些变量保存在内存中,不会随着函数的结束而销毁)。
而对于全局变量,垃圾回收器很难判断这些变量是否需要,所以尽量少使用全局变量。
垃圾回收模式:标记清除
从根部(在js中,我们一般认定全局对象window作为root)出发看是否能达到某个对象,如果能达到则认定这个对象还被需要,如果无法达到,则释放它。