JavaScript Promise场景循环变量取值异常问题及修复
异步循环变量绑定问题示例与解答
共用工具函数
两段测试代码复用相同的异步工具函数:
function getBlogPosts() { const posts = [ { id: 1, title: 'Post One', body: 'A blog post!' }, { id: 2, title: 'Post Two', body: 'Another blog post!' }, { id: 3, title: 'Post Three', body: 'A third blog post!' }, ]; return new Promise((resolve) => { setTimeout(() => resolve(posts), 200); }); } function getBlogComments(postId) { const comments = [ { postId: 1, comment: 'Great post!' }, { postId: 2, comment: 'I like it.' }, { postId: 1, comment: 'You make interesting points.' }, { postId: 3, comment: 'Needs more corgis.' }, { postId: 2, comment: 'Nice work!' }, ]; // 筛选对应文章的评论 const postComments = comments.filter((comment) => comment.postId === postId); return new Promise((resolve) => { setTimeout(() => resolve(postComments), 300); }); }
错误版本代码
运行后会出现关联错误:即使返回的评论属于id为1、2的文章,展开的...post始终为第三篇文章的内容
function loadContent() { getBlogPosts().then((posts) => { for (post of posts) { getBlogComments(post.id).then((comments) => { console.log({ ...post, comments }); }); } }); } loadContent();
参考正确版本(map+Promise.all并行实现)
运行后可以正确关联文章和对应评论:
async function loadContent() { const posts = await getBlogPosts(); // 不直接await单个请求,而是生成Promise数组 const promises = posts.map((post) => { return getBlogComments(post.id).then((comments) => { return { ...post, comments }; }); }); // await Promise.all实现并行执行 const postsWithComments = await Promise.all(promises); console.log(postsWithComments); } loadContent();
问题1:错误版本为什么无法获取循环过程中对应的正确post值?
核心原因是变量绑定规则和异步执行顺序共同导致的,结合词法环境与Promise工作机制解释:
- 第一是循环变量没有独立绑定:错误代码里
for (post of posts)未声明循环变量,严格模式下会直接抛出引用错误;非严格模式下post会成为全局变量,整个运行环境中仅存在这一个post绑定;退一步说就算这里写for (var post of posts),由于var是函数级作用域,整个外层then回调中也只会生成一个post变量,三次循环迭代都是在修改这同一个变量的值。 - 第二是异步回调执行时机晚于同步循环:
getBlogPosts状态变为fulfilled后,传入的then回调会同步执行,内部的for循环会一次性完成3次遍历,同步发起3个getBlogComments请求,此时共享的post变量已经被覆盖为循环最后一个值——也就是id为3的第三篇文章。而getBlogComments的then回调属于异步任务,要等至少300ms、内部的setTimeout触发resolve后才会进入事件队列等待执行,等这些回调真正运行、沿作用域链查找post变量时,拿到的早已是被覆盖后的第三篇文章,因此无论哪篇文章的评论先返回,打印出的post内容都是第三篇。
问题2:不使用map的错误版本修复方案
存在多种不依赖map+Promise.all的修复方案,可根据是否需要并行执行选择:
方案1:保留并行逻辑,通过块级作用域/闭包绑定单次循环的post值
不需要修改并行发起请求的逻辑,只要给每次循环创建独立的词法环境,保存住当前迭代的post值即可,两种常见写法:
- 写法A:循环变量用
let/const声明。ES6规范规定,for循环头部用let/const声明的变量,会为每次迭代创建独立的绑定,每个异步回调的闭包拿到的都是对应迭代的独立post值,不会被后续循环覆盖:
function loadContent() { getBlogPosts().then((posts) => { // 用const声明循环变量,每次迭代生成独立绑定 for (const post of posts) { getBlogComments(post.id).then((comments) => { console.log({ ...post, comments }); }); } }); } loadContent();
- 写法B:用立即执行函数(IIFE)创建独立函数作用域,把当前迭代的post作为参数传入,通过闭包保存该值,可兼容不支持ES6块级作用域的旧环境:
function loadContent() { getBlogPosts().then((posts) => { for (post of posts) { // 立即执行函数接收当前post作为参数,形成独立作用域 (function(currentPost) { getBlogComments(currentPost.id).then((comments) => { console.log({ ...currentPost, comments }); }); })(post) } }); } loadContent();
方案2:串行执行逻辑,通过for...of+await逐个处理
如果场景限制不能并行发起请求,必须串行逐个拉取评论,可以直接在循环中使用await等待单个请求返回,由于每次循环的post都是当前迭代的块级变量,不会出现值覆盖问题:
async function loadContent() { const posts = await getBlogPosts(); const postsWithComments = []; // 循环内await,按顺序串行执行每个请求 for (const post of posts) { const comments = await getBlogComments(post.id); postsWithComments.push({ ...post, comments }); } console.log(postsWithComments); } loadContent();
内容的提问来源于stack exchange,提问作者Han Qi
相关产品推荐
相关产品推荐

