什么是闭包

90 阅读4分钟

什么是闭包?

闭包是 JavaScript 中一个非常重要的概念,但对于初学者来说可能有些难以理解。简单来说,闭包是指函数与其周围状态(词法环境)的捆绑。换句话说,闭包允许函数访问并操作函数外部的变量,即使在其外部函数执行完毕后仍然可以访问。换句话说,闭包是一个函数与其外部环境(词法作用域)之间的连接。

更正式的定义是:闭包是一个函数以及其周围状态(词法环境)的捆绑。这个环境包含了闭包创建时作用域内的任何局部变量。

理解闭包的关键点:

  • 函数嵌套函数: 闭包通常涉及在一个函数内部定义另一个函数(内部函数)。
  • 内部函数访问外部函数的变量: 内部函数可以访问其外部函数的作用域中的变量,包括外部函数的参数和局部变量。
  • 外部函数执行完毕后,内部函数仍然可以访问外部函数的变量: 这是闭包最重要的特性。即使外部函数已经执行完毕并从调用栈中移除,内部函数仍然保持对外部函数作用域的引用。

闭包的构成

闭包由两个关键要素组成:

  • 函数:闭包中的核心部分。
  • 词法作用域(Lexical Scope):函数可以访问其定义时作用域中的变量。

闭包的形成通常发生在一个函数返回另一个函数,或者在某个函数内部定义了函数并返回这个函数。

闭包的形成过程:

当一个函数被创建时,它会创建一个闭包。这个闭包由函数本身和其周围的词法环境组成。词法环境包含了函数创建时所在的作用域中的所有变量。

举例说明:

function outerFunction(x) {
  let outerVar = '外部变量';

  function innerFunction(y) {
    console.log(outerVar); // 内部函数访问外部函数的变量
    console.log(x + y); // 内部函数访问外部函数的参数
  }

  return innerFunction; // 返回内部函数
}

let myClosure = outerFunction(10); // 调用外部函数,并将返回的内部函数赋值给 myClosure
myClosure(5); // 调用内部函数,输出:'外部变量' 和 15

在这个例子中:

  1. outerFunction 是外部函数,innerFunction 是内部函数。

  2. innerFunction 可以访问 outerFunction 的变量 outerVar 和参数 x

  3. outerFunction(10) 被调用时,它返回 innerFunction,并将 innerFunction 的引用赋值给 myClosure。此时,闭包就形成了。

  4. 即使 outerFunction 已经执行完毕,myClosure(即 innerFunction)仍然可以访问 outerFunction 的作用域,包括 outerVarx 的值。

闭包的用途:

  • 私有变量和数据封装: 闭包可以用来创建私有变量,防止外部直接访问和修改。这在模块化编程中非常有用。

    function createCounter() {
      let count = 0; // 私有变量
    
      return {
        increment: function() {
          count++;
        },
        decrement: function() {
          count--;
        },
        getValue: function() {
          return count;
        }
      };
    }
    
    let counter = createCounter();
    counter.increment();
    console.log(counter.getValue()); // 输出 1
    

    createCounter() 返回一个包含多个函数的对象,每个函数都可以访问 count 变量。

    外部代码无法直接访问 count,只能通过 increment()decrement()getCount() 来操作它。

    • 回调函数与异步编程

    闭包常常用于回调函数中,尤其是在异步编程中,闭包可以使函数在异步操作结束后仍然访问原始的上下文数据。

    function fetchData(url, callback) {
        setTimeout(() => {
            // 模拟网络请求延迟
            const data = { message: "Data fetched from " + url };
            callback(data);  // 回调函数
        }, 1000);
    }
    
    function handleData(data) {
        console.log(data.message);
    }
    
    fetchData('https://api.example.com', handleData);
    
    

    fetchData 函数模拟一个异步操作,内部的回调 handleData 函数是一个闭包。

    回调函数在异步操作完成时能够访问并操作其外部环境的数据。

    • 函数柯里化

    函数柯里化(Currying)是将一个多参数的函数转换为一系列单参数函数的过程。闭包在柯里化函数的实现中发挥了重要作用。

function multiply(a) {
    return function(b) {
        return a * b;
    }
}

const multiplyBy2 = multiply(2);
console.log(multiplyBy2(5));  // 10
console.log(multiplyBy2(10)); // 20

multiply 函数返回一个新的函数,该函数可以访问其外部作用域中的变量 a。 我们可以通过调用 multiply(2) 创建一个新的函数 multiplyBy2,该函数通过闭包记住了 a 的值。

闭包的优势

私有变量和数据封装:闭包使得函数内部的数据能够得到保护,避免外部随意访问和修改。 灵活性与高阶函数:闭包使得 JavaScript 可以实现更灵活的编程模式,如函数柯里化、回调等。 状态持久性:闭包能够持久化状态(例如,记住函数执行时的变量值),这在实现计数器、缓存等功能时非常有用。

闭包的缺点:

内存泄漏: 如果闭包引用了大量的外部变量,并且这些闭包没有被正确地释放,可能会导致内存泄漏。因此,在使用闭包时需要注意及时释放不再使用的闭包。