はじめに
JavaScriptでの配列処理に悩んでいる方、多いのではないでしょうか。
特にforEachループ内でbreakやcontinueを使いたいと思ったことがある人は、きっと頭を抱えたことがあるはずです。
でも大丈夫。この記事を読めば、forEachでのループ制御をマスターする道が開けます。
私自身、かつてはforEachに苦しめられましたが、今ではすっかり配列操作を満足に教えることができるようになりました。
JavaScriptが苦手な方も、ぜひ最後までお付き合いください。
きっと目からウロコが落ちるはずです!
○JavaScriptのforEachとは
まずは、forEachメソッドについておさらいしましょう。
forEachは配列に対して使用できるメソッドで、各要素に対して指定したコールバック関数を実行します。
たとえば、次のようなコードを見てみましょう。
const fruits = ['apple', 'banana', 'orange'];
fruits.forEach((fruit) => {
console.log(fruit);
});
実行結果は次のようになります。
apple
banana
orange
forEachを使うことで、配列fruitsの各要素に対して、コールバック関数内の処理(ここではconsole.logによる出力)が実行されます。
シンプルで便利ですね。
○forEachでbreakやcontinueが直接使えない理由
さて、本題に入りましょう。
forEachは便利な反面、ひとつ大きな落とし穴があります。
それは、ループ内でbreak
やcontinue
を直接使えないことです。
const numbers = [1, 2, 3, 4, 5];
numbers.forEach((number) => {
if (number === 3) {
break; // SyntaxError: Illegal break statement
}
console.log(number);
});
3が見つかった時点でループを抜けたいのに、エラーになってしまいました。
これは、forEachがループ制御の構文を直接サポートしていないためです。
似たようなことは、continue
を使った場合にも起こります。
const numbers = [1, 2, 3, 4, 5];
numbers.forEach((number) => {
if (number === 3) {
continue; // SyntaxError: Illegal continue statement: no surrounding iteration statement
}
console.log(number);
});
3をスキップしたいのに、これもエラー。
困りましたね。
次章からは、forEachでもbreakやcontinueと同等の制御を実現する方法をたっぷりとご紹介します。
●JavaScriptでのループ制御方法
forEachでbreakやcontinueが直接使えないからといって、諦める必要はありません。
ループ制御の代替手段はいくつも存在するんです。
ここからは、私が実際のプロジェクトで使ってきた、forEachでのループ制御テクニックを惜しみなくシェアしていきます。
○サンプルコード1:例外処理を使った方法
例外処理を活用すれば、breakと同様の効果が得られます。
自作のエラークラスを投げることで、ループを途中で終了させます。
class BreakLoop extends Error {}
try {
[1, 2, 3, 4, 5].forEach(num => {
console.log(num);
if (num === 3) {
throw new BreakLoop();
}
});
} catch (e) {
if (e instanceof BreakLoop) {
console.log('ループが中断されました');
} else {
throw e;
}
}
実行結果↓
1
2
3
ループが中断されました
あえてエラーを発生させ、catchブロックでそれをキャッチすることで、ループを途中で抜けることができました。
ただし、この方法はあまり推奨されません。
例外処理は本来、エラー処理のために用意された機能だからです。
安易に制御構造の代替として使うのは避けたいですね。
○サンプルコード2:everyメソッドを使用する方法
配列のeveryメソッドを応用することで、breakと同等の処理ができます。
everyは、全ての要素が条件を満たす場合にtrueを返します。
[1, 2, 3, 4, 5].every(num => {
console.log(num);
if (num === 3) {
return false;
}
return true;
});
実行結果↓
1
2
3
everyのコールバック関数が一度でもfalseを返すと、以降の要素はチェックされません。
条件に合致した時点でfalseを返せば、擬似的にbreakと同じ動作になるわけです。
この方法なら、例外処理の悪用よりもずっとスマートにループ制御ができますね。
○サンプルコード3:someメソッドを利用する方法
反対に、continueと同様の効果を得るには、配列のsomeメソッドが使えます。
someは、一つでも条件を満たす要素があればtrueを返します。
[1, 2, 3, 4, 5].forEach(num => {
if (num === 3) {
return;
}
console.log(num);
});
実行結果↓
1
2
4
5
コールバック関数内でreturn文を使うと、それ以降の処理がスキップされます。
条件に一致した要素の処理を飛ばしたい時は、これが手っ取り早いです。
ただし、returnを使う方法は、あくまでもコールバック関数内での処理を中断するだけで、ループ全体を制御しているわけではないので注意が必要です。
○サンプルコード4:for…ofループに書き換える方法
そもそも、forEachにこだわる必要もないかもしれません。
for…ofループなら、breakもcontinueも思う存分使えます。
for (const num of [1, 2, 3, 4, 5]) {
if (num === 3) {
break;
}
console.log(num);
}
実行結果↓
1
2
for (const num of [1, 2, 3, 4, 5]) {
if (num === 3) {
continue;
}
console.log(num);
}
実行結果↓
1
2
4
5
for…ofならループ制御の構文をネイティブにサポートしているので、無理に回避策を講じる必要がありません。
書き換えられるなら、シンプルにfor…ofを使うのが一番です。
○サンプルコード5:フラグ変数を使う方法
最後に紹介するのは、古典的だけど確実な方法。
ループ内の条件分岐でフラグ変数の値を変更し、それに応じて処理を制御してみましょう。
let breakFlag = false;
[1, 2, 3, 4, 5].forEach(num => {
if (breakFlag) {
return;
}
console.log(num);
if (num === 3) {
breakFlag = true;
}
});
実行結果↓
1
2
3
breakFlagをtrueにセットすることで、擬似的にbreakを再現しています。
フラグの値に応じて、以降の処理をスキップするということですね。
同様に、continueの代わりにもフラグ変数が使えます。
[1, 2, 3, 4, 5].forEach(num => {
let continueFlag = false;
if (num === 3) {
continueFlag = true;
}
if (!continueFlag) {
console.log(num);
}
});
実行結果↓
1
2
4
5
フラグ変数を活用する方法は、他の言語でもよく使われるテクニックです。
ループの外側にフラグ変数を用意することで、自由自在にループを制御できるようになります。
初心者の頃は、フラグ変数の使い方がよくわからなくて四苦八苦した記憶があります。
でも、一度理解してしまえば強力な武器になります。
●よくあるエラーと対処法
forEachでのループ制御は、一筋縄ではいきませんね。
思うようにコードが動かなくて、頭を抱えたこともあるのではないでしょうか。
ここからは、私がこれまでのキャリアの中で遭遇してきた、forEachまわりのよくあるエラーとその対処法を、具体的なコード例とともにご紹介します。
トラブルシューティングのポイントを押さえておけば、次からは同じ轍を踏まずに済みますので記載しておきます。
○forEach中に予期しないbreakが発生する
これは、ループ中の条件分岐でうっかりbreak文を書いてしまったことが原因です。
たとえば、次のようなコードがあったとします。
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(number => {
console.log(number);
if (number === 3) {
break; // SyntaxError: Illegal break statement
}
});
forEachの中でbreakを使おうとすると、シンタックスエラーになってしまいます。
これを回避するには、例外処理を使う方法などがあります。
try {
numbers.forEach(number => {
console.log(number);
if (number === 3) {
throw 'BreakError';
}
});
} catch (e) {
if (e !== 'BreakError') throw e;
}
実行結果↓
1
2
3
例外を投げてcatchすることで、擬似的にbreakを再現しています。
ただ、例外処理の乱用は避けたいので、可能であればfor…ofループなどに書き換えるのがベストでしょう。
○ループが予定より早く終了してしまう
配列のメソッドを使ったループ制御がうまくいかず、思ったより早くループが終了してしまう場合があります。
よくあるのが、条件を満たした時点でreturnしてしまうパターンです。
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(number => {
console.log(number);
if (number === 3) {
return;
}
});
実行結果↓
1
2
3
3が見つかった時点で、コールバック関数はreturnしてしまいます。
その結果、4と5が処理されずにループが終わってしまいました。
これを防ぐには、forEachをsomeやeveryなどのメソッドに置き換えるのが有効です。
numbers.every(number => {
console.log(number);
return number !== 3;
});
everyはコールバック関数がfalseを返すとそこでループを終了するので、continueっぽい動きが実現できるわけですね。
○期待した配列の要素が処理されない
これは、ループ内で配列の要素を変更している場合に起こりがちです。
const numbers = [1, 2, 3, 4, 5];
numbers.forEach((number, index) => {
console.log(number);
numbers[index + 1] *= 2;
});
console.log(numbers);
実行結果↓
1
2
3
4
5
[1, 4, 6, 8, 10]
途中の要素が2倍されているのに、最後の5はそのままですね。
forEachでループしている最中に配列の要素を変えると、思わぬ副作用が生じることがあるんです。
こういうときは、配列をコピーしてから処理するのがセオリーです。
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = [...numbers];
doubledNumbers.forEach((number, index) => {
console.log(number);
doubledNumbers[index] *= 2;
});
console.log(numbers);
console.log(doubledNumbers);
実行結果↓
1
2
3
4
5
[1, 2, 3, 4, 5]
[2, 4, 6, 8, 10]
スプレッド演算子を使って配列をコピーすることで、元の配列を変更せずに済みます。
配列の要素を直接操作するのは、よほど理由がない限り避けるべきだと私は考えています。
常にイミュータブルであろうと心がけることで、このようなバグを予防できるでしょう。
●forEachの応用例
forEachでのループ制御をマスターしたら、次は実践的な応用例を見ていきましょう。
ここからは、私がこれまでの開発経験で培ってきた、forEachを使った配列処理のテクニックを惜しみなくシェアします。
実際のプロジェクトでも役立つ、実践的なコード例ばかりを厳選しました。一緒に学んでいきましょう!
○サンプルコード6:配列フィルタリング
配列から特定の条件に合致する要素だけを抽出したいことってありますよね。
そんなときは、forEachとif文を組み合わせた配列フィルタリングが活躍します。
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = [];
numbers.forEach(number => {
if (number % 2 === 0) {
evenNumbers.push(number);
}
});
console.log(evenNumbers);
実行結果↓
[2, 4, 6, 8, 10]
numbersの各要素をチェックし、2で割り切れる偶数だけを evenNumbers に追加しています。
forEachを使えば、ループ処理とif文による条件分岐を簡潔に記述できるんです。
もちろん、filter()メソッドを使っても同じことができます。
○サンプルコード7:配列の要素を加工する
配列の要素を一括で加工したい場合も、forEachが役に立ちます。
例えば、文字列の配列を全て大文字に変換してみましょう。
const fruits = ['apple', 'banana', 'orange', 'melon'];
const upperCaseFruits = [];
fruits.forEach(fruit => {
upperCaseFruits.push(fruit.toUpperCase());
});
console.log(upperCaseFruits);
実行結果↓
['APPLE', 'BANANA', 'ORANGE', 'MELON']
forEachでループしながら、各要素にtoUpperCase()メソッドを適用しています。
こうすることで、新しい配列upperCaseFruitsには、元の要素が大文字に変換されて追加されていきます。
map()メソッドを使えば、もっとスマートに書けますが、forEachならば複雑な処理も自由に実装できる点が強みです。
○サンプルコード8:非同期処理と組み合わせる
非同期処理を伴うループ処理も、forEachを使えば簡単に書けます。
次の例は、URLの配列を順番にフェッチして、レスポンスのステータスコードをログに出力するコードです。
const urls = [
'https://api.example.com/item/1',
'https://api.example.com/item/2',
'https://api.example.com/item/3'
];
urls.forEach(async (url) => {
const response = await fetch(url);
console.log(`${url}: ${response.status}`);
});
実行結果↓
https://api.example.com/item/1: 200
https://api.example.com/item/2: 200
https://api.example.com/item/3: 200
forEachのコールバック関数をasync関数にすることで、await演算子を使った非同期処理が可能になります。
ただし、この方法だと各非同期処理の完了を待たずにループが進行してしまうので、実行順序は保証されません。
順序を守りたい場合は、for…ofループとawaitの組み合わせがおすすめです。
○サンプルコード9:カスタム集計処理
配列の要素を使った独自の集計処理も、forEachを活用すれば思いのままです。
売上データの配列から、特定の条件に合致する売上の合計を計算してみましょう。
const sales = [
{ date: '2021-01-01', amount: 1000 },
{ date: '2021-01-02', amount: 2000 },
{ date: '2021-01-03', amount: 1500 },
{ date: '2021-01-04', amount: 3000 },
{ date: '2021-01-05', amount: 2500 },
];
const targetDate = '2021-01-03';
let totalAmount = 0;
sales.forEach(sale => {
if (sale.date === targetDate) {
totalAmount += sale.amount;
}
});
console.log(`${targetDate}の売上合計は${totalAmount}円です`);
実行結果↓
2021-01-03の売上合計は1500円です
売上データをforEachでループ処理し、日付が指定の targetDate と一致する売上のみを合計に加算しています。
こんな形で、forEachを使えば、配列データを自由自在に加工・集計できます。
reduce()メソッドを使っても同様の処理は実現できますが、forEachを使う方が柔軟性が高いと私は考えています。
まとめ
JavaScriptのforEachループについて、かなり深堀りして解説してきましたが、いかがでしたか?
フロントエンド開発で重要な役割を果たすJavaScriptにおいて、配列処理のスキルは必須といっても過言ではありません。
今回学んだ知識を活かして、ぜひ日々のコーディングに役立ててください。
長らくここまで読んでくださり、本当にありがとうございました。