刚接触闭包时,我一直觉得“函数记住了外部变量”这句话有点抽象。真正理解它,关键不是背定义,而是看清函数的作用域在什么时候确定,以及外层函数执行结束后,为什么某些变量还没有消失。
一、什么是闭包
闭包可以理解为:函数和它定义时所在词法作用域的组合。当一个函数引用了外层作用域中的变量,即使它离开原来的作用域,在别处执行时,仍然可以访问这些变量。
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
createCounter() 执行完成后返回了内部函数。按照直觉,外层函数已经结束,局部变量 count 应该被销毁;但返回的函数仍然引用着它,所以这块词法环境会继续保留。后续每次调用 counter(),访问的都是同一个 count。
需要注意,闭包记住的是变量绑定,不是简单复制一份当时的值。只要仍在操作同一个闭包环境,变量就可以持续变化。
二、闭包为什么有用
闭包最常见的价值,是保存状态,同时避免把变量直接放到全局作用域。
- 封装私有状态:外部只能通过返回的函数操作
count,不能直接修改它。 - 复用参数:先保存一部分配置,再生成具有特定行为的新函数。
- 延续异步上下文:事件回调和定时器可以继续读取创建它们时的变量。
例如,一个简单的乘法函数可以先用闭包保存倍率:
function multiplyBy(multiplier) {
return function (value) {
return multiplier * value;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
double(5); // 10
triple(5); // 15
double 和 triple 来自两次独立调用,因此各自拥有一套词法环境,分别保存 2 和 3。
三、项目里的常见场景
防抖就是一个很典型的闭包应用。外层函数创建 timer,返回的函数在多次调用之间共享它,因此可以取消上一次尚未执行的任务。
function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
这里如果没有闭包,timer 就很难在多次调用之间持续存在。类似的场景还有节流、函数柯里化、结果缓存、事件处理器以及模块内部状态管理。
四、闭包和内存占用
闭包本身不是内存泄漏。只要闭包仍然可访问,它引用的数据继续保留就是正常行为;真正的问题是闭包已经不再需要,却仍被定时器、事件监听器或其他长生命周期对象引用,导致相关数据无法被垃圾回收。
实际开发中,我会重点处理以下几类资源:
- 组件或页面销毁时清除
setInterval、setTimeout。 - 不再需要时移除事件监听器。
- 避免让长生命周期闭包无意中引用大型对象或已经移除的 DOM。
- 必要时解除外部引用,让闭包及其词法环境可以被回收。
所以重点不是“少用闭包”,而是让闭包的生命周期和业务生命周期保持一致。
五、React 中的过期闭包
React 函数组件每次渲染都会重新执行,并创建新的局部变量和函数。异步回调会捕获创建它那次渲染中的状态,这就可能出现过期闭包。
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 可能一直是首次渲染时的值
}, 1000);
return () => clearInterval(timer);
}, []);
空依赖数组意味着这个 Effect 只使用首次渲染创建的回调,因此回调中的 count 不会自动变成最新值。根据具体需求,可以把 count 加入依赖数组、使用函数式状态更新,或者通过 useRef 保存最新值。
六、理解闭包的判断方式
看到闭包相关代码时,我通常按三个问题判断:
- 内部函数引用了哪些外层变量?
- 这个函数会不会在原作用域之外继续执行?
- 是谁一直持有这个函数,它会在什么时候被释放?
闭包不是一种需要额外开启的语法,而是 JavaScript 词法作用域自然产生的结果。理解变量从哪里来、被谁引用、能存活多久,比单独记住一句定义更有用。