Javascript 零基础教程

JavaScript 闭包基础

闭包 (Closures) 让函数能够访问其周围作用域(surrounding scope)中的变量,即使在外部函数已经执行完毕之后。理解闭包可以解锁强大的编程模式,并帮助你避免常见的陷阱。

本章将探讨什么是闭包、它们是如何工作的,以及为什么它们如此有用。

1. 理解闭包:基础知识

从核心上讲,闭包是函数与该函数声明时所在的词法环境 (lexical environment) 的组合。

这个环境包含了在函数创建时作用域内的所有变量。函数“闭合(closes over)”了这些变量,这意味着即使在原始作用域之外执行该函数,它仍然保留对这些变量的访问权限。

为了说明这一点,让我们看一个简单的例子:

function outerFunction(outerVar) {
  function innerFunction(innerVar) {
    console.log(outerVar, innerVar);
  }
  return innerFunction;
}

const myInnerFunction = outerFunction("Hello");
myInnerFunction("World"); // 输出: Hello World

在这个例子中,innerFunction 就是一个闭包。即使在 outerFunction 返回之后,它仍然可以从 outerFunction 的作用域中访问 outerVar。当调用 myInnerFunction 时,它仍然可以访问并使用 outerVar 的值。

2. 词法环境 (Lexical Environment) 和作用域

理解词法作用域 (Lexical scope) 对于掌握闭包至关重要。

词法作用域意味着函数的作用域是由它在源代码中的位置决定的。在上面的例子中,innerFunction 在词法上嵌套在 outerFunction 内部,所以它可以访问 outerFunction 的变量。

这与动态作用域(JavaScript 不使用动态作用域)不同,动态作用域是由运行时的调用栈决定的。

3. 闭包如何保持状态 (State)

闭包允许函数“记住”其封闭作用域中的值。这种保持状态 (preserve state) 的能力是闭包最强大的特性之一。

让我们看另一个例子:

function createCounter() {
  let count = 0;
  function increment() {
    count++;
    console.log(count);
  }
  return increment;
}

const counter1 = createCounter();
counter1(); // 输出: 1
counter1(); // 输出: 2

const counter2 = createCounter();
counter2(); // 输出: 1
counter1(); // 输出: 3

在这种情况下,increment 是一个闭包,它可以访问 count 变量。

每次调用 createCounter 都会创建一个带有自己私有 count 变量的新闭包。这允许我们要创建多个独立的计数器。这个 count 变量只能通过 increment 函数访问,因此实际上它是私有的。

4. 多重嵌套闭包

闭包可以嵌套。函数可以从所有周围的作用域访问变量。看看这个:

function grandParentFunction(grandParentVar) {
  function parentFunction(parentVar) {
    function childFunction(childVar) {
      console.log(grandParentVar, parentVar, childVar);
    }
    return childFunction;
  }
  return parentFunction;
}

const parent = grandParentFunction("祖父 (GrandParent)");
const child = parent("父亲 (Parent)");
child("孩子 (Child)"); // 输出: 祖父 (GrandParent) 父亲 (Parent) 孩子 (Child)

在这里,childFunction 可以访问来自 grandParentFunctionparentFunction 的变量。

5. 闭包的示例

让我们探索更多例子来巩固对闭包的理解。

5.1 示例 1:封装 (Encapsulation)

function createBankAccount(initialBalance) {
  let balance = initialBalance;
  return {
    deposit: function(amount) {
      balance += amount;
      return balance;
    },
    withdraw: function(amount) {
      if (balance >= amount) {
        balance -= amount;
        return balance;
      } else {
        return "余额不足";
      }
    },
    getBalance: function() {
      return balance;
    }
  };
}

const myAccount = createBankAccount(100);
console.log(myAccount.deposit(50));   // 输出: 150
console.log(myAccount.withdraw(20));  // 输出: 130
console.log(myAccount.getBalance());  // 输出: 130

// console.log(myAccount.balance); // undefined,因为 balance 现在对闭包是私有的

在这个例子中,balance 变量对于由 createBankAccount 创建的闭包是私有的。外部代码不能直接访问或修改 balance;它只能通过 deposit(存款)、withdraw(取款)和 getBalance(查询余额)方法与之交互。这是一种封装形式,有助于保护对象的内部状态。

5.2 示例 2:创建模块 (Modules)

闭包可以用于在 JavaScript 中创建模块。模块是一个包含数据和行为的独立代码单元。

const myModule = (function() {
  let privateVariable = "秘密 (Secret)";
  function privateMethod() {
    console.log("在 privateMethod 内部:", privateVariable);
  }
  return {
    publicMethod: function() {
      console.log("在 publicMethod 内部");
      privateMethod();
    }
  };
})();

myModule.publicMethod(); // 输出: 在 publicMethod 内部, 在 privateMethod 内部: 秘密 (Secret)

// myModule.privateMethod(); // 报错 - 不是一个函数
// console.log(myModule.privateVariable); // undefined

在这个例子中,IIFE(立即调用函数表达式)创建了一个闭包,封装了 privateVariableprivateMethod。这些成员无法从模块外部访问。模块暴露了一个 publicMethod,它可以访问私有成员。我们将在下一课中详细探讨 IIFE。

5.3 示例 3:事件处理程序 (Event Handlers)

闭包通常用于事件处理程序,以访问在创建处理程序时可用的数据。

<!DOCTYPE html>
<html>
<head>
<title>闭包示例</title>
</head>
<body>
  <button id="myButton">点击我</button>
  <script>
    function setupButton(buttonId, message) {
      const button = document.getElementById(buttonId);
      button.addEventListener("click", function() {
        alert(message);
      });
    }
    setupButton("myButton", "按钮被点击了!");
  </script>
</body>
</html>

在这个例子中,传递给 addEventListener 的匿名函数是一个闭包。它可以访问来自 setupButton 函数的 message 变量。即使在 setupButton 执行完毕后,事件处理程序仍然可以访问并使用 message 的值。