Javascript 零基础教程

JavaScript 闭包进阶:如何保持函数状态 (Preserving State)

闭包是 JavaScript 中一项强大的功能,它允许函数即使在外部函数执行完毕后,也能“记住”其周围作用域中的变量。

这种能力对于保持状态 (Preserving State)、创建数据封装和实现设计模式特别有用。理解如何使用闭包来保持状态,对于编写高效且可维护的 JavaScript 代码至关重要。

本章将深入探讨实现这一点的机制,并展示在实际场景中利用这一概念的各种方法。

1. 理解闭包的状态保持

从本质上讲,利用闭包保持状态依赖于这样一个事实:闭包让函数能够访问其词法环境 (Lexical Environment)。这个环境包含了函数定义时作用域内的所有变量。

让我们通过一个例子来拆解它:

function outerFunction() {
  let count = 0;
  function innerFunction() {
    count++;
    console.log(count);
  }
  return innerFunction;
}

const myCounter = outerFunction();
myCounter(); // 输出: 1
myCounter(); // 输出: 2
myCounter(); // 输出: 3

在这个例子中:

  1. outerFunction 定义了一个变量 count 和一个内部函数 innerFunction。
  2. innerFunction 增加并打印 count 的值。
  3. 关键在于,outerFunction 返回 了 innerFunction。

当我们把 outerFunction() 的结果赋值给 myCounter 时,我们实际上是把 innerFunction引用赋值给了它。

innerFunction(现在由 myCounter 引用)被执行时,它仍然可以从 outerFunction 的作用域中访问 count 变量,即使 outerFunction 已经运行结束。这就是闭包的本质:innerFunction “闭合(closes over)”了 count 变量。

1.1 为什么这行得通:词法环境

为了完全理解这一点,我们需要回顾词法作用域(你在上一课中学到的)。

当一个函数被定义时,它会创建一个链接指向其周围的环境。即使外部函数已经完成执行,这个链接依然存在。因此,当通过 myCounter() 调用 innerFunction 时,它仍然可以访问和修改其词法环境中的 count 变量。

1.2 对比全局作用域

为了强调闭包的重要性,让我们看看如果使用全局变量会发生什么:

let globalCount = 0;

function incrementGlobalCount() {
  globalCount++;
  console.log(globalCount);
}

incrementGlobalCount(); // 输出: 1
incrementGlobalCount(); // 输出: 2

虽然这段代码也能实现计数,但 globalCount 变量可以在代码的任何地方被访问和修改。这使得它容易被意外修改,并且难以追踪逻辑。

相反,闭包提供了一种将 count 变量封装outerFunction 内部的方法,使其变为私有,只能通过 innerFunction 访问。这种封装是使用闭包保持状态的一个关键好处。

2. 使用闭包创建私有变量

闭包常被用来在 JavaScript 中模拟私有变量。不像某些其他语言(例如使用 private 关键字),JavaScript 没有内置的创建真正私有变量的机制。但是,闭包提供了一个变通方案。

function createCounter() {
  let count = 0; // 这个变量只能在 createCounter 内部访问
  return {
    increment: function() {
      count++;
    },
    decrement: function() {
      count--;
    },
    getValue: function() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 输出: 2

counter.count = 100; // 尝试直接修改 count
console.log(counter.getValue()); // 输出: 2 (count 保持不变)

在这个例子中,createCounter 返回一个包含三个方法的对象:increment(增加)、decrement(减少)和 getValue(获取值)。

  • 这些方法都可以访问定义在 createCounter 作用域内的 count 变量。
  • 然而,count 变量无法从 createCounter 函数外部直接访问。
  • 尝试修改 counter.countincrementdecrementgetValue 函数所“闭合”的那个真实 count 变量没有任何影响。

这实现了一种形式的数据隐藏,允许你控制状态如何被修改和访问。

2.1 私有变量的好处

  • 封装 (Encapsulation): 将内部数据和实现细节对外部代码隐藏。这降低了意外修改的风险,使代码更容易理解。
  • 模块化 (Modularity): 允许你创建具有定义良好的接口的独立组件,使代码更加模块化和可重用。
  • 抽象 (Abstraction): 隐藏底层实现的复杂性,允许用户在更高的抽象层面上与对象交互。

3. 实战示例与演示

让我们看一些更实际的例子,说明如何在不同场景中使用闭包来保持状态。

3.1 示例 1:创建一个带有私有状态的模块

const myModule = (function() {
  let privateVariable = "你好";
  
  function privateMethod() {
    console.log("在私有方法内部: " + privateVariable);
  }
  
  return {
    publicMethod: function() {
      privateMethod();
    }
  };
})();

myModule.publicMethod(); // 输出: 在私有方法内部: 你好
// myModule.privateMethod(); // 报错: myModule.privateMethod is not a function
console.log(myModule.privateVariable); // 输出: undefined

这个例子使用了立即调用函数表达式 (IIFE)(将在后续课程中介绍)来创建一个模块。privateVariableprivateMethod 只能在 IIFE 的作用域内访问。publicMethod 提供了一种与模块内部状态交互的方式,展示了封装性。

3.2 示例 2:事件处理程序与闭包

考虑这样一个场景:你想要给多个元素添加事件监听器,并且每个监听器都需要访问与该元素关联的特定数据。

function createButtonListeners() {
  for (let i = 0; i < 3; i++) {
    let button = document.createElement('button');
    button.innerText = '按钮 ' + (i + 1);
    
    button.addEventListener('click', function() {
      console.log('按钮 ' + (i + 1) + ' 被点击了!');
    });
    
    document.body.appendChild(button);
  }
}

createButtonListeners();

在这段代码中,闭包确保每个按钮的点击处理程序都能正确“记住”它关联的索引 i

如果没有 let 关键字(或者不使用 IIFE 创建闭包),所有按钮都会记录同一个值(循环结束后的 i 的最终值)。let 在循环的每次迭代中都创建了一个新的 i 绑定,这个绑定随后被事件监听器的闭包捕获。

3.3 示例 3:带有自定义增量的计数器

function createCounterWithIncrement(incrementBy) {
  let count = 0;
  return {
    increment: function() {
      count += incrementBy;
      return count;
    },
    getValue: function() {
      return count;
    }
  };
}

const counterByTwo = createCounterWithIncrement(2);
console.log(counterByTwo.increment()); // 输出: 2
console.log(counterByTwo.increment()); // 输出: 4

const counterByFive = createCounterWithIncrement(5);
console.log(counterByFive.increment()); // 输出: 5
console.log(counterByFive.increment()); // 输出: 10

这个例子展示了如何使用闭包创建多个计数器,每个计数器都有自己自定义的增量值。

createCounterWithIncrement 函数接收一个 incrementBy 参数。得益于闭包,每个返回的对象都有它自己的私有 count 变量和它自己的 incrementBy 值。