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在这个例子中:
- outerFunction 定义了一个变量 count 和一个内部函数 innerFunction。
- innerFunction 增加并打印 count 的值。
- 关键在于,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.count对increment、decrement和getValue函数所“闭合”的那个真实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)(将在后续课程中介绍)来创建一个模块。privateVariable 和 privateMethod 只能在 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 值。