👾

javascriptのforEachは順番に(同期的に)実行される

2024/04/10に公開

現在(2024/04/10)、「javascript forEach 順番」で検索すると一番上に表示されるのがこの記事で、forEachに渡した関数は同期的に処理されないという内容なのですが、全体的に間違っていると思われます。

一度チーム内でforEachって順番に実行されないんだね?となってしまったこともあり、この記事でまとめてみることにしました。
元記事へのコメント形式なので、そんなの当たり前だろ!という方はすみません!

まず、公式のforEachの説明を読んでみる

forEach() メソッドは反復処理メソッドです。指定された関数 callbackFn を配列に含まれる各要素に対して一度ずつ、昇順で呼び出します

同期処理のようですね

❌ forEachに渡している関数は即時関数

これは元記事のコメントでも指摘されていることですが...

まず、いわゆる即時関数(IIFE)はこちらになります。
定義されると同時に実行される関数のことですね

(function () {
  console.log("hello!")
})();

一方、forEachはこちらになります。
forEachに渡しているコールバック関数((v) => console.log(v))はプログラムの実行時に定義されますが、定義と同時に実行される(呼び出される)わけではなく、イテレーションの度に呼び出されることになります
なので、即時関数ではないですね

array.forEach((v) => console.log(v));

❌ 即時関数はjavascriptの言語特性上、平行で実行される

(漢字ミスはさておき)javascriptで並行処理が行われるのは、promiseasync/await構文によって実行環境の非同期APIに処理を投げている場合だけです。
即時関数だからといって並行に実行されたりはしません。

では、元記事の「forEachを使うと、想定していた結果にならない」というのは?

これはおそらくforEachに渡したコールバック関数内ではawaitがうまく動作しないという話だと思います。

const myArray = [1, 2, 3, 4, 5];

async function asyncFunction(item) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`Processing ${item}`);
      resolve();
    }, Math.random() * 1000);
  });
}

async function processArray() {
  myArray.forEach(async (item) => {
    await asyncFunction(item);
  });
}

processArray().then(() => {
  console.log("終了");
})

// Output: 順番にならない&「終了」が先に来てしまう!
// 終了
// Processing 3
// Processing 5
// Processing 4
// Processing 1
// Processing 2

forEachに渡されたasyncコールバック関数は同期的に(順番に)実行されますが、forEachにはawaitでPromiseの履行を待つ機能がないため、asyncFunction()が完了するのを待たずに次のループに移ってしまい、結果的に期待するような動作にはなりません。

async/awaitしたい場合は、forfor...ofを使うのが一般的です。

async function processArray() {
  for (const item of myArray) {
    await asyncFunction(item);
  } 
}

// Output
// Processing 1
// Processing 2
// Processing 3
// Processing 4
// Processing 5
// 終了

また、arrayを順番通りに実行する必要がない場合は、Promise.allmapで並行処理にすることもできます。

async function processArray() {
  await Promise.all(
    myArray.map((item) => asyncFunction(item))
  )
}

// Output
// Processing 4
// Processing 3
// Processing 5
// Processing 1
// Processing 2
// 終了

結論

array.forEachforと同様に同期的に実行される関数なので、処理は順番に行われます。
ただし、非同期処理を書くときはforEach内ではawaitが動作しないため、for文やPromise.allで書く必要があります。

参考文献

Discussion