ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript 闭包陷阱经典案例解析:为什么 makeArmy 的 shooter 函数全部输出 10

JavaScript 闭包陷阱经典案例解析:为什么 makeArmy 的 shooter 函数全部输出 10 文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载本篇基于《现代 JavaScript 教程》The Modern JavaScript Tutorial即本仓库 1-js/06-advanced-functions/03-closure 章节中的「函数大军makeArmy」经典习题深入剖析 JavaScript 闭包与词法环境Lexical Environment的核心机制为什么在while循环中创建的一组函数会共享同一个外部变量最终全部输出同一个值以及如何通过let j i复制变量或改用for循环来修复。读完本文你将彻底理解闭包捕获的是变量本身而非变量的值并能独立分析、修复循环内创建函数的各类常见 bug。题目复现所有 shooter 都输出了 10原题目位于 task.md代码如下function makeArmy() { let shooters []; let i 0; while (i 10) { let shooter function() { // 创建一个 shooter 函数 alert( i ); // 应该显示其编号 }; shooters.push(shooter); // 将此 shooter 函数添加到数组中 i; } // ……返回 shooters 数组 return shooters; } let army makeArmy(); // ……所有的 shooter 显示的都是 10而不是它们的编号 0, 1, 2, 3... army[0](); // 编号为 0 的 shooter 显示的是 10 army[1](); // 编号为 1 的 shooter 显示的是 10 army[2](); // 10其他的也是这样。这段代码的意图非常直观makeArmy()创建一个包含 10 个函数的数组每个函数shooter应当输出自己对应的编号0, 1, 2, ..., 9。但实际运行结果是每个函数都输出10。根本原因闭包捕获的是变量而不是变量的值要理解这个现象需要回顾闭包与词法环境的工作原理完整理论见 变量作用域闭包每个函数在诞生时都会记住创建它的词法环境。函数内部没有局部变量i当它被调用时i来自外部的词法环境。shooter函数引用的是makeArmy()词法环境中的同一个i变量。闭包捕获的是变量本身即对变量的引用而不是变量在创建时刻的值。当army[0]()、army[5]()真正被调用时makeArmy()早已执行完毕while循环在i 10时停止因此外层词法环境中i的最终值是10。所有 10 个shooter函数共享这一个外部词法环境所以它们读取到的都是同一个i 10。下图展示了这一状态数组中的每个函数都通过outer引用指向makeArmy()的词法环境而该环境中i的值已经是10关键结论在while循环的每次迭代中虽然let shooter ...声明语句被执行了 10 次但i始终只有一个本体它存在于makeArmy()的词法环境中而不是存在于每次迭代的块级环境中。因此每次迭代应该保存当时的i这个直觉在这里失效了。修复方案一在 while 循环体内用let j i复制值while循环的块体{...}在每次迭代时都会创建一个新的词法环境。因此只要把i的当前值复制到一个属于本次迭代的局部变量中就能让每个shooter函数各自钉住属于自己的编号function makeArmy() { let shooters []; let i 0; while (i 10) { let j i; // 将 i 的当前值复制到本次迭代的局部变量 j let shooter function() { // shooter 函数 alert( j ); // 应该显示它自己的编号 }; shooters.push(shooter); i; } return shooters; } let army makeArmy(); // 现在代码正确运行了 army[0](); // 0 army[5](); // 5这里的原理在于原始类型number是按值复制的let j i得到的是一份独立于i的副本j声明在while {...}块内属于当前迭代对应的词法环境每个shooter函数创建时其[[Environment]]引用指向本次迭代的块级词法环境因此它访问的是本迭代的j而不是makeArmy()中的i。修复后的词法环境结构如下图所示——每个迭代的词法环境中都保存着自己的j各函数互不干扰修复方案二直接用 for 循环更简洁的修复方式是改用for循环。for循环有一个特殊机制每次迭代都会生成一个带有自己i的新词法环境这正是我们需要的function makeArmy() { let shooters []; for(let i 0; i 10; i) { let shooter function() { // shooter 函数 alert( i ); // 应该显示它自己的编号 }; shooters.push(shooter); } return shooters; } let army makeArmy(); army[0](); // 0 army[5](); // 5这段代码本质上与方案一完全等价for循环在每次迭代中都创建独立的词法环境其中i的值为当前迭代的编号因此每个shooter函数引用的都是属于自己的i这也解释了为什么许多风格指南推荐在循环内创建函数时优先使用for循环因为它的每次迭代天然拥有独立的变量环境不易踩中闭包共享变量的坑。当然如果你偏爱while则必须借助let j i这样的复制快照手法。用仓库自带的测试验证修复结果本仓库为该习题提供了完整的可运行示例与测试文件位于 10-make-army/_js.view 目录source.js —— 原始的错误版本while 直接引用isolution.js —— 官方修复版本for循环test.js —— 配套单元测试。测试文件明确验证了修复后的行为describe(army, function() { let army; before(function() { army makeArmy(); window.alert sinon.stub(window, alert); }); it(army[0] shows 0, function() { army[0](); assert(alert.calledWith(0)); }); it(army[5] shows 5, function() { army[5](); assert(alert.calledWith(5)); }); after(function() { window.alert.restore(); }); });测试通过sinon拦截window.alert逐一断言army[0]()调用的是alert(0)、army[5]()调用的是alert(5)。这说明判定正确性的标准正是每个函数输出自己的编号与我们前面分析的行为完全一致。总结与延伸「函数大军」是一道关于闭包的高频经典题它浓缩了三个必须牢记的知识点闭包捕获的是变量引用而非值函数记住的是创建它的词法环境读取的是该环境中变量的当前值而非创建瞬间的快照。while与for的词法环境行为不同for每次迭代都会创建独立环境while则不会需要手动用let j i制造每迭代一份变量的效果。调试这类 bug 的思路先问这个变量住在哪个词法环境里——i住在makeArmy()的环境里还是住在每次迭代的环境里答案决定了所有函数共享一个值还是各自持有独立值。理解这道题意味着你已经真正掌握了闭包的底层原理也为后续学习 函数对象、[[Environment]]与计数器、以及循环与异步回调如setTimeout与var的经典组合中的同类陷阱打下了坚实基础。赞分享文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载相关推荐JavaScript 闭包陷阱深度解析为什么 makeArmy 里所有 shooter 都输出 10Modern JavaScript Tutorial 实战篇JavaScript 闭包陷阱深度解析为什么 makeArmy 里所有 shooter 都输出 10Modern JavaScript Tutorial 实文档/教程前端TrafficMonitorPlugins10个必备插件让你的系统监控更强大TrafficMonitorPlugins10个必备插件让你的系统监控更强大 TrafficMonitorPlugins是一套专为TrafficMonitor桌面应用插件系统JavaScript 数字舍入陷阱全解析为什么 6.35.toFixed(1) 得到 6.3 而不是 6.4JavaScript 数字舍入陷阱全解析为什么 6.35.toFixed 1 得到 6.3 而不是 6.4 本篇技术指南围绕现代 JavaScript 教程文档教程前端上一篇如何构建自己的MP3标签批量管理工具基于mp3tag4cj的Cangjie应用实战案例下一篇跨机器调试 Claude Codeclaude-devtools SSH 远程会话检查与连接排障完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表