什么是闭包(Closure)?

  1. 闭包让函数可以访问它外部的变量,即使外部函数已经执行完毕。
+ 闭包示例
function outer() {
  let count = 0;
  return function inner() {
    count++;
    console.log(count);
  };
}

const counter = outer();
counter(); // 输出 1
counter(); // 输出 2

在上面的例子中,inner 函数形成了一个闭包,它捕获了外部函数 outer 中的变量 count。即使 outer 函数执行完毕,count 依然保存在内存中,因为 inner 函数仍然引用着它。

闭包的工作机制

JavaScript 的作用域是基于词法作用域,也就是说函数的作用域在函数定义时就已经确定。闭包的形成依赖于这一机制:

  1. 函数定义时,记录下其所在的作用域环境。
  2. 当函数执行时,可以访问定义时作用域中的变量。
  3. 即使外部函数执行结束,作用域链依然被闭包中的函数保留。

浏览器或 JavaScript 引擎通过作用域链保证闭包变量的生存周期不会被垃圾回收。

闭包的应用场景

1. 数据封装与私有变量

JavaScript 没有原生的私有属性,闭包可以实现变量的私有化,防止外部直接访问和修改。

+ 点击展开
function createCounter() {
  let count = 0;
  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1

这里的 count 只能通过 incrementdecrement 方法访问,避免了外部直接篡改。

2. 函数工厂

闭包可以用来创建带有预设参数的函数,称为函数工厂。

function multiplyBy(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = multiplyBy(2); //multiplyBy(2)返回一个函数,这个函数会把输入的数字乘以2.
console.log(double(5)); // 10

const triple = multiplyBy(3);
console.log(triple(5)); // 15

multiplyBy 是一个函数工厂:它根据传入的 factor 生成一个新的函数。

闭包的作用:返回的函数记住了 factor,即使外层函数执行完毕,factor 仍然可用。

3. 事件处理和异步编程

闭包可以保存事件处理函数或异步回调中的变量状态。

for (var i = 1; i <= 3; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}
// 输出:1, 2, 3,分别在1秒、2秒、3秒后

使用立即执行函数表达式(IIFE)形成闭包,避免了 var 声明变量被循环覆盖的问题。

使用闭包时的注意点

1. 内存泄漏风险

闭包持有外部变量引用,可能导致一些对象无法被垃圾回收。如果闭包过度使用且持有大量数据,可能造成内存泄漏。应避免在闭包中引用不必要的变量。

2. JavaScript 的垃圾回收机制

JavaScript 使用标记清除(Mark-and-Sweep)垃圾回收算法:

  • 引擎从根对象(如全局对象、当前执行栈中的变量)开始标记所有可达的对象。
  • 未被标记的对象视为不可达对象,会被回收释放内存。

闭包中的变量只要被闭包函数引用,变量就被视为“可达”,不会被回收。


3. 闭包内存回收的时机

闭包的内存回收依赖于闭包本身的生命周期:

  • 闭包不再被任何变量引用时,闭包及其闭合的作用域变量才可被回收.
  • 只要闭包函数仍被引用,闭合的数据就不能被回收。

示例:

function makeCounter() {
  let count = 0;
  return function() {
    return ++count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1, // 此时 count  不会被回收
console.log(counter()); // 2

counter = null; // 解除引用
// 这时闭包函数和 count 变量都可以被回收

2. 性能开销

闭包会增加作用域链的查找成本。在性能敏感的代码中,合理使用闭包,避免过度嵌套。

3. 变量共享问题

在循环中使用闭包时,常见变量共享问题需要注意。ES6 的 let 关键字可以解决此类问题。

在 ES5 时代,如果你写:

for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), i * 1000);
}
结果会是
4
4
4

原因是:

  1. var 声明的变量没有块级作用域,整个循环里只有一个 i。
  2. 每次迭代的回调函数都共享同一个 i。
  3. 当定时器执行时,循环早已结束,i 已经变成 4,所以三个回调都打印 4。

✅ ES6 的解决方案:let

在 ES6 中,let 引入了 块级作用域。 在循环中使用 let i 时,每一次迭代都会创建一个新的 i 变量,作用域只属于这一轮循环。

所以这段代码:

for (let i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), i * 1000);
}
执行结果是
Code
1   // 1秒后
2   // 2秒后
3   // 3秒后

原因:

  1. 每次循环迭代,都会生成一个新的 i,并绑定到对应的闭包里。 2.定时器回调函数捕获的是当前迭代的 i,不会和其他迭代共享。
  2. 因此三个回调分别打印 1、2、3。

🎯 总结逻辑 问题:用 var 时,闭包共享同一个循环变量,导致结果相同。

解决:用 let,每次迭代有独立的作用域,闭包捕获的是不同的变量。


参考链接:

  1. MDN Web 文档 - Closures
  2. JavaScript 高级程序设计(第3版) - Nicholas C. Zakas