- JavaScript
- 技術 💻
JavaScript map filter reduce 使い方|17例と落とし穴
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万件規模で何度も実行する処理なら、
reduce1回や 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期連続の黒字決算。