JavaScript 闭包:函数为什么能记住外部变量

2023/05/20

刚接触闭包时,我一直觉得“函数记住了外部变量”这句话有点抽象。真正理解它,关键不是背定义,而是看清函数的作用域在什么时候确定,以及外层函数执行结束后,为什么某些变量还没有消失。

一、什么是闭包

闭包可以理解为:函数和它定义时所在词法作用域的组合。当一个函数引用了外层作用域中的变量,即使它离开原来的作用域,在别处执行时,仍然可以访问这些变量。

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

需要注意,闭包记住的是变量绑定,不是简单复制一份当时的值。只要仍在操作同一个闭包环境,变量就可以持续变化。

二、闭包为什么有用

闭包最常见的价值,是保存状态,同时避免把变量直接放到全局作用域。

  1. 封装私有状态:外部只能通过返回的函数操作 count,不能直接修改它。
  2. 复用参数:先保存一部分配置,再生成具有特定行为的新函数。
  3. 延续异步上下文:事件回调和定时器可以继续读取创建它们时的变量。

例如,一个简单的乘法函数可以先用闭包保存倍率:

function multiplyBy(multiplier) {
  return function (value) {
    return multiplier * value;
  };
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

double(5); // 10
triple(5); // 15

doubletriple 来自两次独立调用,因此各自拥有一套词法环境,分别保存 23

三、项目里的常见场景

防抖就是一个很典型的闭包应用。外层函数创建 timer,返回的函数在多次调用之间共享它,因此可以取消上一次尚未执行的任务。

function debounce(fn, delay = 300) {
  let timer = null;

  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

这里如果没有闭包,timer 就很难在多次调用之间持续存在。类似的场景还有节流、函数柯里化、结果缓存、事件处理器以及模块内部状态管理。

四、闭包和内存占用

闭包本身不是内存泄漏。只要闭包仍然可访问,它引用的数据继续保留就是正常行为;真正的问题是闭包已经不再需要,却仍被定时器、事件监听器或其他长生命周期对象引用,导致相关数据无法被垃圾回收。

实际开发中,我会重点处理以下几类资源:

  1. 组件或页面销毁时清除 setIntervalsetTimeout
  2. 不再需要时移除事件监听器。
  3. 避免让长生命周期闭包无意中引用大型对象或已经移除的 DOM。
  4. 必要时解除外部引用,让闭包及其词法环境可以被回收。

所以重点不是“少用闭包”,而是让闭包的生命周期和业务生命周期保持一致。

五、React 中的过期闭包

React 函数组件每次渲染都会重新执行,并创建新的局部变量和函数。异步回调会捕获创建它那次渲染中的状态,这就可能出现过期闭包。

useEffect(() => {
  const timer = setInterval(() => {
    console.log(count); // 可能一直是首次渲染时的值
  }, 1000);

  return () => clearInterval(timer);
}, []);

空依赖数组意味着这个 Effect 只使用首次渲染创建的回调,因此回调中的 count 不会自动变成最新值。根据具体需求,可以把 count 加入依赖数组、使用函数式状态更新,或者通过 useRef 保存最新值。

六、理解闭包的判断方式

看到闭包相关代码时,我通常按三个问题判断:

  1. 内部函数引用了哪些外层变量?
  2. 这个函数会不会在原作用域之外继续执行?
  3. 是谁一直持有这个函数,它会在什么时候被释放?

闭包不是一种需要额外开启的语法,而是 JavaScript 词法作用域自然产生的结果。理解变量从哪里来、被谁引用、能存活多久,比单独记住一句定义更有用。