11. 闭包到底是什么?如何判断? 副本.md 来源:https://my.feishu.cn/file/YkXMbaWAVoa2E7xVWM5csbb3nDc 采集状态:已完成正文采集;原文件源码视图逐行采集,行号 1 至 79 连续,滚动到底。 ### 1. 核心概念 (Core Concept) 闭包(Closure)是 JavaScript 中一个核心概念,它是指一个函数以及其周围状态(lexical environment,词法环境)的引用打包在一起的组合。换句话说,闭包允许我们将函数与该函数所运行的外部环境隔离开来。 ### 2. 为什么需要它? (The "Why") 1. **数据封装和私有变量 (Data Encapsulation and Private Variables):** 闭包提供了一种机制来创建私有作用域,使得某些变量只能在特定的函数内部访问,从而实现数据的封装,避免全局污染或意外修改。 2. **保持状态 (Maintaining State):** 闭包可以“记住”创建它们时的环境,即使外部函数已经执行完毕,闭包内部仍能访问到外部函数作用域中的变量,从而在函数调用之间保持状态。 3. **延迟执行和事件处理 (Deferred Execution and Event Handling):** 在异步操作、事件处理或定时器中,常常需要函数在 future 的某个时间点执行,闭包可以确保这些函数在执行时能访问到当时正确的作用域变量。 ### 3. API 与用法 (API & Usage) 闭包并非一个需要显式调用或特殊语法的 API,它是 JavaScript 语言中基于词法作用域的一种自然产物。当一个内部函数被“暴露”到其词法作用域之外时,闭包就形成了。 **判断闭包的核心依据是:** 一个函数是否能够记住并访问到在其**词法作用域**外部定义的变量,即使该外部作用域的函数已经执行完毕。 以下是一个经典的闭包示例: ```javascript function makeGreeter(greeting) { // 'greeting' 是 makeGreeter 的局部变量 // 返回的匿名函数形成了一个闭包 return function(name) { console.log(greeting + ', ' + name + '!'); }; } // 当 makeGreeter() 执行完毕后,它的作用域理论上应该销毁 const sayHello = makeGreeter('Hello'); const sayHey = makeGreeter('Hey'); // 但是,sayHello 和 sayHey 函数(闭包) // 仍然能够访问到创建它们时 makeGreeter 的 'greeting' 变量 sayHello('Alice'); // 输出: Hello, Alice! sayHey('Bob'); // 输出: Hey, Bob! ``` 在这个例子中,`sayHello` 和 `sayHey` 是由 `makeGreeter` 返回的内部函数。即使 `makeGreeter` 已经执行完成(其调用栈已弹出),但 `sayHello` 和 `sayHey` 仍然可以访问到 `makeGreeter` 作用域内的 `greeting` 变量。这就是闭包的体现。 另一个常见的例子,用于解决循环中的异步问题: ```javascript // 错误示例:i 在定时器执行时已经是 5 // for (var i = 0; i < 5; i++) { // setTimeout(function() { // console.log(i); // 总是输出 5 // }, i * 100); // } // 使用闭包解决 (以函数参数创建新作用域) for (var i = 0; i < 5; i++) { (function(index) { setTimeout(function() { console.log(index); // 输出 0, 1, 2, 3, 4 }, index * 100); })(i); // 将当前的 i 值作为参数传递给立即执行函数 (IIFE) } // 使用 let (块级作用域,自然形成闭包) for (let j = 0; j < 5; j++) { setTimeout(function() { console.log(j); // 输出 0, 1, 2, 3, 4 }, j * 100); } ``` 在这个循环示例中,通过立即执行函数(IIFE)创建新的作用域,将当前的 `i` 值“捕获”进去,从而使得定时器回调函数(闭包)能够访问到每次迭代时正确的 `index` 值。使用 `let` 声明则利用了其块级作用域的特性,每轮循环都会为 `j` 创建一个新的绑定,从而自然地解决了问题,这也体现了块级作用域与闭包的关系。 ### 4. 关键注意事项 (Key Considerations) 1. **内存开销 (Memory Overhead):** 闭包会使其引用的外部变量一直保存在内存中,直到闭包本身被垃圾回收。如果闭包不再需要,应解除对它的引用,避免潜在的内存泄漏。 2. **“捕获”变量的引用 (Capturing Variable Reference):** 闭包捕获的是外部变量的引用,而不是值的副本(基本类型在多数情况下看起来像副本是因为它们是按值传递的,但闭包记住的是变量本身)。如果外部变量的值在其被捕获后发生改变,闭包内部访问到的将是改变后的值(除非使用技巧如 IIFE 或 `let` 在每次迭代中创建新的变量绑定)。 3. **判断的核心是词法作用域 (Judgment Based on Lexical Scope):** 闭包的形成与函数的定义位置(词法作用域)相关,而不是执行位置。判断一个函数是否形成闭包,是看该函数是否访问了其定义位置所在的外部作用域的变量。 4. **JavaScript 中的常见现象 (Common Phenomenon in JS):** 闭包在 JavaScript 中非常常见,几乎所有的回调函数、事件处理器、模块模式等等都广泛使用了闭包的特性。理解闭包对于理解 JavaScript 的运行机制至关重要。 ### 5. 参考资料 (References) * [MDN Web Docs: Closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) * [JavaScript.info: Closures](https://javascript.info/closure) * [You Don't Know JS Yet: Scope & Closures](https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/README.md) (推荐深度阅读) 原文链接与附件索引 MDN Web Docs: Closures https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures JavaScript.info: Closures https://javascript.info/closure You Don't Know JS Yet: Scope & Closures https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/scope-closures/README.md