)
教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载导读在 JavaScript 中循环 异步回调如setTimeout、事件监听、网络请求是最容易踩坑的组合之一。本文基于 jstips 仓库第 34 期技巧 Implementing asynchronous loop完整还原每秒打印循环索引这一经典需求从失败到成功的全过程剖析var提升、闭包按值传递与 ES6let块级作用域的底层原理并结合仓库内多篇关联技巧给出可直接复制运行的解决方案。读完本文你将彻底掌握异步循环的正确写法并能向他人清晰解释为什么输出全是 5。问题现场一个每秒打印索引的异步函数假设我们要写一个异步函数让循环索引i每经过一秒输出一次。最直觉的写法如下for (var i0; i5; i) { setTimeout(function(){ console.log(i); }, 1000 * (i1)); }预期的输出是0, 1, 2, 3, 4但实际运行结果却是 5 5 5 5 5五个回调全部打印了同一个值5显然这完全不符合预期。原因剖析回调引用的不是当时的 i之所以会出现这种结果根源在于var声明的i是函数级作用域且五个setTimeout回调共享同一个变量。具体执行顺序是for循环同步地、极快地跑完 5 次迭代i从 0 递增到 5循环条件i 5不再满足而退出五个setTimeout定时器在 1 秒、2 秒……5 秒后依次触发回调触发时它们访问的i早已不是创建定时器那一刻的值而是循环结束后当前作用域里的最新值——5。也就是说每个 timeout 指向的是原始的i而不是它的拷贝。这一点与本仓库另一篇技巧 Closures inside loops 描述的现象完全一致闭包捕获的是变量本身按引用捕获而非捕获时的快照。直觉方案临时变量缓存为什么依然失败问题看似简单一个立刻浮现的想法是把循环索引缓存在一个临时变量里for (var i0; i5; i) { var temp i; setTimeout(function(){ console.log(temp); }, 1000 * (i1)); }然而再次运行输出变成了 4 4 4 4 4虽然从5变成了4但依然不是我们想要的0, 1, 2, 3, 4。为什么var temp i也救不了场原因有两点缺一不可块block不会创建作用域var的作用域是所在函数而非所在花括号。因此每一次迭代里的var temp声明实际都指向同一个变量变量声明提升hoistingvar的声明会被提升到作用域顶部变量初始化器同样如此。这一点在仓库技巧 Hoisting 中有详细说明——变量声明与函数定义会被提升到顶部而赋值并不会。事实上上面那段代码和下面这段完全等价var temp; for (var i0; i5; i) { temp i; setTimeout(function(){ console.log(temp); }, 1000 * (i1)); }把声明提到循环外后看得更清楚五个回调共享同一个temp循环结束时temp的最终值停在4最后一次赋值是temp i此时i为 4于是五个回调全都输出 4。可见只要共享同一个变量无论它叫i还是temp结果都一样失败。解决方案一IIFE 立即执行函数创建闭包按值捕获索引要打破共享变量的困局最经典的做法是在每次迭代中创建一个独立的函数作用域并把i作为参数按值传进去。这里我们使用自调用函数IIFEImmediately Invoked Function Expressionfor (var i0; i5; i) { (function(num){ setTimeout(function(){ console.log(num); }, 1000 * (i1)); })(i); }每次迭代都会立即调用一次匿名函数i的当前值0、1、2、3、4作为实参num被传入。由于函数参数是按值传递的num是i的一份独立拷贝因此 5 个setTimeout回调各自闭包捕获的num互不相同最终正确输出0, 1, 2, 3, 4。深入JavaScript 的参数传递语义为什么传入参数就能冻结住值这取决于 JavaScript 的参数传递规则原文对此有精辟总结原始类型如 Number、Date、String按值传递传入函数后基本相当于一份拷贝。在函数内部修改它们不会影响外部作用域的对应变量对象Object是特例对象本身传递的是引用。如果内部函数修改了对象的某个属性这个改动会反映到所有引用该对象的作用域中。也就是说对于循环索引这种数字原始类型按值传入恰好切断了与外层i的绑定闭包因此拿到了真正属于自己的快照。这一借助闭包作用域向回调传参的思路在仓库技巧 Passing arguments to callback functions 中也有同款应用通过返回闭包把参数a、b携带进事件回调或用Function.prototype.bind预先绑定参数原理同源。变体为什么有人写成num却仍引用i注意上面的代码里定时器延迟部分仍写的是1000 * (i1)这是同步执行阶段就被计算好的数值与异步回调无关因此即使引用i也安全。真正需要按值捕获的只有回调内部使用的变量。若不小心在回调内仍写console.log(i)则会退回到最初的错误行为——这是理解本方案的关键细节。解决方案二ES6let块级作用域一行替换彻底解决另一个更简洁的方案是使用 ES6 的let关键字。let与var最大的区别在于作用域粒度let是块级作用域block scopedvar是函数级作用域。for (let i0; i5; i) { setTimeout(function(){ console.log(i); }, 1000 * (i1)); }for循环的循环体就是一个块。let声明的i只存活于当前这一次迭代的块内并且每次迭代都会重新绑定一个新的i其初值继承自上一轮迭代结束时的值。于是 5 个回调闭包各自捕获了 5 个不同的i正确输出0, 1, 2, 3, 4且无需额外的 IIFE 包装。佐证let 在循环中的每轮重新绑定语义仓库技巧 ES6, var vs let 对两种关键字做了系统对比对比维度varlet作用域函数作用域或全局块级作用域提升hoisting声明提升访问得到undefined不提升访问触发ReferenceError循环中的闭包所有闭包共享同一变量每轮迭代重新绑定闭包各自独立for (var i 0; i 5; i) { setTimeout(function () { console.log(i); // 输出 5 五次 }, 100); }替换为let后for (let i 0; i 5; i) { setTimeout(function () { console.log(i); // 输出 0, 1, 2, 3, 4 }, 100); }同一现象在 Closures inside loops 的funcs数组案例中再次被印证用var收集 3 个函数全部打印i value is 3用 IIFE 按值传入或改用let才能分别打印0/1/2。可见异步循环 闭包的陷阱与解法在多种场景下完全互通。补充对比三种方案的取舍与最佳实践把本文讨论的三种写法放在一起对比方案代码形态原理适用场景直接varsetTimeout(function(){ console.log(i); }, ...)共享函数作用域变量不可用输出全为 5var temp i循环体内缓存临时变量var提升 无块级作用域不可用输出全为 4IIFE 闭包(function(num){ ... })(i)参数按值传递闭包独立快照ES5 环境、需要保留var风格时letfor (let i0; ...)块级作用域每轮重新绑定ES6 环境现代浏览器与 Node.js需要说明的是选择let并不意味着var应被全面取代。正如 ES6, var vs let 中强调的let是新的块级作用域 var应当仅在原本就需要块级语义的位置使用它var仍是该变量贯穿整个函数的有效信号。此外若需在旧浏览器ES5 时代中运行可借助转译器将let编译为兼容代码或直接采用 IIFE 闭包方案。总结循环 异步的经典陷阱本质是闭包按引用捕获变量与var函数级作用域/声明提升叠加的结果。jstips 第 34 期技巧给出了一条清晰的解决路径先用var踩坑理解原因再用 IIFE 按值传参建立闭包快照最后用 ES6let一劳永逸。将这一思路从setTimeout推广到事件监听、fetch、setInterval等一切异步回调场景都能写出符合直觉、行为可预期的代码。更多同主题延伸可继续阅读仓库中的 Closures inside loops、ES6, var vs let、Hoisting 与 Passing arguments to callback functions 等关联文章。赞分享教程【免费下载链接】jstipsThis is about useful JS tips!项目地址https://gitcode.com/gh_mirrors/js/jstips点击查看免费下载相关推荐jstips 精读Tip 34实现异步循环——破解 setTimeout 闭包陷阱与 let 块作用域jstips 精读Tip 34实现异步循环——破解 setTimeout 闭包陷阱与 let 块作用域 导读 在 JavaScript 中把循环与 se教程Mac Mouse Fix3条侧键映射让鼠标好用一档Mac Mouse Fix3条侧键映射让鼠标好用一档 你在 Chrome 里按下前进侧键页面毫无反应。这个按键在 Windows 上负责前进macOS教程深入对比 ES6 中 var 与 let作用域、变量提升、循环闭包与选型实践深入对比 ES6 中 var 与 let作用域、变量提升、循环闭包与选型实践 var 与 let 是 JavaScript 中最基础也最常被混淆的两个声明关键教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考