彻底搞懂 JavaScript 闭包:从“前台保管柜”说起
闭包(Closure)是 JS 里最容易“会用但说不清”的概念之一。你可能写过它、踩过它的坑、甚至靠它做过封装——但如果要你解释“闭包到底是什么”,又容易卡壳。
这篇不背定义,咱们用一个生活类比把它讲透:闭包就像前台的保管柜。
1. 为什么需要闭包?
先看一个现实需求:你想做一个计数器:
- 外部不能随便改计数值(防止被乱改)
- 但你可以通过函数去“加一”“读取”
如果没有闭包,你可能只能把 count 暴露在全局,或者挂到对象上——可控性差、容易污染命名空间。
闭包出现,就是为了解决这类“需要私有状态,但又要持续使用”的需求。
2. 类比:前台保管柜 = 闭包
想象你去健身房:
- 你把手机、钥匙放进一个柜子(私有变量)
- 前台给你一张柜子钥匙(一个函数)
- 你离开前台很久了,但只要你还拿着钥匙,你就能随时打开柜子取东西
- 别人没有钥匙,就算知道柜子里有东西,也拿不到
对应到 JS:
- “柜子”是某个函数内部的变量(词法环境)
- “钥匙”是引用这些变量的内层函数
- “柜子不会消失”是因为:只要钥匙还在用,JS 就会保留那份环境
3. 闭包到底是什么?一句话说清
闭包 = 函数 + 它创建时所在的词法作用域的引用。
关键词是 “创建时”(不是调用时)。
也就是说:函数会记住它“出生”的地方,而不是它“被叫去干活”的地方。
4. 代码实战:最经典的闭包例子(计数器)
function createCounter() {
let count = 0 // 私有变量:外部拿不到
return function () {
count += 1
return count
}
}
const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
console.log(counter()) // 3
这里发生了什么?
createCounter()执行完,按理说内部变量该“下班”- 但它返回的函数一直在引用
count - 所以
count被“保管”起来,持续存在
这就是闭包的“保管柜效应”。
5. 你会预测输出吗?(闭包与“变量共享”)
很多闭包坑都来自“循环 + 异步”。
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
你以为输出:0 1 2
实际输出:3 3 3
原因(用保管柜解释):
var i只有一个柜子- 三个回调函数拿到的是同一把柜子钥匙
- 等定时器执行时,循环早结束了,
i已经是 3
修正方案 A:用 let(每轮一个新柜子)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
修正方案 B:用 IIFE 手动创建新柜子(老派但好懂)
for (var i = 0; i < 3; i++) {
;((j) => {
setTimeout(() => console.log(j), 0)
})(i)
}
6. 闭包的常见用途:它不是“技巧”,是“结构能力”
A. 做私有变量(封装)
function createUser(name) {
let score = 0
return {
getName: () => name,
addScore: (n) => (score += n),
getScore: () => score,
}
}
const u = createUser('Alice')
u.addScore(10)
console.log(u.getScore()) // 10
外部无法直接改 score,只能通过你提供的接口改。
B. 函数工厂(带配置的函数)
function createLogger(prefix) {
return function log(msg) {
console.log(`[${prefix}]`, msg)
}
}
const apiLog = createLogger('API')
apiLog('request start')
prefix 就是被闭包“记住”的配置。
C. 模块化(在没有打包工具的年代尤其常见)
闭包天然就能做模块隔离,避免全局污染。
7. 闭包的代价与坑:别把柜子当垃圾桶
闭包不是“免费午餐”,它会让某些变量活得更久。
A. 内存占用:引用不释放,环境就不释放
如果你在闭包里引用了很大的对象(DOM、数组、缓存),而闭包又长期存在,就容易造成内存压力。
实践建议:
- 用完就把引用置空(尤其在事件监听器、定时器里)
- 避免在闭包里无意捕获大对象(比如把整个
data都抓进来,只用其中一个字段)
B. 闭包 + 事件监听:最容易“不小心常驻”
function bind() {
const big = new Array(1e6).fill('x')
const handler = () => {
console.log(big[0])
}
window.addEventListener('click', handler)
// 如果你永远不 remove,这个 big 可能也会跟着长期存在
}
8. 总结:闭包是“记住过去”的能力
用“前台保管柜”记忆闭包:
- 函数是钥匙
- 作用域变量是柜子里的东西
- 只要钥匙还在,柜子就不会被清空
- 多个钥匙可能开同一个柜子(var 循环坑)
- 别把柜子塞太满(注意内存与释放)