根本原因是var声明的变量共享同一作用域,异步回调执行时循环已结束;解决关键是为每次迭代创建独立变量绑定:首选let(es6块级作用域,简洁可靠),次选iife(兼容es5,冗长不推荐)。

循环中闭包输出全是最后一个值,根本原因是 var 声明的变量共享同一个作用域,而异步回调(如 setTimeout)执行时循环早已结束。解决的关键,是让每次迭代拥有自己独立的变量绑定。目前主流方案就两个:用 let,或用 IIFE,但它们的适用场景和写法逻辑不同。
let 是现代首选:简洁、语义清晰、符合规范
ES6 的 let 天然具备块级作用域特性。在 for 循环中,每次迭代都会为 i 创建一个全新的绑定,每个闭包捕获的都是当次迭代独有的 i,互不干扰。
- 写法干净:直接把
var i换成let i,无需额外包装 - 行为确定:这是语言规范明确保证的,不是“黑魔法”
- 适用广泛:不仅对 setTimeout 有效,对事件监听、Promise.then、forEach 回调等所有异步/延迟场景都适用
示例:
for (let i = 0; i setTimeout(() => console.log(i), 100); // 输出 0, 1, 2IIFE 是兼容旧环境的兜底方案
在必须支持 ES5 或不能使用 let 的老项目里,IIFE(立即执行函数)仍可手动构造独立作用域:把当前的 i 作为参数传入,使内部函数闭包捕获的是这个形参的副本。
- 核心结构:
(function(j) { /* 使用 j */ })(i) - 本质是“把变量值快照下来”,靠函数参数实现隔离
- 缺点明显:代码冗长、嵌套深、可读性差,已不推荐在新项目中主动采用
示例:
for (var i = 0; i (function(j) {setTimeout(() => console.log(j), 100);
})(i); // 输出 0, 1, 2
其他可行但需谨慎的方式
有些写法能“绕过”问题,但并非真正解决问题,也不够健壮:
-
setTimeout 第三个参数传值:如
setTimeout(console.log, 100, i)—— 可行但语义模糊,回调函数签名被强行改变,不适合复杂逻辑 -
数组 forEach 替代 for:因 forEach 回调参数天然隔离,
[0,1,2].forEach(i => setTimeout(...))也能工作,但前提是已有数组;若原逻辑依赖索引或需 break/continue,则不适用
真正要避开的误区
别再用这些方式“假装解决了”:
- 在 IIFE 里继续用
var i—— 作用域没变,只是多套了一层壳,问题照旧 - 以为加个
console.log(i)在循环体内就能“固定”值 —— 同步日志不影响异步回调里的引用关系 - 只改了声明方式却忽略对象引用陷阱 —— 若闭包捕获的是 config 对象,后续修改 config.url,回调仍会读到新值,这时需要深拷贝或冻结快照











