面试题:闭包和循环的异步如何结合
在 JavaScript 中,闭包和循环的异步结合是一个经典问题,主要解决循环中异步操作捕获循环变量的问题。由于异步操作(如 setTimeout、Promise、AJAX)会在循环结束后执行,若不使用闭包,所有异步操作都会捕获到循环的最终变量值。
问题场景:循环中的异步陷阱
先看一个不使用闭包的例子,预期打印 0,1,2,但实际结果是 3,3,3:
for (var i = 0; i < 3; i++) {setTimeout(() => {console.log(i); // 输出:3 3 3(而非预期的 0 1 2)}, 100);
}
原因:var 声明的 i 是函数级作用域(全局共享),循环结束后 i 变为 3,三个 setTimeout 回调都引用同一个 i。
解决方案:用闭包绑定循环变量
闭包可以为每个异步操作创建独立的作用域,保存当前循环变量的值。
方案 1:立即执行函数(IIFE)创建闭包
for (var i = 0; i < 3; i++) {// 立即执行函数,将当前 i 作为参数传入,创建独立作用域(function (currentI) {setTimeout(() => {console.log(currentI); // 输出:0 1 2}, 100);})(i); // 传入当前循环的 i 值
}
原理:每次循环时,IIFE 会捕获当前 i 的值并创建闭包,setTimeout 回调引用的是闭包中的 currentI(固定为当前循环的值)。
方案 2:let 声明(块级作用域替代闭包)
ES6 的 let 会为每个循环迭代创建独立的块级作用域,本质上是语法糖实现了闭包效果:
for (let i = 0; i < 3; i++) { // 用 let 替代 varsetTimeout(() => {console.log(i); // 输出:0 1 2}, 100);
}
原理:let 在循环中每次迭代都会创建新的变量绑定,每个 setTimeout 回调捕获的是当前迭代的 i。
方案 3:数组方法 + 闭包(forEach/map)
数组遍历方法的回调函数本身形成闭包,可自然捕获当前元素:
[0, 1, 2].forEach(i => {setTimeout(() => {console.log(i); // 输出:0 1 2}, 100);
});
原理:forEach 的回调函数为每个元素创建独立作用域,i 是当前元素的副本。
异步循环(如接口请求)中的闭包应用
在实际项目中,常需要循环调用接口并处理结果,闭包可确保每个请求对应正确的参数:
// 模拟批量请求接口
function fetchData(id) {return new Promise(resolve => {setTimeout(() => resolve(`数据${id}`), 100);});
}// 循环发起请求,用闭包绑定每个 id
for (let i = 0; i < 3; i++) {// 用 let 声明的 i 形成块级作用域(闭包)fetchData(i).then(data => {console.log(`id=${i} 的结果:${data}`); // 输出:id=0 的结果:数据0;id=1 的结果:数据1;id=2 的结果:数据2});
}
核心总结
- 问题本质:异步操作执行时,循环变量已变化,导致所有回调共享最终值。
- 闭包作用:为每个异步操作创建独立作用域,保存循环变量的当前值。
- 现代方案:优先使用
let(块级作用域)或数组遍历方法,简化闭包写法。 - 注意:在异步循环中,若需控制并发(如限制同时请求数量),还需结合 Promise.all/race 等方法。
通过闭包,能让循环中的异步操作准确捕获每次迭代的变量状态,避免逻辑错误。
