【JavaScript】forEach・map・filter の違いと使い方|配列処理の最適解を徹底解説
※この記事は JavaScript の基本文法を理解している中級者以上の読者を想定しています。
JavaScript 開発では配列処理が欠かせません。ES5 以降に導入された高階関数 forEach
・map
・filter
を正しく使い分ければ、コードは読みやすくテストしやすくなり、パフォーマンス面でも恩恵を受けられます。
ここではそれぞれのシグネチャと戻り値、メモリ消費の傾向、代表的ユースケースを比較しながら「どの状況でどの関数を選ぶべきか」を具体例付きで解説します。
forEach の特徴:副作用中心の逐次処理に向いた最小オーバーヘッド関数
forEach
は「配列の各要素に対してコールバックを呼び出す」だけで、戻り値は undefined
です。返り値がないぶんメモリ割り当てが少なく、ロギングや DOM 操作など副作用を目的とした処理に適しています。
その一方、break
や return
でループを抜けられないため、途中終了が必須のアルゴリズムには不向きです。
const arr = [1, 2, 3];
arr.forEach((v, i) => {
console.log(`${i} 番目は ${v}`);
});
// 戻り値は undefined
ベンチマークを例示すると、MacBook Pro (M1, 2020)/Node.js 20.11/100 万要素・単純加算という条件では約 8 ms 前後で完了しました。もっと複雑な処理や x86 CPU、旧ブラウザでは結果が大きく変わるため「常に最速」とは断言できません。性能がボトルネックになりそうな場合は、for...of
ループや Web Workers との比較検証が必須です。
map の特徴:イミュータブルなデータ変換を担う純粋関数
map
は各要素に変換を適用し、同じ長さの新配列を返します。元配列は不変のまま残るため、React+Redux や Vue 3+Pinia など状態管理ライブラリと高い親和性があります。
コピーコストは発生しますが、その対価として可読性とバグ抑止のメリットが得られます。
const prices = [1200, 2400, 3600];
const withTax = prices.map(p => Math.round(p * 1.1));
// withTax は [1320, 2640, 3960]
同条件ベンチマークでは 10 ms 前後と forEach
と大差ありません。ただし、コールバックが重い計算や外部 API 呼び出しを含む場合、map
が遅く感じるケースもあります。
データサイズや内容が大きく変動するアプリでは、プロファイラを使って頻繁に確認するのが安全策です。
filter の特徴:要素選別に特化した述語関数 ― null・undefined 対応も忘れずに
filter
は述語(true/false を返す関数)を各要素に適用し、真となった要素だけを含む短い新配列を返します。配列の長さは保持されないため、結果が空配列になる場面を考慮した例外処理が重要です。
また、プロパティが存在しない要素に対しては安全にアクセスしないとランタイムエラーを招きます。
const users = [
{ name: 'Alice', admin: true },
{ name: 'Bob' }, // admin プロパティなし
{ name: 'Carol', admin: false }
];
const admins = users.filter(u => Boolean(u?.admin));
// [{ name: 'Alice', admin: true }]
同環境でのテストでは、条件の合致率によって9〜12 ms とばらつきました。抽出率が低いほど GC の負担が減る一方、述語の計算コストが高いと遅延が顕著になります。
ユースケースに応じて find
(最初の要素のみ取得)や every
/some
(真偽チェック)を検討すると、さらに高速化できる場合があります。
map と filter の連携例:条件抽出から変換まで一括で処理
実務では、filter で条件に合致した要素を抽出し、その後 map で必要な情報だけを取り出す処理がよく使われます。たとえば、admin ユーザーの名前だけを抽出する場合は次のように書けます。
const users = [
{ name: 'Alice', admin: true },
{ name: 'Bob' },
{ name: 'Carol', admin: false }
];
const adminNames = users
.filter(u => Boolean(u?.admin))
.map(u => u.name);
console.log(adminNames); // ['Alice']
パフォーマンス指標は「参考値」として扱う ― 測定条件開示と再計測のすすめ
ここに記載した ms 単位の数値は、冒頭で示したハードウェア・Node.js 版・要素数・演算内容に限定した一例でしかありません。V8・SpiderMonkey・JavaScriptCore など各エンジンは年々最適化が進み、同じコードでもブラウザバージョンや JIT のウォームアップ状況、CPU キャッシュの当たり方で結果が変わります。チーム開発では次の手順を推奨します。
- ソース管理に micro‑benchmark スクリプトを同梱し、CI で定期測定
- 結果ログにハードウェアとコンパイルフラグ、OS、メモリ状態を記録
- 閾値超過時にパフォーマンス回帰テストを自動アラート
こうした運用を挟むことで「再現性のない速度比較」から脱却し、信頼できるボトルネック特定が可能になります。
非同期コードとの相性:forEach は「await で逐次化」できない点に注意
forEach
・map
・filter
はいずれも同期に各コールバックを呼び出し、その戻り値をすぐに捨てるか新配列に格納します。コールバックが async
関数なら Promise が返り、JavaScript は「生成された Promise」を次要素の処理へ渡すだけで待機しません。
// 良くない例: forEach 内の await は期待通り同期しない
const urls = ["/a", "/b"];
urls.forEach(async url => {
await fetch(url); // ここで待つが forEach 本体は次へ進む
});
逐次実行したければ for...of
+await
の組み合わせか、map
で Promise 配列を返して for…of + await
または Promise.all
で制御する方法を採ります。
逐次&並列を両立するコード例
// 並列取得
const promises = urls.map(u => fetch(u));
const results = await Promise.all(promises);
// 完全逐次
for (const u of urls) {
await fetch(u);
}
選定フローチャート:三つの質問で最適メソッドを決める
1. 副作用が目的?
YES → forEach
2. 要素数を変えたい?
YES → filter
NO → 次へ
3. 各要素を変換したい?
YES → map
NO → forEach
または for...of
もちろん、複数メソッドをチェーンして「抽出 → 変換 → 副作用」の三段パイプラインを構築するのも一般的です。可読性が下がるほどチェーンする場合は、一度配列処理を切り出してユニットテストを充実させると安心です。
まとめ:高階関数を理解して堅牢・高速な JavaScript を書こう
forEach
・map
・filter
は API シグネチャこそ似ていますが、目的・戻り値・メモリ使用量が異なります。副作用主体なら forEach
、イミュータブル志向の変換なら map
、要素の取捨選択なら filter
が第一候補です。
関数 | 主な目的 | 戻り値 | 典型的な用途 |
---|---|---|---|
forEach | 副作用のある処理 | undefined | ロギング、DOM 操作、API 呼び出し |
map | 要素の変換 | 同じ長さの新配列 | 金額計算、HTML生成、状態更新 |
filter | 要素の選別 | 短くなった新配列 | 検索、絞り込み、条件抽出 |
ただしパフォーマンスは処理内容と実行環境で容易に逆転します。ベンチマークは参考値と理解し、必ず自分のアプリケーションで再計測しましょう。
適切な選定と安全な null チェック、非同期制御の理解を組み合わせれば、読みやすくバグが少ないコードベースを構築できます。この記事が日々の実装やコードレビューの一助となれば幸いです。