函数闭包
JavaScript 闭包
Section titled “JavaScript 闭包”闭包(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 使用词法作用域,这意味着变量的作用域由其在源代码中编写时的位置决定,而不是由函数调用的位置决定。内部函数可以“看到”其包含(外部)函数的变量。
什么是闭包?
Section titled “什么是闭包?”当在外部函数(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 变量。
使用闭包解决计数器困境
Section titled “使用闭包解决计数器困境”一个常见的问题是创建一个计数器,其中计数变量(count variable)受到保护,无法从外部修改,但可以通过一个特定函数(specific function)进行递增(incremented)。
使用全局变量(global variable)存在风险:
let globalCounter = 0; // 暴露在外,可以被代码的任何部分修改function incrementGlobal() { globalCounter++; console.log(globalCounter);}incrementGlobal(); // 1globalCounter = 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: 1counter1(); // 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_1console.log(uniqueIdGenerator()); // Output: id_2在 createCounter 中,count 变量是 createCounter 作用域的一部分。返回的匿名函数是一个闭包(closure)。每次调用 createCounter 时,都会创建一个新的作用域,从而初始化一个新的 count 变量。返回的函数维护着对其特定 count 的引用。
IIFE 版本(uniqueIdGenerator)只运行一次,设置私有的 id 变量,并返回用于递增的函数。id 受到保护,只能通过返回的函数进行修改。
闭包的常见用途
Section titled “闭包的常见用途”- 数据隐私 / 封装(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)的情况至关重要。
关于闭包的关键要点
Section titled “关于闭包的关键要点”- 闭包(Closures)允许函数访问外部(包围)作用域(outer (enclosing) scope)中的变量,即使外部函数已经执行完毕。
- 它们在每次创建函数时产生,即在函数创建时产生。
- 它们是实现诸如数据隐私(data privacy)、函数工厂(function factories)以及在异步操作(asynchronous operations)中维护状态(state)等模式的基础概念。
- 注意内存使用:如果闭包持有对不再需要的大型对象的引用,它可能会阻止这些对象被垃圾回收(garbage collected)。这在现代 JavaScript 引擎中问题较小,但了解这一点仍然有益。