news 2026/10/7 2:05:36

JavaScript 闭包独立计数:深入解析 makeCounter 多次调用下的词法环境与状态隔离

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript 闭包独立计数:深入解析 makeCounter 多次调用下的词法环境与状态隔离
  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

本文围绕现代 JavaScript 教程(The Modern JavaScript Tutorial)中「Counter 是独立的吗?」这一经典习题展开,基于 1-js/06-advanced-functions/03-closure/3-counter-independent/task.md 及其配套答案,结合 闭包章节正文 的词法环境原理,彻底讲清「为什么同一工厂函数产出的两个闭包互不干扰、各自维护独立的 count」,并延伸讲解共享闭包、循环陷阱等进阶场景。读完本文,你将能准确预判任意闭包对外部变量的访问结果,并能熟练运用[[Environment]]、词法环境链等概念分析复杂的嵌套函数代码。

一、题目与直觉:两个计数器是否共享状态

原题给出了一个最简计数器工厂:

function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); let counter2 = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter2() ); // ? alert( counter2() ); // ?

问题在于:用同一个makeCounter创建的两个计数器counter与counter2,第二个会从0, 1接着数,还是输出2, 3?

从表面直觉看,两个函数都来自同一个工厂,count似乎应该被「共享」。但正确答案是:counter2()依次输出0和1,两个计数器完全独立。

原因在配套答案 solution.md 中一句话点破:

函数counter和counter2是通过makeCounter的不同调用创建的,因此它们具有独立的外部词法环境,每一个都有自己的count。

二、底层原理:每次函数调用都会创建独立的词法环境

要真正理解上面这句话,必须回到闭包章节正文(article.md)中对词法环境(Lexical Environment)的定义。

在 JavaScript 中,每个运行的函数、代码块{...}以及整个脚本,都有一个隐藏的关联对象——词法环境。它由两部分组成:

  1. 环境记录(Environment Record):一个存储所有局部变量作为其属性的对象(还包含this等信息);
  2. 对外部词法环境的引用:指向外层代码对应的词法环境。

「变量」本质上是环境记录这个内部对象的属性,读取或修改变量,就是读取或修改词法环境的属性。

关键点在于:每次调用makeCounter(),都会在调用开始时自动创建一个全新的词法环境,用来存储这次调用的局部变量和参数。以makeCounter为例,代码中虽然只写了一个count,但两次调用会产生两个互不相干的{count: 0}环境记录,如下图所示:

图中的矩形代表环境记录(存放变量),箭头代表对外部词法环境的引用;每次调用makeCounter(),右侧都会生成一个新的环境对象。

三、[[Environment]]:函数「诞生地」的永久记忆

那么,返回的匿名函数凭什么能找到属于自己的count?

所有的函数在创建(诞生)时,都会记住创建它们的词法环境。技术上讲,每个函数都有一个名为[[Environment]]的隐藏属性,保存着对创建该函数时所在词法环境的引用:

  • counter.[[Environment]]指向第一次makeCounter()调用创建的环境{count: 0};
  • counter2.[[Environment]]指向第二次makeCounter()调用创建的环境{count: 0}。

该引用在函数创建时被设置并永久保存,与函数之后在哪里被调用无关。这正是闭包的本质——在 JavaScript 中,所有函数都是天然闭包:它们能通过[[Environment]]访问「出生地」的外部变量。

四、调用时刻:变量查找沿词法环境链进行

当调用counter()时,引擎会为该调用再创建一个新的词法环境,并把它的外部引用指向counter.[[Environment]]:

访问count时,查找顺序是:先搜索函数自身的内部词法环境(为空,因为没有局部变量),再顺着外部引用找到makeCounter()的词法环境,在那里找到count并修改它。修改只发生在counter自己的那个环境中:

于是counter()反复调用时,count在同一位置从0递增到1、2、3……而counter2走的完全是另一条环境链,修改的是另一个{count: 0}记录。两条链互不相通,所以输出0, 1。

若用完整的查找规则概括:当代码访问一个变量时,会先搜索内部词法环境,然后依次向外搜索,直到全局词法环境(参见 article.md 的 Step 3)。变量在哪个环境中被找到,就在哪个环境中被更新。

五、同族习题:从「独立」到「共享」的边界

理解了「每次调用独立建环境」后,同目录下的姊妹习题正好帮你划清边界:

5.1 闭包只看创建位置,不看调用位置

2-closure-variable-access/task.md 中,makeWorker内部有let name = "Pete",全局又有let name = "John",返回的函数在全局调用时显示的是"Pete"——因为闭包捕获的是创建位置的外部词法环境,调用位置的变量对它毫无影响。这与本题目「独立计数器」共享同一套原理:决定变量归属的永远是[[Environment]]指向的「出生地」。

5.2 同一个环境内的多个闭包则共享变量

4-counter-object-independent/task.md 给出了对照案例:Counter构造函数内部用this.up和this.down两个函数共享同一个count:

function Counter() { let count = 0; this.up = function() { return ++count; }; this.down = function() { return --count; }; } let counter = new Counter(); alert( counter.up() ); // 1 alert( counter.up() ); // 2 alert( counter.down() ); // 1

答案(见 solution.md)指出:这两个嵌套函数是在同一次Counter()调用、同一个词法环境中创建的,所以共享同一个count。对比之下结论非常清晰:

  • 不同调用 → 不同词法环境 → 变量相互独立(本文主题);
  • 同一调用 → 同一词法环境 → 变量被多个闭包共享(本题)。

5.3 循环陷阱:所有闭包共享同一份i

反向的经典误区出现在 10-make-army/task.md:「函数大军」中 10 个shooter全部输出10而不是0..9。原因与本题完全对称:所有 shooter 都诞生于makeArmy()的同一个词法环境,调用时读取的是环境中最终的i = 10。修复方式有两种(详见 10-make-army/solution.md):

  • 在while循环体内用let j = i;复制一份「迭代局部」变量;
  • 或改用for (let i = 0; ...)——for每次迭代都会生成带有自己i的新词法环境。

这从反面印证了:闭包之间是否共享状态,完全取决于它们是否指向同一个词法环境。

六、内存视角:独立的词法环境何时被回收

独立计数器还引出一个内存问题:makeCounter()调用结束后,它的词法环境会不会被销毁?

正常情况下,函数调用完成后,其词法环境会随引用消失而被垃圾回收。但只要返回的嵌套函数仍可达,它持有的[[Environment]]引用就会让对应的词法环境继续存活(article.md「垃圾收集」一节)。因此counter存在一天,第一次调用的{count: ...}就保留一天;counter2同理,两份环境各自独立地在内存中存活。

若不再需要计数器,将引用置空即可释放:counter = null;——当词法环境对象变得不可达时,它就会像其他对象一样被回收。

七、小结与自查

回到本文标题的问题——用同一个工厂函数创建的两个计数器是独立的吗?

场景词法环境结果
makeCounter()两次独立调用两个不同环境,各有自己的count独立:0,1与0,1
同一构造函数内创建up/down同一个环境,共享count共享:1,2,1
循环中创建 N 个闭包(while)同一环境,共享最终i全部输出10(陷阱)
循环中创建 N 个闭包(for)每次迭代一个新环境各自输出0..9

判断闭包行为的唯一准则,就是找到函数[[Environment]]指向的词法环境,再沿环境链完成变量查找。建议读者回到 task.md 亲手运行代码验证输出,并依次完成 2-closure-variable-access、4-counter-object-independent 与 10-make-army 三道对照习题,即可彻底掌握词法作用域与闭包状态隔离这一核心主题。

  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载
上一篇:Crater数据库索引使用情况:监控与优化未使用索引
下一篇:终极指南:如何配置bspwm与Firefox实现无缝窗口管理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:04:03

AD7177-2 32位ADC驱动实战:寄存器配置、SPI时序与避坑指南

简介:这份资源面向嵌入式驱动开发工程师与ADC应用开发者,提供AD7177-2高精度Σ-Δ型模数转换器的驱动实现参考,帮助解决芯片初始化配置、寄存器读写、数据采集与异常处理等实际问题。压缩包共5个文件,以3个.h头文件与2个.c源文件为…

作者头像 李华
网站建设 2026/10/7 2:03:53

Electron Forge Flatpak Maker 完全指南:构建沙箱化 Linux 应用安装包

开发工具桌面应用前端构建 【免费下载链接】forge :electron: A complete tool for building and publishing Electron applications 项目地址: https://gitcode.com/gh_mirrors/fo/forge 点击查看 免费下载 导读 electron-forge/maker-flatpak 是 Electron Forge…

作者头像 李华
网站建设 2026/10/7 2:03:22

C#调用金橙子MarkEzd.dll做激光打标上位机:封装、避坑与产线集成

简介:金橙子激光打标软件二次开发所需的MarkEzd.dll动态链接库与配套头文件包,面向需要在C#环境下调用其接口的上位机开发、自动化设备集成及打标控制类工程师。压缩包内共2个文件,整体大小约29KB,其中dll封装了图形处理、参数配置…

作者头像 李华
网站建设 2026/10/7 2:02:51

ponytail代码片段管理插件:安装、配置与团队协作实战

最近逛代码效率工具的社区,会看到不少关于 ponytail 的讨论。这名字乍看像个搞怪的马尾辫玩具,实际上是一款非常实在的代码片段管理与开发效率插件。最开始我是在扩展商店搜 snippet 增强工具时偶然发现的,装上一试,居然把团队里纠…

作者头像 李华