1. 什么是闭包?
闭包是一种现象。闭包是指在 JavaScript 中,内部函数可以访问外部函数作用域中的变量,并且在外部函数执行结束后仍然保持对这些变量的引用。换句话说,只要函数中使用了外部的数据,就创建了闭包。而作用域链是实现闭包的手段。
要不要放入闭包取决于该变量有没有被引用。
自动形成的闭包,是会被销毁掉的。
因为没有引用,所以闭包也会被销毁。
function eat() {
let food = '米饭'
console.log(food)
}
eat()
console.log(food)
方法执行结束之后,上下文被销毁,food 变量也会跟着消失。
然后我们修改一下:
function eat() {
let food = '米饭'
return function () {
console.log(food)
}
}
let fd = eat()
fd() // 米饭
此时因为 food 一直被引用,没有被垃圾回收机制回收掉。所以仍然可以打印 food。
所以闭包的特点就是:
- 可以让外部环境访问到函数内部的局部变量
- 可以让局部变量持续保存下来,不随着它的上下文环境一起销毁
从而根据此特征,可以解决全局变量污染的问题。
var name = 'GlobalName'
var init = (function () {
var name = 'initName'
function callName() {
console.log(name)
}
return function (){
callName()
}
})()
init() // initName
总结一下:
- 闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在JavaScript中是通过作用域链来实现的闭包。
- 只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
- 我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文一起销毁。
但是,在谈闭包之前需要先知道作用域和垃圾回收。
作用域
- 作用域是一个独立的地盘,不会让变量暴露出去,不同作用域下的同名变量不会有冲突。
- 作用域在定义时就确定,并且不会改变。
- 如果当前作用域中没有查到值,就向上级作用于中去查,直到找到全局作用域,这个查找过程形成的链就是作用域链。
垃圾回收
- Javascript 执行环境会负责管理代码执行过程中使用的内存,其中就涉及到一个垃圾回收机制。
- 垃圾收集器会定期(周期性)找出那些不再继续使用的变量,只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收。
2. 闭包的经典问题
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
此时会打印三个4 。原因就是因为闭包。setTimeout 访问了外部变量 i,形成闭包。i 变量只有一个,循环到 4 时循环结束,而又因为 setTimeout 等待 1s 执行, i 变成 4 的时候才会打印输出。
为了解决这个问题,我们有两种方法。
方法一:
for (var i = 1; i <= 3; i++) {
(function (index) {
setTimeout(function () {
console.log(index)
}, 1000)
})(i)
}
让 setTimeout 中的匿名函数不再访问外部变量,而是访问自己的变量,相当于有了自己的变量 index,而这个 index 在每次传入的时候都是不一样的,从而不再创建闭包,使用自己作用域中的 变量,打印 1,2,3。
方法二:
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
使用 let。
总结一下:
- 闭包可以解决一个全局变量污染的问题。
- 如果是自动产生的闭包,我们无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为null,即手动清除变量,这样下次JavaScript垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的量给回收了。