一篇文章彻底搞懂 JS 闭包

247 阅读5分钟

一、什么是闭包?(通俗理解)

一个非常通俗的定义是:

闭包就是能够读取其他函数内部变量的函数。

但更准确、更专业的描述是:

闭包是一个函数与其被创建时所在的词法作用域(lexical scope)的组合。

换句话说,当一个内部函数引用了其外部函数的变量或参数时,即使外部函数已经执行完毕并返回了,这个内部函数仍然持有并可以访问那些外部变量。这个“内部函数 + 它引用的外部变量”所形成的组合体,就叫做闭包。

二、为什么会产生闭包?(核心机制)

要理解闭包,必须先理解 JavaScript 的作用域链(Scope Chain)词法作用域(Lexical Scoping)

  1. 词法作用域(静态作用域):函数的作用域在函数定义的时候就已经决定了,而不是在函数调用的时候。函数内部可以访问定义时所处的上下文(即外部作用域)的变量。
  2. 作用域链:当访问一个变量时,JavaScript 引擎会首先在当前函数的作用域内查找。如果没找到,就会沿着定义时的作用域链,一层一层地向上查找,直到全局作用域。

闭包产生的关键步骤:

  • 外部函数 outer 内部定义了一个内部函数 inner
  • inner 引用了 outer 中的变量 outerVar
  • 你将 inner 函数返回,或者以其他方式(如设置为全局变量、事件回调等)使其在 outer 函数之外可被调用。
  • outer 函数执行完毕,正常情况下,它的作用域应该被销毁(垃圾回收)。
  • 但是,因为 inner 函数还在引用着 outer 的作用域(因为要访问 outerVar),所以 JavaScript 引擎不会回收 outer 的作用域。
  • 这个被保留下来的作用域,就和 inner 函数捆绑在了一起,形成了闭包。

三、一个经典的例子

function outer() {
  let outerVar = '我在外部!';

  function inner() {
    console.log(outerVar); // 内部函数引用了外部变量
  }

  return inner; // 将内部函数返回
}

const myClosure = outer(); // 执行 outer,返回 inner 函数,赋值给 myClosure
// 此时,outer 函数已经执行完毕

myClosure(); // 输出: "我在外部!"

分析:

  1. 调用 outer(),它创建了一个局部变量 outerVar 和一个函数 inner
  2. outer() 返回 inner 函数,并赋值给全局变量 myClosure
  3. outer() 的执行上下文按理说应该结束了,但由于 inner 函数(现在通过 myClosure 可访问)引用了 outerVar,所以 outer 的作用域被保留了下来。
  4. 当我们调用 myClosure()(也就是 inner())时,它依然能够访问到 outerVar 这个变量。

四、闭包的常见用途

闭包非常有用,是许多编程模式的基础。

  1. 创建私有变量(数据封装) 这是闭包最著名的用途。JavaScript 没有原生支持私有变量,但闭包可以模拟它。

    function createCounter() {
      let count = 0; // 这是一个“私有”变量,外部无法直接访问
    
      return {
        increment: function() {
          count++;
          return count;
        },
        decrement: function() {
          count--;
          return count;
        },
        getValue: function() {
          return count;
        }
      };
    }
    
    const counter = createCounter();
    console.log(counter.getValue()); // 0
    console.log(counter.increment()); // 1
    console.log(counter.increment()); // 2
    console.log(counter.decrement()); // 1
    
    // 无法直接从外部修改 count
    // console.log(counter.count); // undefined
    

    这里的 count 变量被 increment, decrement, getValue 三个方法共享,但外部代码无法直接访问或修改它,实现了很好的封装性。

  2. 实现函数柯里化(Currying) 柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数的新函数的技术。

    function multiply(a) {
      return function(b) {
        return a * b;
      };
    }
    
    const multiplyByTwo = multiply(2);
    console.log(multiplyByTwo(5)); // 10
    console.log(multiplyByTwo(10)); // 20
    
    const multiplyByTen = multiply(10);
    console.log(multiplyByTen(5)); // 50
    
  3. 在异步编程和事件处理中保留状态 在循环中为多个元素绑定事件时,闭包非常有用。

    for (var i = 0; i < 5; i++) {
      // 使用立即执行函数(IIFE)创建一个闭包,捕获每次循环的 i 值
      (function(index) {
        setTimeout(function() {
          console.log(index); // 输出 0, 1, 2, 3, 4
        }, 1000);
      })(i);
    }
    

    (注意:这里用 var 和 IIFE 是经典解法。如果用 let 声明 i,由于其块级作用域的特性,问题会自动解决,但原理类似,都是为每次迭代创建一个新的作用域。)

五、闭包的注意事项

闭包虽然强大,但使用不当也会带来问题。

  1. 内存泄漏 因为闭包会阻止外部函数的作用域被回收,如果闭包本身会长期存在(比如被赋给了一个全局变量),那么它引用的所有变量都会一直占用内存,即使你可能不再需要它们了。

    解决方法:在不再需要闭包时,手动解除对它的引用(例如,将保存它的变量设置为 null)。

  2. 性能考量 由于需要维护额外的作用域,闭包对内存消耗和速度会有一点点负面影响。但在现代 JavaScript 引擎中,这个影响通常很小,不应成为你放弃使用闭包的理由,除非是在性能极其关键的场景。

总结

特性描述
本质函数 + 其创建时的词法作用域
核心机制作用域链和词法作用域
产生条件内部函数引用外部变量,且内部函数在外部被调用
主要优点数据封装、创建私有变量、实现高级函数模式(如柯里化)
主要缺点improper use can lead to memory leaks