はじめに
先日お風呂上りにぼけ~っとXを眺めていたところ、以下のポストを拝見しました。
このポストを読んで、質問する側・回答する側どちらも素晴らしいエンジニアさんで、自分も見習わなければと気が引き締まりました。
それはそれでいいんですが、もし自分が同じ質問をされた場合、しっかりと答えてあげられたのかと考えると、正直なところ自信はこれっぽっちもありません…。
こういう疑問に対してしっかりと言語化して回答できることこそが、基本をしっかり理解できている ということだと思いますので、いい機会だととらえて自分の回答を言語化しておこうと思います。(要はちゃんと復習しておこうということでござす)
自分なりの回答ということで誤っている点もあるかと思いますので、お気づきの際はぜひぜひご指摘いただければと思います。
結論
質問の内容は以下です。JavaScript のコーディングに対しての質問内容です。
forEachとかfilterってなんの意味があるんですか?全部forで書けますよね?
これに対する自分なりの回答は以下のとおりです。
「 for
を使ってはいけないわけではない。性能面でも僅かな差がある程度なので、最終的には読みやすさや保守性・メンテナンス性を優先したほうがいい」
とりわけチーム開発では、「何がやりたいのか?」がパッと見て伝わるコードに圧倒的な価値があります。forEach
や filter
はその観点でとても有効です。
別に for でも悪くはない
for で置き換えられる
質問者の疑問のとおり、forEach
も filter
も for
ループで置き換えできます。
以下は「配列の各要素の値を2倍にした新しい配列を作成する」場合の、for
ループを利用した例です。
const numbers = [1, 2, 3, 4];
const doubledNumbers = []; // 新しい配列用の変数をループ外で用意
for(let i = 0; i < numbers.length; i++){
doubledNumbers.push(numbers[i] * 2);
}
console.log(doubledNumbers); // [2, 4, 6, 8]
読み手が考えることが増える
上記のコード例の場合、
- ループ用の変数
i
- 結果を詰めるための配列
doubledNumbers
-
push
の処理をしていること
など、コードの読み手は意外といろいろと考えながら読む必要があります。読み手の脳内メモリを消費することになるので読み手には負担が増えてしまいます。
一方、以下は map
を使ったコード例です。
const numbers = [1, 2, 3, 4];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8]
「新しい配列を作る」、「それぞれの要素を2倍に変換する」、「元の配列は変えない」という意図が、非常にストレートに表れていると思います。最低限「map
は新しい配列を返すメソッド」ということさえ理解できていれば、コードをぱっと見ただけで動きが想像できます。
イミュータブルを意識する
「イミュータブル」とはざっくりいうと変更できないという意味です。逆に「ミュータブル」とは変更できることを意味します。JavaScriptのデータ構造は、配列やオブジェクトなど基本的にミュータブルになっています。例えば、push()を使用して中身を変更できるということがまさに変更可能ということです。
コード量が増えると「どこで書き換えているか追いかけづらい」状態になってきます。そうなると、以下のような問題が起きがちです。
- 思わぬ箇所で配列やオブジェクトの中身が変わってしまう
- 予期せぬバグにつながる
- 変更箇所を見つけるのに時間がかかる
filter
やmap
などのメソッドは、元の配列を変更せず、新しい配列を返す(イミュータブル指向の実装)ため、「どこか別の場所で書き換わってしまう可能性が低い」という特徴があります。これがバグ予防やセキュリティ対策の観点で「安全」だと言われる理由のひとつです。
forEach と filter の基本とコード例
それでは今回例として挙げられているforEach``filter
についてコード例を交えながら見ていきましょう。
forEach:単純にループして処理を行う
const numbers = [1, 2, 3, 4];
numbers.forEach((num) => {
console.log(num * 2);
});
- 「配列の各要素を1つずつ取り出して何かする」 という意図が明確
-
for
ループでは必要だったインデックスの管理(i++
など)が不要 - 戻り値はなく、あくまで「副作用としての処理」に使われる
これをfor
ループで書くと以下になります。
const numbers = [1, 2, 3, 4];
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i] * 2);
}
単純な例なので、分かりやすさもコード自体も、それほど差は感じないかもしれません。しかし、このコードだと、for
ループの中身を確認してはじめて、「すべての要素に対して何か処理をしていること」が分かります。forEach
を利用すれば、その時点で読み手に「配列の各要素を1つずつ取り出して何か処理をする」という意図が伝わります。
filter:必要な要素だけ抜き出す
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4, 6]
console.log(numbers); // [1, 2, 3, 4, 5, 6] (変更されていない)
- 「配列のうち、条件に合う要素だけを抜き出す」 という意図がすぐ分かる
- 元の配列を変えない ため、他の処理への影響を考えずに済む
これをfor
ループで書くと以下になります。
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
evenNumbers.push(numbers[i]);
}
}
console.log(evenNumbers); // [2, 4, 6]
これでも動きは同じになりますが、先ほどの例と同様に、for
ループ内の処理を確認して初めて「元の配列から条件に合致する要素だけを新しい配列に追加する」ことが分かります。
しかし、filter
を利用すれば、その時点で「ある条件に合致した要素だけを含む新しい配列を作成すること」という意図がより直感的に伝わると思います。
セキュアコーディングとの関係
イミュータブルで予期せぬ不具合を減らす
セキュアコーディングと聞くと、脆弱性や暗号化の話を思い浮かべるかもしれませんが、それだけではなく「意図しない変更を最小限にする」という観点もセキュアなコーディングとして重要な観点です。その「意図しない変更」が原因で脆弱性が生まれることもあるからです。
- 配列を破壊的に変更すると、他の箇所への影響を常に気にしなければならない
- バグが起こったとき、「どの時点で、どの変数が変更されたのか」を追いかけるのが大変
その点、filter
やmap
は新しい配列を返すだけなので、元の配列が変化しません。意図しない変更が入り込む余地が減り、コードを読んでも「どこかでこっそりデータが書き換わってないか?」という不安が大幅に軽減されます。これは読み手にとって優しいコードと言えます。
スコープを意識しやすい
ループ用の変数や結果格納用の配列をどこで宣言しているかによって、意図しないスコープの「はみ出し」が起きるとバグが発生しやすくなります。forEach
やfilter
などはそれぞれの関数スコープの中で完結しやすく、可変変数(i や push先の配列など)を極力使わない書き方になります。
まとめ
ここまで長くなってしまいましたが、自分なりの考えをまとめてみます。
- forでも全く問題ないケースはある
ただし、インデックス管理や外部変数へのpushなど、コードがやや冗長になりがち。 - forEach / filter / map / reduceなどのメソッドは「何をしたいか」が明確
初心者にも伝わりやすく、可読性・保守性が上がりやすい。 - 元の配列やオブジェクトを破壊しない(イミュータブル指向)ことがバグを減らす
どこかで思わぬ変更が入らないため、見通しが良くなる。 - セキュアコーディングでも「不要な変更箇所は作らない」ことが重要
コードが大きくなったときの保守・バグ修正が格段にラクになる。
「for
ループとforEach
やfilter
の違いって何だっけ?」と感じたときは、以下の観点で考えれば使い分けの判断になるのではと思います。
- 可読性(何をしたいかわかりやすいか?)
- 変更箇所の管理(余計な変数や破壊的操作はないか?)
- 保守やセキュリティへの影響(バグや想定外の書き換えを起こしにくいか?)
最後、自分で書いていて何が言いたいのかよくわからなくなってきました。自分の考えを言語化するのはやはり難しいですね。うまく言語化できないということは、まだまだ自分の理解が浅い状態であるということを受け止め、基本をしっかり押さえることの大事さを肝に銘じました!ってところで今回は終わりします。
ありがとうございました!
基本が中途半端だと、すべてが中途半端に終わるんだよね。 - 小暮宙太
参考
Array.prototype.filter() - JavaScript | MDN
Array.prototype.forEach() - JavaScript | MDN
ミュータブルな型とイミュータブルな型の相違を知ろう