你以为只是“延迟执行”其实变量早就被偷换了在 JavaScript 中setTimeout是最常用的异步工具之一。但当它和for循环、闭包一起出现时无数开发者都踩过同一个坑for(vari0;i3;i){setTimeout((){console.log(i);// 你期待输出 0,1,2实际却是 3,3,3},100);}为什么因为varsetTimeout 闭包 变量共享陷阱。今天我们就彻底拆解这个经典问题并告诉你如何用现代 JS 写出正确、安全、可维护的延迟逻辑。问题根源var的函数作用域 异步执行关键点有二1.var没有块级作用域for循环中的var i实际上是在整个函数或全局作用域中声明一次所有循环迭代共享同一个i。2.setTimeout是异步的当setTimeout的回调真正执行时for循环早已结束此时i的值已经是3循环终止条件。所以三个回调都引用了同一个已经变成 3 的变量i。常见错误解法别再用了解法一用setTimeout第三个参数传参可行但不推荐for(vari0;i3;i){setTimeout((x){console.log(x);},100,i);// 把 i 作为参数传入}虽然能工作但语义不直观回调函数签名被污染在复杂逻辑中难以维护。解法二立即执行函数IIFE——过时方案for(vari0;i3;i){(function(j){setTimeout((){console.log(j);},100);})(i);}这确实能创建新作用域但代码冗长阅读成本高ES6 之后已有更优雅方案。正确姿势用let声明循环变量这是最简单、最现代、最推荐的方式for(leti0;i3;i){setTimeout((){console.log(i);// 输出 0, 1, 2},100);}为什么let能解决let具有块级作用域每次循环迭代都会创建一个新的绑定binding每个setTimeout回调捕获的是当前迭代的独立 i互不干扰。这不是“魔法”而是 ES6 规范明确规定的语义。更复杂的场景循环中创建函数数组陷阱不止出现在setTimeout任何异步回调或延迟执行的函数都可能中招consthandlers[];for(vari0;i3;i){handlers.push(()console.log(i));}handlers.forEach(fnfn());// 输出 3,3,3修复方式同样简单consthandlers[];for(leti0;i3;i){handlers.push(()console.log(i));// 输出 0,1,2}或者用Array.map等函数式写法天然避免问题consthandlers[0,1,2].map(i()console.log(i));特别提醒Node.js 和浏览器都一样这个陷阱与运行环境无关无论是浏览器中的事件监听Node.js 中的定时任务React/Vue 中的副作用处理只要涉及var 异步 循环就可能出错。终极建议彻底告别var在现代 JavaScript 工程中默认使用const不可变绑定需要重赋值时用let永远不要用var除非维护老代码。配合 ESLint 规则{rules:{no-var:error}}从源头杜绝此类问题。结语setTimeout本身没有错错的是我们对作用域和闭包的理解偏差。而let的出现正是为了终结这类“反直觉”的陷阱。下次当你写循环异步时请记住不是代码跑错了是你还在用十年前的变量声明方式。升级你的语法远离闭包陷阱转发给那个还在用var写循环的同事吧各位互联网搭子要是这篇文章成功引起了你的注意别犹豫关注、点赞、评论、分享走一波让我们把这份默契延续下去一起在知识的海洋里乘风破浪