JavaScript 闭包是如何工作的:基于 Stack Overflow 示例的解析
\n\n如果你曾经搜索过“JavaScript closures”或在 Stack Overflow 上阅读过讨论,那么你很可能见过那个著名的问题:“How do JavaScript closures work?”。这个帖子获得了数千次浏览和数百个回答——从简单的解释到对 V8 引擎的深入探讨。为什么?因为闭包是那种初看像魔法,但随后会成为你超级武器的概念之一。
\n\n在这篇文章中,我们不仅仅是复述理论。我们将深入 JavaScript 的底层,分析内存中发生的情况,并最终理解为什么这段代码会以这种方式运行,而不是其他方式。准备好了吗?我们开始吧。
\n\n什么是闭包?简洁的定义
\n\n让我们从核心开始。闭包 (closure) 是一个函数,它“记住”了其外部作用域中的变量,即使这个外部函数已经执行完毕。
\n\n听起来像咒语?我们直接看代码。这是你在 Stack Overflow 上能找到的经典示例:
\n\nfunction createCounter() {\n let count = 0;\n return function() {\n count++;\n return count;\n };\n}\n\nconst counter = createCounter();\nconsole.log(counter()); // 1\nconsole.log(counter()); // 2\nconsole.log(counter()); // 3\n\n这里,createCounter 返回一个匿名函数。这个内部函数“封闭”了变量 count。即使在 createCounter 执行完毕之后(我们以为它内部的所有变量都应该消失),count 仍然存在,并且在每次调用 counter() 时发生变化。这就是闭包。
底层原理:词法环境
\n\n要理解这是如何工作的,我们需要深入 JavaScript 引擎的内部。每个函数在创建时都有一个隐藏属性 [[Environment]]。它存储了对创建该函数的词法环境 (Lexical Environment) 的引用。
词法环境就是一个对象,它包含:
\n- \n
- 环境记录 (Environment Record) —— 在该作用域中声明的所有变量和函数。 \n
- 对外部环境的引用 (outer) —— 父级作用域。 \n
当我们调用一个函数时,会为该调用创建一个新的词法环境。内部函数通过作用域链 (scope chain) 访问外部函数的变量。
\n\n回到我们的示例。当我们声明 createCounter 时,会创建一个全局词法环境。其中包含变量 createCounter(函数)和 counter(目前为 undefined)。
当我们调用 createCounter() 时:
- \n
- 为该调用创建一个新的词法环境。其中包含变量
count = 0。 \n - 创建一个匿名函数。它的
[[Environment]]指向当前环境(其中存在count)。 \n - 该函数被返回并赋值给
counter。 \n
最有趣的部分来了:createCounter 执行完毕,但它的词法环境并没有被垃圾回收器删除!因为有一个指向它的引用——内部函数的 [[Environment]]。只要 counter 函数本身存在,这个环境就会一直留在内存中。
现在,每次我们调用 counter() 时,都会为该调用创建一个新的环境,它通过作用域链看到那个包含 count 的旧环境。这样我们就得到了“记忆”的效果。
为什么这很重要?实际应用场景
\n\n闭包不仅仅是一个学术概念。它们被广泛使用。让我们分析几个实际例子。
\n\n1. 私有变量(封装)
\n\n在 JavaScript 中没有真正的