返回

当为循环使用 setTimeout 时,谁是舞者?

前端

现象分析

让我们先来看一个简单的例子:

for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

运行这段代码,您可能会惊讶地发现,控制台中的输出是:

5 -> 5, 5, 5, 5, 5

也就是说,循环中的每个元素都输出 "5",而且几乎是同时输出的。这与我们的预期不符,因为我们希望每个元素在延迟 1 秒后才输出。

为什么会发生这种情况呢?这是因为 JavaScript 的事件循环机制。当我们调用 setTimeout 函数时,它会创建一个新的计时器,该计时器在指定的延迟时间后触发一个回调函数。在这个例子里,回调函数是匿名箭头函数 () => { console.log(i); }

问题在于,当 for 循环执行时,变量 i 的值是 0。然后,在每个计时器触发时,它都会调用回调函数,而此时 i 的值已经是 5 了。因此,所有计时器都会输出 5,而且几乎是同时输出的。

解决方法

要解决这个问题,我们需要确保在回调函数中使用的是循环中每个元素的值,而不是最后一个元素的值。有六种方法可以做到这一点:

1. 借助 let 的暂时性死区

for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);

  // 在这里声明一个新的变量 j,它与闭包中的变量 i 不同。
  let j = i;

  // 在回调函数中使用 j 而不是 i。
  setTimeout(() => {
    console.log(j);
  }, 2000);
}

输出结果:

5 -> 0, 1, 2, 3, 4

2. 使用闭包

for (let i = 0; i < 5; i++) {
  // 将 i 作为参数传递给回调函数。
  setTimeout((i) => {
    console.log(i);
  }, 1000);
}

输出结果:

5 -> 0, 1, 2, 3, 4

3. 使用 forEach 方法

[0, 1, 2, 3, 4].forEach((i) => {
  setTimeout(() => {
    console.log(i);
  }, 1000);
});

输出结果:

5 -> 0, 1, 2, 3, 4

4. 使用 map 方法

const numbers = [0, 1, 2, 3, 4];

const delayedNumbers = numbers.map((i) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(i);
    }, 1000);
  });
});

Promise.all(delayedNumbers).then((values) => {
  console.log(values);
});

输出结果:

5 -> [0, 1, 2, 3, 4]

5. 使用 async/await

async function delay(i) {
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve(i);
    }, 1000);
  });

  return i;
}

async function main() {
  const numbers = [0, 1, 2, 3, 4];

  for (const i of numbers) {
    const delayedNumber = await delay(i);
    console.log(delayedNumber);
  }
}

main();

输出结果:

5 -> 0, 1, 2, 3, 4

6. 使用 generator 函数

function* delayedNumbers() {
  for (let i = 0; i < 5; i++) {
    yield new Promise((resolve) => {
      setTimeout(() => {
        resolve(i);
      }, 1000);
    });
  }
}

async function main() {
  for await (const i of delayedNumbers()) {
    console.log(i);
  }
}

main();

输出结果:

5 -> 0, 1, 2, 3, 4

总结

在 JavaScript 中,为 for 循环里面的每个元素使用 setTimeout 时,可能会导致意想不到的行为。这是因为 JavaScript 的事件循环机制导致了回调函数在错误的时间执行。为了解决这个问题,我们需要确保在回调函数中使用的是循环中每个元素的值,而不是最后一个元素的值。本文提供了六种解决方法,帮助您更好地掌握 JavaScript 的异步编程。