闭包的定义有许多的说法,下面来介绍下我理解的。
首先,在 js 中闭包是通过作用域链来实现的。
闭包可以看做是一个封闭的空间,用来存储当前作用域的变量,来在其他地方引用。
执行函数时,只要在函数中使用了外部数据,就会创建闭包。
验证一下:
function fun() {
const i = 1
console.log(i) // 断点
}
fun()
const i = 1
function fun() {
console.log(i)
}
fun()
而变量是否放入到闭包中,要看其他地方有没有对这个变量的引用。
举例:
const a = 1
function fun() {
const b = 2
const c = 3
function fun1() {
const d = 4
const e = 5
function fun2() {
console.log(a, b, c, d)
}
fun2()
}
fun1()
}
fun()
可以看到创建了3个闭包,分别存储对应作用域的变量。
fun
fun1
而变量 e
并没有被放到闭包中,因为没有被引用。
闭包的产生会占用空间。那如何销毁闭包,释放空间?
创建闭包分为2种情况,销毁也有所区别。
自动创建的闭包,在函数调用完会直接销毁掉。
在上面的例子中,fun()
执行完成后,变量 a
在全局环境中(没有在全局闭包中)正常输出,变量b
会报错。
fun()
console.log(a) // 1
console.log(b) // Error
可以看到已经没有任何闭包存在了,垃圾回收器会自动回收没有引用的变量 b,c,d,e
,不会有内存被占用的情况。
手动创建的闭包,可以设置在函数调用完依然保留。
先看一个例子:
function getUser() {
const name = '下雪天的夏风'
console.log(name);
}
getUser()
console.log(name); // Error
很明显,局部变量 name
会随着 getUser()
的执行上下文创建而创建,销毁而销毁。所以getUser()
执行完后,name
也就不存在了,打印报错。
修改代码如下:
function getUser() {
const name = '下雪天的夏风'
return function () {
console.log(name)
}
}
const user = getUser()
user() // 下雪天的夏风
调用 user()
为什么能访问到 name
,因为垃圾回收器只会回收没有被引用的变量。原本getUser()
调用完,name
就会被销毁掉,但此时向外部返回了一个匿名函数,该函数引用了 name
,所以name
不会被垃圾回收器回收。
因为这2个特点,也就产生了下面的使用场景:
在 js 还无法模块化的时期,多人协作有可能会导致定义的全局变量产生命名冲突。使用闭包可以将变量和对应的功能放到一个独立的空间中,来在一定程度上解决全局变量污染问题。
const name = '全局' // 全局变量
const init = (function () {
const name = '局部name1'
function callName() {
console.log(name)
}
return function () {
callName()
}
})()
init() // 局部 name1
const initSuper = (function () {
const name = '局部name2'
function callName() {
console.log(name)
}
return function () {
callName()
}
})()
initSuper() // 局部 name2
参考 这篇文章
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
上面的代码中,我们预期 1s 后输出 0,1,2,但实际会输出3个3。
这个问题就是因为闭包导致的。setTimeout
的回调函数访问了外部变量 i
,形成闭包。而变量 i
只有1个,循环结束后,访问的变量 i
也是同一个。
解决:
方式1:利用立即执行函数,实现 setTimeout
的回调函数不再访问外部变量。
for (var i = 0; i < 3; i++) {
;(function (index) {
setTimeout(function () {
console.log(index)
}, 1000)
})(i)
}
方式2:利用 setTimeout
的第3个参数,实现 setTimeout
的回调函数不再访问外部变量。
for (var i = 0; i < 3; i++) {
setTimeout(
function (index) {
console.log(index)
},
1000,
i
)
}
// 或
for (var i = 0; i < 3; i++) {
setTimeout(console.log, 1000, i)
}
方式3:利用 let
关键字产生的块级作用域,让每次循环都是新的变量i
。
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
注意是块级作用域的原因,此时并没有产生闭包。
以上。