JavaScript の map は「全部を変換する」、filter は「条件に合うものだけ残す」、reduce は「全部を1つの値にまとめる」ための配列メソッドです。for 文で書いていた処理の多くは、この3つと関連メソッドで短く、意図が伝わる形に書き換えられます。

この記事では、for 文からの書き換え例を17組用意し、find・some・Object.groupBy・toSorted などの関連メソッド、reduce の初期値を省略したときの TypeError、map の return 忘れ、forEach に await を書いても待たない問題、性能の考え方までをまとめました。掲載しているコードはすべて Node.js 22.22.2 で実行し、実際の出力を載せています。

map・filter・reduce と関連メソッドの早見表

やりたいこと メソッド 戻り値 例
全要素を変換する map 同じ長さの新しい配列 nums.map((n) => n * 2)
条件に合うものだけ残す filter 短くなりうる新しい配列 nums.filter((n) => n > 0)
1つの値にまとめる reduce 任意の値 nums.reduce((s, n) => s + n, 0)
最初の1件を探す find / findIndex 要素(なければ undefined)/ 位置(なければ -1) users.find((u) => u.id === 3)
後ろから探す findLast / findLastIndex 同上 logs.findLast((l) => l.error)
1つでも当てはまるか some true / false users.some((u) => !u.active)
すべて当てはまるか every true / false users.every((u) => u.age >= 20)
値を含むか includes true / false ["a", "b"].includes("a")
変換して平らにする flatMap 新しい配列 orders.flatMap((o) => o.items)
グループに分ける Object.groupBy / Map.groupBy オブジェクト / Map Object.groupBy(users, (u) => u.dept)
元を変えずに並べ替え toSorted 新しい配列 nums.toSorted((a, b) => a - b)
各要素で処理するだけ forEach undefined list.forEach((x) => console.log(x))

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript map filter reduce 使い方|17例と落とし穴」

まずは基本の形

3つとも「配列の要素を1つずつ受け取る関数(コールバック)」を渡して使います。コールバックには (要素, インデックス, 配列全体) の3つが渡されます。

const nums = [3, 8, 1, 6, 5];

nums.map((n) => n * 2);             // [6, 16, 2, 12, 10]
nums.filter((n) => n % 2 === 0);    // [8, 6]
nums.reduce((sum, n) => sum + n, 0); // 23
map filter reduce
コールバックが返すもの 変換後の値 残すなら true 次の「累積値」
結果の長さ 元と同じ 元以下 1つの値
元の配列 変わらない 変わらない 変わらない
第2引数 this の指定(ほぼ使わない) 同左 初期値(ほぼ必須)

reduce のコールバックは (累積値, 要素) を受け取り、次の累積値を返します。[3, 8, 1, 6, 5].reduce((sum, n) => sum + n, 0) は、0+3=3 → 3+8=11 → 11+1=12 → 12+6=18 → 18+5=23 と進みます。

for文からの書き換え17例

以下のデータを使います。

const nums = [3, 8, 1, 6, 5];
const users = [
  { id: 1, name: "佐藤", age: 28, dept: "開発", active: true },
  { id: 2, name: "鈴木", age: 35, dept: "営業", active: false },
  { id: 3, name: "高橋", age: 42, dept: "開発", active: true },
  { id: 4, name: "田中", age: 23, dept: "総務", active: true },
];

1. 全要素を2倍にする(map)

// for文
const r = [];
for (let i = 0; i < nums.length; i++) r.push(nums[i] * 2);

// map
nums.map((n) => n * 2);
// 実行結果:[6,16,2,12,10](どちらも同じ)

2. 偶数だけ取り出す(filter)

// for文
const r = [];
for (const n of nums) if (n % 2 === 0) r.push(n);

// filter
nums.filter((n) => n % 2 === 0);
// 実行結果:[8,6]

3. 合計する(reduce)

// for文
let s = 0;
for (const n of nums) s += n;

// reduce
nums.reduce((sum, n) => sum + n, 0);
// 実行結果:23

4. オブジェクトの配列から名前だけ取り出す(map)

users.map((u) => u.name);
// 実行結果:["佐藤","鈴木","高橋","田中"]

5. 条件に合う最初の1件(find)

// for文(見つけたら break)
let found;
for (const u of users) {
  if (u.age >= 40) { found = u; break; }
}

// find(見つかった時点で止まる)
users.find((u) => u.age >= 40)?.name;   // "高橋"
users.find((u) => u.age >= 60);         // undefined

filter(...)[0] でも同じ結果になりますが、find は見つかった時点で探すのをやめるので、意図も伝わりやすく無駄もありません。

6. 1つでも当てはまるか(some)

users.some((u) => !u.active);   // true(鈴木さんが非アクティブ)

7. すべて当てはまるか(every)

users.every((u) => u.age >= 20);   // true

some は true が1つ見つかった時点、every は false が1つ見つかった時点で止まります。なお空の配列では some は false、every は true を返します。

8. 値を含むか(includes)

["開発", "営業"].includes("総務");   // false

9. 最大値(reduce / Math.max)

nums.reduce((max, n) => (n > max ? n : max), -Infinity);   // 8
Math.max(...nums);                                          // 8

数値の配列なら Math.max(...nums) が簡単です。ただし要素が非常に多い(数十万件など)と引数の上限に達することがあるため、その場合は reduce を使います。

10. 種類ごとに件数を数える(reduce)

// for文
const c = {};
for (const u of users) c[u.dept] = (c[u.dept] ?? 0) + 1;

// reduce
users.reduce((acc, u) => {
  acc[u.dept] = (acc[u.dept] ?? 0) + 1;
  return acc;
}, {});
// 実行結果:{"開発":2,"営業":1,"総務":1}

11. グループに分ける(Object.groupBy)

const g = Object.groupBy(users, (u) => u.dept);
// 名前だけにして表示すると:
// {"開発":["佐藤","高橋"],"営業":["鈴木"],"総務":["田中"]}

以前は reduce で書いていたグループ化が、ES2024 の Object.groupBy で1行になりました。

12. 入れ子の配列を平らにしながら変換(flatMap)

const orders = [
  { id: "A", items: ["ペン", "ノート"] },
  { id: "B", items: ["消しゴム"] },
];

// for文(二重ループ)
const r = [];
for (const o of orders) for (const it of o.items) r.push(`${o.id}:${it}`);

// flatMap
orders.flatMap((o) => o.items.map((it) => `${o.id}:${it}`));
// 実行結果:["A:ペン","A:ノート","B:消しゴム"]

13. 配列からオブジェクト(辞書)を作る(Object.fromEntries)

Object.fromEntries(users.map((u) => [u.id, u.name]));
// 実行結果:{"1":"佐藤","2":"鈴木","3":"高橋","4":"田中"}

14. 重複を取り除く(Set)

[...new Set(users.map((u) => u.dept))];
// 実行結果:["開発","営業","総務"]

15. 絞り込んでから変換する(filter → map)

const products = [
  { name: "りんご", price: 120, stock: 5 },
  { name: "みかん", price: 80, stock: 0 },
  { name: "ぶどう", price: 450, stock: 2 },
];

// for文
const r = [];
for (const p of products) {
  if (p.stock > 0) r.push(`${p.name} ${Math.floor(p.price * 1.1)}円`);
}

// filter → map
products
  .filter((p) => p.stock > 0)
  .map((p) => `${p.name} ${Math.floor(p.price * 1.1)}円`);
// 実行結果:["りんご 132円","ぶどう 495円"]

「何を残すか」と「どう変換するか」が別の行に分かれるので、あとから条件だけ、表示だけを変えるのが楽になります。

16. 位置を探す・後ろから探す(findIndex / findLast)

[nums.findIndex((n) => n > 5), nums.findLast((n) => n % 2 === 1), nums.findLastIndex((n) => n > 100)];
// 実行結果:[1,5,-1]

17. 金額 × 数量の合計(reduce)

products.reduce((sum, p) => sum + p.price * p.stock, 0);
// 実行結果:1500(120×5 + 80×0 + 450×2)

書き換えの判断表

for 文の中身 置き換え先
push で新しい配列を作る(全件) map
if で選んで push filter
if と変換の両方 filter → map(または flatMap)
見つけたら break find / findIndex / some
1つの変数に足し込む reduce
オブジェクトに振り分ける Object.groupBy / reduce
二重ループで平らな配列を作る flatMap
途中で continue / break が複雑に絡む for 文のままのほうが読みやすいことも多い

reduce の初期値を省略したときの落とし穴

reduce の第2引数(初期値)を省略すると、配列の最初の要素が初期値になり、2番目の要素から処理が始まります。これが次の3つの問題を起こします。

[1, 2, 3].reduce((a, b) => a + b);                               // 6(数値なら動く)
[].reduce((a, b) => a + b);                                      // ?
[{ price: 100 }, { price: 200 }].reduce((sum, p) => sum + p.price); // ?
[{ price: 100 }].reduce((sum, p) => sum + p.price);                // ?

実際の出力です。

6
TypeError: Reduce of empty array with no initial value
[object Object]200
{ price: 100 }
ケース 何が起きたか 初期値 0 を付けると
空の配列 TypeError で停止 0
オブジェクトの配列 最初の要素(オブジェクト)に数値を足して "[object Object]200" 300
要素が1つ コールバックが呼ばれず、オブジェクトがそのまま返る 100

検索結果が0件のときだけ本番で落ちる、というのが典型的な形です。reduce には、いつも初期値を書くと覚えておくのが安全です。

map で return を忘れる

[1, 2, 3].map((n) => { n * 2; });          // ?
[1, 2, 3].map((n) => { return n * 2; });   // ?
[1, 2].map((n) => { id: n });              // ?
[1, 2].map((n) => ({ id: n }));            // ?
[ undefined, undefined, undefined ]
[ 2, 4, 6 ]
[ undefined, undefined ]
[ { id: 1 }, { id: 2 } ]

アロー関数で { を書くと「関数の本体」になり、return を書かない限り undefined を返します。3つ目は特に気づきにくく、{ id: n } はオブジェクトではなく「id: というラベル付きの文」として解釈されます。オブジェクトを返すときは ({ ... }) と丸かっこで囲みます。

map(parseInt) の罠

["1", "2", "3"].map(parseInt);   // [ 1, NaN, NaN ]
["1", "2", "3"].map(Number);     // [ 1, 2, 3 ]

map はコールバックに (要素, インデックス, 配列) を渡すため、parseInt("2", 1) のようにインデックスが「基数」として渡されてしまいます。関数をそのまま渡すときは、その関数の引数の意味を確認するか、(s) => parseInt(s, 10) と書きます。

forEach と map の違い

[1, 2, 3].forEach((n) => n * 2);   // undefined
forEach map
戻り値 undefined 新しい配列
目的 画面出力・送信など副作用のため 変換のため
つなげて書く できない .filter() などにつなげられる
途中で止める できない(break 不可) できない
await を書いたとき 待たない(後述) Promise の配列になる

戻り値を使うなら map、使わないなら forEach(または for...of)です。map の結果を捨てて forEach 代わりに使うと、読む人が「変換結果をどこで使うのか」と迷うことになります。途中で止めたい処理は、for...of と break、または some / find を使います。

非破壊と破壊的メソッド:sort と toSorted

const a = [10, 1, 2, 25];
const b = a.sort();
// a: [ 1, 10, 2, 25 ]  b: [ 1, 10, 2, 25 ]  a === b: true

sort は元の配列を並べ替えて(破壊的)、しかも既定では文字列として比較します。そのため 10 が 2 より前に来ています。ES2023 の toSorted なら、元の配列はそのままです。

const a = [10, 1, 2, 25];
const b = a.toSorted((x, y) => x - y);
// a: [ 10, 1, 2, 25 ]  b: [ 1, 2, 10, 25 ]
破壊的(元を変える) 非破壊(新しい配列を返す) 補足
sort() toSorted() 数値は (a, b) => a - b を渡す
reverse() toReversed()
splice() toSpliced()
arr[i] = x with(i, x)
push / pop / shift / unshift / fill [...arr, x] / slice など

toReversed・with・toSpliced の実行結果です(元の [1, 2, 3] は変わっていません)。

{ rev: [ 3, 2, 1 ], w: [ 1, 99, 3 ], sp: [ 2, 3 ], a: [ 1, 2, 3 ] }

React の状態(state)など、元の配列を変えてはいけない場面では、非破壊のメソッドを選ぶと安全です。

map でも「中身」は書き換わる

const a = [{ n: 1 }];
const b = a.map((o) => { o.n = 99; return o; });
a[0].n;   // 99

map が作るのは新しい「配列」ですが、中のオブジェクトは同じものです。元を変えたくない場合は a.map((o) => ({ ...o, n: 99 })) のように新しいオブジェクトを返します。

知っておくと便利な関連メソッドと挙動

実行結果つきでまとめます。

コード 実行結果 ポイント
[0, 1, "", "a", null, 2, undefined, NaN].filter(Boolean) [ 1, 'a', 2 ] 空の値を除く。0 も消えるので数値には注意
new Array(3).map(() => 0) [ <3 empty items> ] 空きスロットは map が飛ばす
Array.from({ length: 3 }, (_, i) => i * 10) [ 0, 10, 20 ] 連番の配列はこちらで作る
[NaN].includes(NaN) / [NaN].indexOf(NaN) true / -1 includes は NaN も見つけられる
[[1, [2]], [3]].flat(Infinity) [ 1, 2, 3 ] 何段でも平らにする
Map.groupBy([1, 2, 3], (n) => n % 2 ? "odd" : "even") Map(2) { 'odd' => [ 1, 3 ], 'even' => [ 2 ] } キーを文字列以外にしたいとき
Object.getPrototypeOf(Object.groupBy(...)) null プロトタイプを持たないオブジェクト。hasOwnProperty などは使えないので Object.hasOwn を使う

非同期処理:map + Promise.all

配列メソッドに async 関数を渡すときは、forEach と filter に注意が必要です。1件100ミリ秒かかる fetchUser(id) を5件分呼んだ、実際の結果です。

const ids = [1, 2, 3, 4, 5];

// 1. forEach + await(待たない)
const r = [];
ids.forEach(async (id) => { r.push(await fetchUser(id)); });
console.log(r.length);           // 0件(直後)→ 200ミリ秒後に見ると5件

// 2. map だけ
ids.map(async (id) => fetchUser(id));   // [Promise { <pending> }, ...]

// 3. map + Promise.all(同時に実行)
await Promise.all(ids.map((id) => fetchUser(id)));   // 101ms

// 4. for...of + await(1件ずつ順番に)
for (const id of ids) r.push(await fetchUser(id));  // 502ms
1 forEach: 0 件 0ms
  200ms後: 5 件
2 mapだけ: Promise { <pending> }
3 Promise.all: user1,user2,user3,user4,user5 101ms
4 for...of: 5 件 502ms
書き方 実行時間(5件×100ms) 使う場面
forEach + await 待たずに先へ進む 使わない
map + Promise.all 約100ms(同時) 互いに独立した処理
for...of + await 約500ms(順番) 順序が大事、または相手に負荷をかけたくない
2件ずつ Promise.all 301ms 同時実行数を制限したい
Promise.allSettled ― 一部が失敗しても全件の結果がほしい

filter に async を渡すと、全部残る

ids.filter(async (id) => await isActive(id));
// 実行結果:[ 1, 2, 3, 4, 5 ]  ← 絞り込まれていない

async 関数は必ず Promise を返し、Promise は true として扱われるため、すべての要素が残ります。先に判定結果をそろえてから filter します。

const flags = await Promise.all(ids.map((id) => isActive(id)));
ids.filter((_, i) => flags[i]);
// 実行結果:[ 2, 4 ]

同時実行数を制限する例と、失敗を含めて受け取る例です。

// 2件ずつ実行(301ms)
const r = [];
for (let i = 0; i < ids.length; i += 2) {
  r.push(...(await Promise.all(ids.slice(i, i + 2).map(fetchUser))));
}

// 失敗があっても全件の結果を受け取る
const results = await Promise.allSettled([1, 2].map(async (id) => {
  if (id === 2) throw new Error("404");
  return id;
}));
results.map((x) => (x.status === "fulfilled" ? x.value : x.reason.message));
// 実行結果:[1,"404"]

性能の考え方

「偶数だけを2倍して合計する」処理を、100万件と1,000件の配列で比べました(Node.js 22.22.2、15回の中央値。環境によって数値は変わります)。

書き方 100万件 1,000件
for 文(インデックス) 3.04 ms 0.00 ms
for...of 11.25 ms 0.00 ms
forEach 10.95 ms 0.02 ms
reduce 1回で済ませる 10.76 ms 0.02 ms
filter → map → reduce 28.84 ms 0.03 ms
反復子ヘルパー(values().filter().map()) 39.35 ms 0.04 ms
  • for 文はたしかに最速ですが、1,000件では差が 0.03 ms 程度で、画面の操作や通信の時間に比べればごくわずかです
  • メソッドをつなぐと、途中の配列がそのつど作られるぶん遅くなります。100万件規模で何度も実行する処理なら、reduce 1回や for 文にまとめる価値があります
  • 迷ったら、まず読みやすく書き、計測して遅い場所だけ直すのが効率的です

性能より大きな差が出る書き方

reduce の中で毎回スプレッド構文(...)でオブジェクトをコピーすると、件数の2乗に比例して遅くなります。1万件で比べた実際の結果です。

// 毎回コピー
items.reduce((acc, x) => ({ ...acc, [x.id]: x.v }), {});
// acc に追加して返す
items.reduce((acc, x) => { acc[x.id] = x.v; return acc; }, {});
// Object.fromEntries
Object.fromEntries(items.map((x) => [x.id, x.v]));
スプレッドで毎回コピー: 18189 ms
accに追加して返す      : 1.42 ms
Object.fromEntries     : 6.25 ms

約18秒と約1.4ミリ秒で、1万倍以上の差になりました。map か for 文かの違いよりも、こうした書き方の違いのほうが、性能への影響ははるかに大きくなります。

補足:反復子ヘルパー(ES2025)

Node.js 22 では、配列を作らずに順に処理する反復子ヘルパーも使えます。take で必要な件数だけ取り出せるのが特徴です。

[1, 2, 3, 4, 5, 6].values().filter((n) => n % 2 === 0).map((n) => n * 10).take(2).toArray();
// 実行結果:[ 20, 40 ]

よくある質問

Q. map・filter・reduce と for 文、どちらを使えばいいですか? A. 配列を変換・絞り込み・集計するなら、まず map・filter・reduce で書くのがおすすめです。何をしているかがメソッド名で伝わります。途中で break したい処理や、複数の条件が複雑に絡む処理は、for 文のほうが読みやすいこともあります。

Q. reduce が読みにくいと言われました。 A. 合計以外の reduce は、専用のメソッドで置き換えられないかを先に考えます。グループ化は Object.groupBy、辞書づくりは Object.fromEntries、平らにするのは flatMap / flat です。それでも reduce を使う場合は、累積値の変数名を acc ではなく countByDept のように中身が分かる名前にすると伝わりやすくなります。

Q. toSorted や Object.groupBy は、どの環境で使えますか? A. toSorted は ES2023、Object.groupBy は ES2024 の機能で、この記事では Node.js 22 で動作を確認しています。現行の主要ブラウザでも使えますが、古いブラウザへの対応が必要な場合は、対象環境の対応状況を MDN などで確認してください。

Q. オブジェクトにも map は使えますか? A. オブジェクトには map はありません。Object.entries で配列にしてから map し、Object.fromEntries で戻します。例:Object.fromEntries(Object.entries(prices).map(([k, v]) => [k, v * 1.1]))

まとめ

  • map は変換、filter は絞り込み、reduce は集計。どれも元の配列は変えない
  • 探すなら find、有無なら some / every / includes、グループ化は Object.groupBy、平らにするなら flatMap
  • reduce には初期値を必ず書く。省略すると空配列で TypeError、オブジェクトでは "[object Object]200" になる
  • アロー関数で { を書いたら return が必要。オブジェクトを返すなら ({ ... })
  • 戻り値を使わない処理は forEach、ただし await は待たない。非同期は map + Promise.all か for...of
  • sort は元を変え、文字列として並べる。元を残すなら toSorted((a, b) => a - b)
  • 性能は読みやすさ優先で書いて計測。reduce 内のスプレッドのコピーのような書き方に注意する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript map filter reduce 使い方|17例と落とし穴」

株式会社RJCでは、日々のコードの書き方も、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?