3
3

forをforEachに変更するときに新しく知ったこと

Last updated at Posted at 2023-11-23

はじめに

forで書いたコードをレビューで「非同期の処理がないなら可読性向上のためにforEachの処理に変えたらどう?」とご指摘をいただいた時に新しく知ったことです。

修正したコード

// 修正前
for (let index = 0; index < docArr.length; index += 1) {
    console.log(docArr[index]);
}
// 修正後
docArr.forEach((doc, index) => {
    console.log(docArr[index]);
});

気付いたこと

ループ処理をスキップしたい時、forEachの中ではcontinueの代わりにreturnを書くこと

ちなみに以下のような処理の違いがあります。
returnは関数の中で使用され、関数の実行を終了します。
continueはループ内で使用され、そのイテレーションを終了し、次のイテレーションに進みます。

forEachは同期的な処理をしているため、非同期処理を行うには不十分

以下のようにコールバックで非同期関数を呼び出ししましたが、逐次処理がされていません。

// 参考URLのコードを実行しました
const f = (value) => {
    return new Promise((resolve) =>
        setTimeout(() => {
            console.log(value);
            resolve();
        }, Math.random() * 1000))
};

["A", "B", "C", "D", "E"].forEach(async (v) => {
    await f(v);
});
console.log("終了");
// 結果
終了
C
D
E
B
A

for以外でループ内で非同期を実行したい場合のやり方

方法1:for...ofを使用する

const arr = ["A", "B", "C", "D", "E"];
let p = Promise.resolve();

for (const v of arr) {
  p = p.then(() => f(v));
}

p.then(() => console.log("終了"));

もしくは

async function callArr() {
	for (const v of arr) {
		await f(v);
	}
    console.log("終了");
}
callArr();

方法2:forEachを工夫する

// 参考URLのコードを実行しました
let p = Promise.resolve();
["A", "B", "C", "D", "E"].forEach((v) => (p = p.then(() => f(v))));
p.then(() => console.log("終了"));

Promise.resolve()は解決済み(fulfilled)のPromiseを生成します。
pが最初に Promise.resolve() で初期化された後、then以降の() => f(v)の処理が走ります。p= で、新しいPromiseを再代入しています。そのため、前のPromiseが解決された後に、新しいPromiseが生成され、次の非同期処理が順次待機することになります。

方法3:reduceを使う
reduceメソッドは各配列の要素に対して、コールバック関数を適用し、最終的に一つの値にまとめます。

// 参考URLのコードを実行しました
["A", "B", "C", "D", "E"]
  .reduce((p, v) => p.then(() => f(v)), Promise.resolve())
  .then(() => console.log("終了"));

reduce(callbackFn, initialValue)の形をとっています。

callbackFnは (p, v) => p.then(() => f(v))
initialValueは Promise.resolve()
となります。

pは前回の callbackFn の呼び出し結果の値を受け取ります。
初回の呼び出しでは initialValue がある場合はinitialValueが入ります。

つまり、初回ではpにinitialValueのPromise.resolve()が入り、2回目以降はf(v)の結果のPromiseが渡されることとなります。

vは現在の要素の値です。初回の呼び出しでは initialValue が指定された場合は array[0] の値です。

ダメだった方法:Promise.allとmapを使う

// 参考URLのコードを実行しました
const main = async () => {
  await Promise.all(["A", "B", "C", "D", "E"].map((v) => f(v)));
  console.log("終了");
};
main();
// 結果
B
D
A
C
E
終了

Promise.allでPromiseがすべて解決されるまで待つので、順番に出力されるかと思いました。しかし、各Promiseの解決順序を保証しないので、このような結果になりました。

おわりに

ループ内で非同期処理を行うならforかfor...ofがよくて、非同期処理を使用しない場合はforよりもforEachの方が記載がシンプルでいいなと思いました。reduceについて詳しく知らなかったので勉強になりました。

参考

https://zenn.dev/sora_kumo/articles/612ca66c68ff52
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce

3
3
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3