到底什么是闭包?
闭包(closure)在 JavaScript 中可以说是无处不在,但由于我们无法直接观测到它,所以就导致我们经常忽视了它的存在,以致于难以掌握。
事实上,每当我们创建一个函数闭包也会随之产生,它是基于词法作用域书写代码时自然产生的结果。
const a = 1
function foo() {
console.log(a)
}
上面这段代码有闭包吗?如果有,你能说说它具体是什么吗?
接下来,让我们一起来探究一下到底什么是闭包。
基本概念
我们先来看看 MDN 文档是如何描述闭包这个概念的:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
MDN 把闭包的基本概念描述得已经非常清楚了,但是还缺少一条关键信息,那就是在什么情况下闭包会产生作用。
在《你不知道的 JavaScript(上卷)》一书中是这么定义闭包的:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
这句话的后半段非常重要,一来加强说明了闭包一定会在函数创建时产生,再者是指出了闭包在什么情况下发挥作用。
这句话前半段中的词法作用域可以理解为函数所在位置的作用域,但实际上还包含了在该函数中能够通过作用域访问到的所有变量(引用)。
综上所述,我们可以将闭包的基本概念总成为两点:
-
闭包会在函数被创建时,自动根据所在的词法作用域产生。
-
闭包主要是在函数不在所在词法作用域中执行的情况下起作用。
若隐若现的闭包
让我们先来看这段代码:
function foo() {
const a = 1
function bar() {
console.log(a)
}
bar() // 1
}
foo()
调用 foo()
函数时,bar()
函数也被执行,基于词法作用域的查找规则,bar()
函数可以访问外部作用域(foo()
函数的作用域)中的变量 a
。
这段代码中有闭包吗,从上文中闭包的基本概念来看,肯定产生了闭包,只是我们很难找到它们的踪迹。
第一个闭包是 foo()
函数在创建时产生的,它引用了外部全局作用域,第二个闭包是 bar()
函数在创建时产生的,它引用了 foo()
函数的作用域以及外部全局作用域。
再来看这段代码:
function foo() {
const a = 1
function bar() {
console.log(a)
}
return bar
}
const baz = foo()
baz() // 1
与上一段代码不同,在调用 foo()
函数时,我们将内部的 bar()
函数引用作为返回值赋值给了变量 baz
,当我们调用 baz()
函数时,实际上调用的就是 bar()
函数本身。
一般来说,当 foo()
函数执行完时,内部作用域会被销毁,垃圾回收机制会被触发,变量 a
占用的内存会被释放。但是当我们在全局作用域中调用 bar()
函数后,还是打印出了变量 a
的值,这就说明 foo()
函数的内部作用域没有被销毁,变量 a
还在内存中。
这就是闭包的强大之处,bar()
函数产生的闭包阻止了 foo()
函数的内部作用域被销毁。这也佐证了闭包基本概念中的第二点,闭包主要是在函数不在所在词法作用域中执行的情况下起作用。
我们再来看看其他情况:
function sleep(fn, delay, name) {
setTimeout(function bar() {
fn(name)
}, delay)
}
sleep((who) => {
console.log( `${who} wake up`)
}, 1000, 'jack')
// 约一秒后
// jack wake up
这段代码中,当 sleep()
函数执行完后,它的作用域也不会立即消失,这是因为 bar()
函数依然保持着有 sleep()
函数作用域的闭包。
1000 毫秒后,bar()
函数顺利地从 sleep()
函数作用域中找到了变量 fn
和变量 data
。
像类似 setTimeout()
这样的异步任务中,我们很容易发现闭包的影子,因为它们都使用了回调函数,该函数中又引用了外层作用域的变量。
const foo = (function (param) {
return function bar() {
console.log(param)
}
})('A')
foo() // A
上面这段代码使用了 IIFE 立即执行函数,当该函数执行完时,它创建的作用域会一直保留,因此 bar()
函数能够访问到变量 param
,当然这也是闭包的作用。
原来如此,IIFE 也是容易发现闭包的地方,闭包真是无处不在。
闭包的作用
上文中我们理清了闭包的概念及特点,现在我们来总结一下闭包的作用。
在上文中也提及到,闭包可以阻止外部函数执行完后,其作用域不被销毁,继而保证内部函数依然可以访问外部函数作用域中的变量。
我们来看看这个例子:
for (var i = 0; i <= 10; i++) {
(function (i) {
setTimeout(() => console.log(i), i * 1000)
})(i)
}
每次循环 IIFE 都会创建一个独立的作用域,每个作用域下的变量 i
都不一样。由于传给 setTimeout()
函数的箭头函数会产生一个闭包,引用了 IIFE 创建的作用域,该作用域的生命周期得到了延长,所以上面这段代码会依次打印 0 到 10,大约一秒钟打印一次。
总而言之,闭包最大作用就是可以延长作用域的生命周期。
例如我们常用的防抖(debounce)函数:
function debounce(fn, delay, ...curries) {
let timer = null
const callback = function (params) {
fn.apply(this, [...curries, ...params])
}
return function () {
if (timer !== null) {
clearTimeout(timer)
}
timer = setTimeout(callback.bind(this, arguments), delay)
}
}
const handleClick = debounce((...data) => console.log(data), 500, 'A')
这个简单实现的防抖函数例子中,debounce()
函数返回的匿名函数产生了一个引用 debounce()
函数作用域的闭包,因此 debounce()
函数作用域的生命周期被延长,里面的变量 timer
和变量 callback
都能在匿名函数中访问到。
闭包的副作用?
长久以来,闭包一直被一部分人认为是最容易造成内存泄露的罪魁祸首。
const foo = (function () {
const a = 1
return function () {
console.log(a)
}
})()
这段代码中,只要变量 foo
指向的匿名函数存在,分配给变量 a
的内存就永远不会得到释放。
的确,滥用闭包会有内存泄露的危险,但是更加值得我们关注的是它带来的好处。
总之,我们应该去认识和拥抱它,并利用好这一特性带给我们的魔力,用过的人都说好!
小结
本文主要探究了 JavaScript 中闭包这一晦涩难懂的特性,总结了它的基本概念、特点以及作用。
以上就是本文的全部内容,希望对你有所帮助。
本人水平有限,如有错误或有争议的地方,请及时指出。
最后,感谢阅读,欢迎交流分享!