- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
本文围绕现代 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 中,每个运行的函数、代码块{...}以及整个脚本,都有一个隐藏的关联对象——词法环境。它由两部分组成:
- 环境记录(Environment Record):一个存储所有局部变量作为其属性的对象(还包含
this等信息); - 对外部词法环境的引用:指向外层代码对应的词法环境。
「变量」本质上是环境记录这个内部对象的属性,读取或修改变量,就是读取或修改词法环境的属性。
关键点在于:每次调用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 相关知识。
相关推荐
JavaScript 闭包计数器独立性解析:makeCounter 多次调用为何各自计数
JavaScript 闭包计数器独立性解析:makeCounter 多次调用为何各自计数 导读 本篇文章基于现代 JavaScript 教程(Modern Ja
文档/教程前端深入理解 JavaScript 闭包独立性:以 makeCounter 双计数器为例
深入理解 JavaScript 闭包独立性:以 makeCounter 双计数器为例 闭包(Closure)是 JavaScript 中最核心也最容易被误解的概
文档/教程前端Whole Program LLVM高级特性:LTO支持与位码存储功能详解
Whole Program LLVM高级特性:LTO支持与位码存储功能详解 Whole Program LLVM(WLLVM)是一个强大的工具,用于从C/C++
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考