闭包是一个会“记忆”的函数
在 JavaScript 中,每次定义函数时,它都会悄悄保留一个指向周围变量的链接。之后这个函数运行时,哪怕是在完全不同的地方,它依然能看到那些变量。这就是闭包。
最简短的演示:
makeGreeter 运行,返回一个内部函数,然后退出。你可能以为它的局部变量 name 会随之消失,毕竟函数已经执行完了。但内部函数仍在使用 name,所以 JavaScript 会让它继续存活。greetAda 记住了 "Ada",greetBoris 记住了 "Boris"。两个闭包,记住了两个各自独立的值。
闭包源于词法作用域
闭包背后的规则叫作词法作用域:函数能看到的是它被写下的位置周围的变量,而不是它被调用的位置周围的变量。“词法”的意思就是“取决于它在源代码中的位置”。
show 输出的是 "I'm outside",而不是 "I'm inside caller"。它是写在顶层 outer 旁边的,所以它看到的就是那个变量。从某个恰好也有自己的 outer 的地方调用它,并不会改变结果。
闭包不过是比外层函数活得更久的词法作用域。只要还有人引用某个变量,它就不会消失。
每次调用都会产生新的闭包
每次调用外层函数都会创建一组新的变量,从这次调用返回的内部函数记住的就是这一组变量。这就是上面的 greetAda 和 greetBoris 互不冲突的原因。
经典的例子是计数器:
a 和 b 各自持有自己的 count。返回的函数之外,任何代码都碰不到这些变量,count 是完全私有的。这并不是我们打开了什么语言特性,而是闭包的工作方式自然带来的结果。
不用类也能实现私有变量
由于被包住的变量只能通过返回的函数访问,你可以用闭包构建拥有真正私有状态的小对象:
balance 并不是返回对象上的属性,它存在于闭包中。读取或修改它的唯一途径,就是通过你暴露出来的方法。带 #private 字段的类也能做到这一点,但闭包写法比它们早了几十年,至今在整个生态中随处可见。
循环中的经典陷阱
闭包最常让人栽跟头的地方是循环。看看使用 var 时会发生什么:
你以为会得到 0、1、2,实际得到的却是 3、3、3。原因是:var 是函数作用域的,整个循环只有一个 i。三个闭包捕获的是同一个变量,等它们运行时,循环早已结束,i 的值是 3。
换成 let:
现在输出的是 0、1、2。let 是块级作用域的:循环的每次迭代都会为 i 创建一个新的绑定,所以每个闭包捕获的都是自己的值。这是优先使用 let 而不是 var 的最大理由。
闭包捕获的是变量,而不是值
有一点很微妙但很重要:闭包抓住的是变量本身,而不是函数定义时那个值的快照。
printMessage 在运行时才读取 message,而不是在创建时。如果你想要快照,就先把值复制到一个局部变量里,这实际上正是 let 在 for 循环中做的事情。
一个常见的实战模式:once
下面这个小工具用闭包确保某个函数只会运行一次:
called 和 result 是私有状态,返回的函数存活多久,它们就存活多久。不需要全局标志,也不需要额外的对象。这种模式(小巧的辅助函数、私有状态、闭包)是 JavaScript 最有用的能力之一。
关于内存
只要还有东西引用闭包,闭包就会让它捕获的变量保持存活。这通常正是你想要的,但如果你把闭包挂到某个长期存在的东西上(比如 DOM 事件监听器或全局缓存),而它又捕获了很大的数据,那么在闭包消失之前,这些数据都无法被垃圾回收。
function attach() {
const hugeData = new Array(1_000_000).fill("...");
document.addEventListener("click", () => {
console.log(hugeData.length);
});
}
只要监听器还挂着,hugeData 就会一直留在内存中。移除监听器(或者避免捕获用不到的东西),引用就会被清除。你不需要事事都精打细算,只要知道闭包和内存是相互关联的就够了。
本篇要点
- 闭包就是一个函数,加上它被定义时看到的那些变量。
- 每次调用外层函数,都会为其内部闭包创建一组新的变量。
- 闭包让你不用类也能拥有私有状态。
- 在循环中使用
let,让每次迭代都有自己的绑定。 - 闭包捕获的是变量本身,而不是创建时的值。
下一步:this 关键字
闭包处理的是函数周围的变量。下一个要点是函数在什么对象上被调用,在 JavaScript 中这由 this 控制,而它的行为与我们刚讲的被捕获变量大不相同。
常见问题
JavaScript 闭包是什么?
闭包是一个函数,它能记住定义它时所在作用域里的变量,即使外层作用域已经执行完毕也不例外。严格来说,JavaScript 中的每个函数都是闭包。这个词通常出现在函数被返回或传到别处、却仍在使用原作用域变量的时候。
闭包有什么用?
闭包让函数可以随身携带私有状态。你可以得到与某个函数绑定的数据,而不需要类或全局变量。常见用途有计数器、只执行一次的回调、带缓存的辅助函数,以及把实现细节隐藏在一个小巧的 API 后面。
为什么在使用 var 的循环里,闭包的行为很奇怪?
var 是函数作用域的,所以每次迭代共享同一个变量。循环中创建的闭包都引用这一个变量,等它们运行时,变量已经变成了最终值。改用 let 就行:它是块级作用域的,每次迭代都会得到自己的绑定。