讲解
闭包是「函数 + 它记住的外层变量」的组合。JavaScript 的函数在定义时就绑定了当时的作用域链,之后无论这个函数被传到哪里、什么时候执行,它都能读写定义处外层作用域的变量——哪怕外层函数早已执行完毕。简单说:内层函数把外层函数的变量「包」起来带走了,这就是闭包。
闭包最经典的应用是制造私有状态:外层函数里声明的变量外界访问不到,只有返回的内层函数能碰它,于是计数器、缓存、配置都能封装起来,不被外部随意篡改。工厂函数(multiplier(3) 返回一个「乘 3 的函数」)、防抖节流、记忆化缓存、模块化方案,底层都是闭包。循环中创建回调时,let 每轮生成新绑定正是靠闭包让每个回调记住自己那轮的 i。
闭包的代价是内存:被闭包引用的外层变量不会被垃圾回收,活得和闭包一样久。绝大多数场景这无关紧要,但在长生命周期对象(全局缓存、常驻监听器)持有闭包、闭包又引用大对象时,就是内存泄漏的温床。排查泄漏时,「谁还引用着这块内存」的答案往往是一个被遗忘了的闭包。
示例
// 私有计数器:count 外界访问不到
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
get value() {
return count;
},
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log('计数:', counter.value);
// 工厂函数:每个返回的函数记住自己的 factor
function multiplier(factor) {
return (n) => n * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5), triple(5));
// 记忆化:闭包保存缓存
function memoize(fn) {
const cache = new Map();
return (x) => {
if (cache.has(x)) return cache.get(x) + '(来自缓存)';
const result = fn(x);
cache.set(x, result);
return result;
};
}
const square = memoize((n) => n * n);
console.log(square(4), square(4));
// 循环闭包:let 让每个回调记住自己的 i
const handlers = [];
for (let i = 0; i < 3; i++) {
handlers.push(() => '第 ' + i + ' 个');
}
console.log(handlers.map((h) => h()));
常见坑
- 循环 var 闭包陷阱:for (var i...) 中所有回调共享同一个 i,循环结束后读到的都是终值。用 let 或把 i 作为参数传进立即执行函数。
- 在循环闭包里修改共享变量:多个闭包引用同一个外层变量时,一个改、全部看到,若不是有意为之就是 bug。
- 无意识地持有大对象:事件回调、定时器闭包引用了 DOM 节点或大数组,元素移除后闭包还在,内存无法回收。及时 removeEventListener、clearInterval。
- 把闭包当万能私有:闭包变量外部读不到但也无法被测试和调试工具直接观测,需要可观测性时考虑 class 的 # 私有字段。
小结
闭包让函数持久记住定义处的变量,支撑私有状态、工厂函数、记忆化等模式;代价是引用不释放。下一节讲另一个函数核心概念:this。