闭包是函数与其定义时词法作用域绑定形成的运行时结构;识别关键:外层函数返回内层函数,且内层函数引用外层局部变量,如createCounter中count被持续访问。
JavaScript 闭包不是“某个特殊函数”,而是一个函数与其词法作用域绑定后形成的**运行时结构**——只要一个函数能访问并持有它定义时所在作用域里的变量,哪怕外层函数早已执行完毕,这个函数就是闭包。
闭包的最小可验证形态非常朴素:外层函数返回内层函数,且内层函数引用了外层的局部变量。
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter(); // ← 这里就产生了闭包
console.log(counter()); // 1
console.log(counter()); // 2
关键点:
count 是 createCounter 内部的局部变量,按理说函数执行完就该被回收return 出去的匿名函数仍能读写 count,说明 JS 引擎把它“锁”在了闭包中counter(),操作的都是同一份 count ——这就是状态保持这是新手高频翻车现场:用 for 循环给多个按钮绑点击事件,结果所有回调都输出最后一个索引值。
for (var i = 0; i < 3; i++) {
btns[i].addEventListener('click', function() {
console.log(i); // 全是 3!因为 var 是函数作用域,i 最终为 3
});
}
修复方式有三种,本质都是靠闭包“捕获当前轮次的值”:
let(推荐):let 是块级作用域,每轮循环生成独立绑定 → console.log(i) 输出 0、1、2(function(j) { ... })(i),把 i 当参数传进去形成闭包forEach 替代 for:arr.forEach((_, i) => { ... }),回调函数天然形成闭包它解决的是“如何安全地携带上下文”这个根本问题:
createSafe() 返回的 deposit 和 checkBalance 方法共享一个无法从外部篡改的 money 变量memoizeFunction(add) 返回的缓存版 add,内部闭包持有一个 cache 对象debounce(fn, delay) 返回的函数必须记住上次定时器 ID 和原始 fn,否则无法清除或延迟执行createApi(baseUrl) 返回一
堆 get/post 方法,它们都闭包着同一个 baseUrl 和默认 headers闭包本身没有性能问题,但滥用会导致内存泄漏:比如给大量 DOM 元素绑定闭包回调,又没手动解绑,那些被闭包引用的变量就一直活在内存里。真正要警惕的,从来不是“用了闭包”,而是“忘了清理闭包持有的大对象或 DOM 引用”。