【TS/JS】ユースケース別 ループの書き方【同期/非同期/直列/並列】
はじめに
JavaScript, TypeScriptにはループを実現する方法がいくつもあり、ユースケースによって最適なものが異なります。
今では理解できるようになりましたが、初心者の頃はループの書き方が多すぎてどれを使っていいのかわからず、適切でない使用方法をしていたことがありました。
forループの種類を誰かに説明するにはあまりにパターンが多く、まとまっている記事も見当たらなかったため、この記事でまとめます。
ぜひチートシートとしてご活用ください!
配列の要素を繰り返し処理する
const items = [1, 2, 3, 4, 5];
for (const item of items) {
console.log(item);
}
まずは基本型です。
for ... in
という似たような構文もあり混乱してしまいますが、for ... in
は継承元のプロパティも含めて列挙する仕様となっており、デバッグ用途以外で使うことはほとんどありません。
多くのスタイルガイドで非推奨とされていることが多いので、for ... of
を使いましょう。
indexを取りたい場合
const items = [1, 2, 3, 4, 5];
for (const [index, item] of items.entries()) {
console.log(index, item);
}
items.entries()
で配列のインデックスと値のペアを取得できます。
別の書き方
const items = [1, 2, 3, 4, 5];
items.forEach((item, index) => {
console.log(index, item);
});
forEach
を使うことでより簡潔に書くことができます。
ただし、こちらはbreak
やcontinue
ができず非同期処理が意図した通りに動作しないなど癖があります。
式として値を返したい場合
const items = [1, 2, 3, 4, 5];
const result = items.map((item, index) => {
return `${index}: ${item}`;
});
console.log(result);
forEach
は戻り値がvoid
となっており、値を返せない仕様になっています。
値を返したい場合にはそのための関数としてmap
が用意されています。
逆に、値を返さないのにmap
を使うのはコードの意図が伝わりづらくなってしまうため、やめましょう。
配列には他にも多くの便利なメソッドがあり面白いので、興味を持った方はぜひ調べてみてくださいね。
指定回数繰り返す
他の言語ではfor n in range(10)
のような配列を生成する組み込み関数が用意されていることもありますが、JavaScriptにそれはないため、自力で用意する必要があります。
そのため書き方が複数あり、好みになってしまいます。
const count = 10;
for (let i = 0; i < count; i++) {
console.log(i);
}
一番良く見るパターンです。
i
がlet
で宣言されているため、ループ内で値を変更できてしまうデメリットがあります。
実害はあまりないもしれませんが、コーディング規約によっては禁止されていることもあるので、使わずに済む方法も覚えておくとよいでしょう。
const count = 100;
for (const i of Array(count).keys()) {
console.log(i);
}
こちらは別解です。
Array(count)
で指定した長さの空の配列を生成し、keys()
でそのindexだけを取り出しています。
Array.from({ length: count }, (_, i) => i)
などよりもサクッと作れるのでおすすめです。
条件を満たす間繰り返す
while
を使った方法
class SmartPhone {
private battery: number = 0;
charge(): void {
this.battery += 10;
console.log('charge:', this.battery);
}
isLowBattery(): boolean {
console.log('isLowBattery:', this.battery < 100);
return this.battery < 100;
}
}
const smartPhone = new SmartPhone();
while(smartPhone.isLowBattery()) { // trueの間繰り返す
smartPhone.charge();
}
while
を使うことで条件を満たす間繰り返す処理を実現できます。
途中でcontinue
やbreak
を使うことで、ループを抜けたりスキップしたりすることもできます。
do while
を使った方法
const smartPhone = new SmartPhone();
do {
smartPhone.charge();
} while(smartPhone.isLowBattery());
do while
を使うことで最初の一回だけ条件を無視して処理を実行することもできます。
前回の結果を次のループに使いたい
const hanoi = (height: number, first: number, temp: number, last: number): void => {
if (height === 0) return; // 早期リターンで止め時を指定しておく
hanoi(height - 1, first, last, temp);
console.log(`${first} -> ${last}`);
hanoi(height - 1, temp, first, last);
};
hanoi(3, 1, 2, 3);
自分自身を呼び出すことで次のループに前回の結果を引き継ぐことができます。
この方法は末尾再帰の実装があるSafariやBun以外のランタイムではStack Overflowを起こす可能性があるため、注意が必要です。
再帰的な構造が必要なければreduce
を使うことで同様の処理を実現できます。
const items = [1, 2, 3, 4, 5];
const result = items.reduce((acc, item) => {
console.log(acc, item);
return acc + item;
}, 0);
console.log('Total:', result);
非同期処理
非同期処理を含む場合、ループはさらに複雑になります。
直列処理
const items = [1, 2, 3, 4, 5];
const process = async (item: number) => new Promise((resolve) => {
console.log(item);
setTimeout(resolve, 1000);
});
const run = async () => {
for (const item of items) {
await process(item);
}
};
void run();
直列処理は、配列の要素を順番に一つずつ待ち、終わり次第次の要素を処理するものです。
このような処理がしたい場合はforEachでは実現できません。
for ... of
のブロック内ではawait
を使うことができるため、非同期関数を呼び出すことができます。
直列処理かつ結果を取得したい場合
const items = [1, 2, 3, 4, 5];
const process = async (item: number): Promise<number> =>
new Promise((resolve) => {
console.log(item);
setTimeout(() => resolve(item), 1000);
});
const run = async () => {
const result = await Array.fromAsync(items, process);
console.log(result);
}
void run();
ES2024で追加された新しい書き方です。
Promise.all
とArray.from
の間の子のような関数で、こちらも配列の要素を順番に待機して処理することができます。
以前はこの書き方ができず、結果を可変の配列に追加する必要がありました。
新しい書き方のため、環境によってはpolyfillを用意しないといけない点に注意してください。
並列処理
const items = [1, 2, 3, 4, 5];
const process = async (item: number): Promise<number> =>
new Promise((resolve) => {
console.log(item);
setTimeout(() => resolve(item), 1000);
});
const run = async () => {
const result = await Promise.all(items.map(item => process(item)));
console.log(result);
}
void run();
並列処理は、実行順序を気にせずに全ての要素を一度に処理する場合に使います。
Promise.all
はPromise
の配列を受取り、全てのPromiseが完了するまで待機する関数となっています。
引数はArrayLike<Promise<T>>
を取るため、あらかじめ実行するPromiseの配列を作成して渡してあげる必要があります。
await
しないで呼び出した非同期関数はPromise
を返すため、map
のコールバックに渡してあげることでPromiseの配列を作成できます。
並列処理かつ、失敗しても処理を続行したい場合
const items = [1, 2, 3, 4, 5];
const process = async (item: number) => {
await new Promise(resolve => setTimeout(resolve, 1000));
if (item === 3) {
throw new Error("error");
}
return item;
};
const run = async () => {
const result = await Promise.allSettled(items.map(item => process(item)));
result.forEach((promise, index) => {
if (promise.status === "fulfilled") {
console.log(index, promise.value);
} else {
console.error(index, promise.reason);
}
});
}
void run();
TypeScript Playground
Promise.allSettled
はPromise.all
と似ていますが、失敗してもErrorをthrowすることなく処理を続行します。
戻り値は{ status: "fulfilled", value: T } | { status: "rejected", reason: any }
の配列で、Result型になっています。
戻り値のstatus
プロパティで成功か失敗かを判定でき、それぞれvalue
で値、reason
でエラーを取得できます。
まとめ
JavaScript, TypeScriptにはループを実現する方法がいくつもあり、ユースケースによって最適なものが異なります。
意外にも最近追加されたばかりの関数もあり、知らなかったという方もいるのではないでしょうか。
ぜひこの記事を参考にして、最適なループの書き方を見つけてください!
Discussion
「indexを取りたい場合」が勉強になりました。Object や Map を使うときに
.entries()
をよく使いますが、配列に使う発想はありませんでした。i < items.length
といった終了条件の古風な?for文の書き方で頑張っていましたが、今後は.entries()
も覚えて使い分けます!励みになるコメントありがとうございます。.entries()と.keys()はこの記事で一番知ってもらいたかったところだったので、知ってもらえて良かったです!
私もObjectのイメージが強く、Arrayにも生えていて同じように使えるというのを知ったのは最近でした。