Skip to content

函数闭包

闭包(closure)是 JavaScript 中一个强大的特性,指内部函数(inner function)可以访问其外部(包围)函数(outer (enclosing) function)的变量和参数,即使外部函数已经执行完毕。这个概念是许多高级 JavaScript 模式的基础。

理解作用域:全局作用域、函数作用域和块级作用域

Section titled “理解作用域:全局作用域、函数作用域和块级作用域”

在深入探讨闭包之前,让我们回顾一下作用域(scope):

  • 全局作用域(Global Scope): 在任何函数或块外部声明的变量具有全局作用域。它们可以从代码中的任何位置访问。在浏览器中,全局对象是 window。
  • 函数作用域(Function Scope,var 的局部作用域): 使用 var 在函数内部声明的变量是该函数局部的,不能从外部访问。
  • 块级作用域(Block Scope,let 和 const 的局部作用域): 使用 let 或 const 在块(例如,if 语句或 for 循环的 {...} 内,或只是一个独立的块)内部声明的变量是该块局部的。它们不能在该块外部访问。

作用域示例:

const globalVar = "I am global";
function outerFunction() {
const outerVar = "I am in outerFunction";
var functionScopedVar = "I am var in outerFunction"; // 函数作用域
console.log(globalVar); // 可访问
if (true) {
let blockScopedVar = "I am in a block"; // 块级作用域
console.log(blockScopedVar); // 在此处可访问
console.log(outerVar); // 在此处可访问
}
// console.log(blockScopedVar); // 错误:blockScopedVar 在此处未定义
function innerFunction() {
const innerVar = "I am in innerFunction";
console.log(globalVar); // 可访问(来自全局作用域)
console.log(outerVar); // 可访问(来自 outerFunction 的作用域 - 词法作用域!)
console.log(functionScopedVar); // 可访问(来自 outerFunction 的作用域)
// console.log(blockScopedVar); // 错误:blockScopedVar 在此处未定义(如果在单独的块中声明)
}
innerFunction();
}
outerFunction();
// console.log(outerVar); // 错误:outerVar 在此处未定义

词法作用域(Lexical Scoping,也称静态作用域 Static Scoping): JavaScript 使用词法作用域,这意味着变量的作用域由其在源代码中编写时的位置决定,而不是由函数调用的位置决定。内部函数可以“看到”其包含(外部)函数的变量。

当在外部函数(outer function)内部定义一个内部函数(inner function),并且该内部函数随后在外部函数外部可用时(例如,通过返回值或作为参数传递),就形成了闭包(closure)。内部函数“记住”了其被创建时的词法环境(lexical environment,即作用域链 scope chain)。这意味着即使外部函数已经执行完毕,内部函数仍然可以访问其外部函数的变量。

一个简单的闭包示例:

function createGreeter(greeting) {
// 'greeting' 是外部函数作用域的一部分
function greet(name) {
// 'greet' 是内部函数,形成了闭包
// 它可以通过其词法环境访问 'greeting'
console.log(greeting + ", " + name + "!");
}
return greet; // 返回内部函数
}
// 调用 createGreeter,它返回 'greet' 函数
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
// 即使 createGreeter 已经执行完毕,
// sayHello 和 sayHi 仍然可以访问各自的 'greeting' 变量。
sayHello("Alice"); // Output: Hello, Alice!
sayHi("Bob"); // Output: Hi, Bob!

在这个例子中,sayHello 和 sayHi 都是闭包(closures)。每个闭包都“捕获”(closes over)了各自调用 createGreeter 时对应的 greeting 变量。

一个常见的问题是创建一个计数器,其中计数变量(count variable)受到保护,无法从外部修改,但可以通过一个特定函数(specific function)进行递增(incremented)。

使用全局变量(global variable)存在风险:

let globalCounter = 0; // 暴露在外,可以被代码的任何部分修改
function incrementGlobal() {
globalCounter++;
console.log(globalCounter);
}
incrementGlobal(); // 1
globalCounter = 100; // 哎呀,计数器状态被破坏了
incrementGlobal(); // 101

使用闭包创建一个私有计数器:

function createCounter() {
let count = 0; // 'count' 对于该闭包是私有的
return function() {
count++;
console.log(count);
return count;
};
}
const counter1 = createCounter();
const counter2 = createCounter();
counter1(); // Output: 1
counter1(); // Output: 2
counter2(); // Output: 1 (counter2 有自己的 'count' 变量)
// console.log(count); // 错误:在此处无法访问 'count'

这种模式通常使用立即调用函数表达式(Immediately Invoked Function Expression,简称 IIFE)来实现,以立即创建私有作用域(private scope):

const uniqueIdGenerator = (function() {
let id = 0; // 私有的 id 变量
return function() {
id++;
return `id_${id}`;
};
})();
console.log(uniqueIdGenerator()); // Output: id_1
console.log(uniqueIdGenerator()); // Output: id_2

在 createCounter 中,count 变量是 createCounter 作用域的一部分。返回的匿名函数是一个闭包(closure)。每次调用 createCounter 时,都会创建一个新的作用域,从而初始化一个新的 count 变量。返回的函数维护着对其特定 count 的引用。

IIFE 版本(uniqueIdGenerator)只运行一次,设置私有的 id 变量,并返回用于递增的函数。id 受到保护,只能通过返回的函数进行修改。

  • 数据隐私 / 封装(Data Privacy / Encapsulation): 创建私有变量(private variables)和方法(methods),类似于面向对象编程(object-oriented programming)(如计数器示例所示)。
  • 函数工厂(Function Factories): 生成具有特定配置的其他函数(如 createGreeter 示例所示)。
  • 事件处理程序和回调函数(Event Handlers and Callbacks): 允许事件处理程序(event handlers)或回调函数(callback functions)访问它们定义所在作用域中的变量,即使是在稍后执行时也是如此。例如,在循环中创建事件监听器(event listeners)时:
  • 部分应用 / 柯里化(Partial Application / Currying): 通过在闭包内固定一些参数,创建比原始函数接受参数更少的函数。
  • 模块模式(Module Pattern): 在 ES6 模块出现之前,闭包(通常与 IIFE 结合)是创建包含私有成员(private members)和公共成员(public members)的模块的常用方式。

示例:事件处理程序中的闭包(经典问题与解决方案)

// 存在问题的循环,未正确为 'i' 创建闭包
// (由于块级作用域,使用 'let' 时此示例的行为可能符合预期,
// 但它展示了一个经典的 'var' 问题,该问题可以通过闭包或 'let' 解决。)
// <button>Button 0</button> <button>Button 1</button> <button>Button 2</button>
// 假设 HTML 中存在这些按钮,它们的 id 分别是 'btn-0'、'btn-1'、'btn-2'
/*
for (var i = 0; i < 3; i++) {
document.getElementById('btn-' + i).addEventListener('click', function() {
// 当按钮被点击时,循环已经结束,'i' 的值是 3。
console.log('You clicked button: ' + i); // 总是输出 3
});
}
*/
// 使用闭包的解决方案(使用 IIFE 或另一个函数):
/*
for (var i = 0; i < 3; i++) {
(function(currentIndex) {
document.getElementById('btn-' + currentIndex).addEventListener('click', function() {
console.log('You clicked button: ' + currentIndex);
});
})(i); // 传递 'i' 以捕获其当前值
}
*/
// 使用 'let' 的现代解决方案(在每次迭代中为 'i' 创建一个新的绑定)
/*
for (let i = 0; i < 3; i++) {
document.getElementById('btn-' + i).addEventListener('click', function() {
console.log('You clicked button: ' + i);
});
}
*/

虽然 for 循环中的 let 优雅地解决了这个特定的循环场景问题,但理解闭包对于许多其他需要保留状态(state)或上下文(context)的情况至关重要。

  • 闭包(Closures)允许函数访问外部(包围)作用域(outer (enclosing) scope)中的变量,即使外部函数已经执行完毕。
  • 它们在每次创建函数时产生,即在函数创建时产生。
  • 它们是实现诸如数据隐私(data privacy)、函数工厂(function factories)以及在异步操作(asynchronous operations)中维护状态(state)等模式的基础概念。
  • 注意内存使用:如果闭包持有对不再需要的大型对象的引用,它可能会阻止这些对象被垃圾回收(garbage collected)。这在现代 JavaScript 引擎中问题较小,但了解这一点仍然有益。