JavaScript:配列の高階関数(map / filter / reduce)
1学習の目的
- map・filter・reduce を使い分けて、for ループで書いていた変換・絞り込み・集計を1行で表現できるようになる。
- メソッドをつないで処理の流れを組み立てられるようになる。現代のJavaScriptで最も多用される書き方で、実務のコードはこの3つで大半ができている。
2基礎解説
| やりたいこと | 書き方 | 戻り値 |
|---|---|---|
| 全部を変換 | nums.map(n => n * 2) | 同じ数の新しい配列 |
| 条件で絞る | nums.filter(n => n > 5) | 減った新しい配列 |
| 1つにまとめる | nums.reduce((s, n) => s + n, 0) | 値ひとつ |
| 1件だけ探す | users.find(u => u.id === 3) | 要素 or undefined |
- map は要素数が変わらず、filter は減る。「変換したい」のか「絞りたい」のかで選ぶ。両方したいときは .filter(…).map(…) とつなぐ。
- { } を書いたら return が必須。map(n => { n * 2 }) は [undefined, undefined] になる。式1つなら { } を書かないほうが安全。
- reduce は「積み上げる箱(第2引数)」を必ず書く。reduce((合計, 要素) => …, 0) の 0 が初期値で、省略すると空配列でエラーになる。
- これらは元の配列を変更しない。新しい配列が返るので、JS06の sort と違ってコピーを気にしなくてよい。
- forEach は戻り値が undefined。結果が欲しいなら map、画面出力など「実行するだけ」なら forEach と使い分ける。
現場使用例:APIの応答から必要な項目だけ抜き出す、条件に合う行だけ表示、合計金額の算出、フォーム入力を一括で数値に変換。
const nums = [1, 2, 3, 4, 5, 6];
console.log(nums.map(n => n * 2)); // [2,4,6,8,10,12]
console.log(nums.filter(n => n % 2 === 0)); // [2,4,6]
console.log(nums.reduce((sum, n) => sum + n, 0)); // 21
// つないで書ける(絞ってから変換)
console.log(nums.filter(n => n > 3).map(n => n * 10)); // [40,50,60]
// オブジェクトの配列から必要な項目だけ取り出す
const users = [
{ name: "田中", score: 82 },
{ name: "鈴木", score: 45 },
];
const passed = users.filter(u => u.score >= 60).map(u => u.name);
console.log(passed); // ['田中']
// よくある間違い:{ } を書いて return を忘れる
console.log(nums.map(n => { n * 2 })); // [undefined, ...]
3基本ドリル(10問)
出力 == [ 2, 4, 6, 8 ]
nums.map(n => n * 2) と書く。
const nums = [1, 2, 3, 4]; console.log(nums.map(n => n * 2));
map の中の関数は要素1つを受け取って、変換後の値を返す。for ループ3行が1行になる。
A. 1 B. 2 C. 3 D. 6 選択★☆☆無料
解答 == C
map は変換であって絞り込みではない。
C
map は要素数が変わらない。中身が変わるだけ。減らしたいときは filter を使う。
出力 == [ 8, 10, 6 ]
nums.filter(n => n > 5) と書く。条件が true の要素だけ残る。
const nums = [3, 8, 1, 10, 6]; console.log(nums.filter(n => n > 5));
filter の中の関数はtrue / false を返す。true になった要素だけが新しい配列に入り、元の並び順は保たれる。
const nums = [10, 20, 30]; console.log(nums.____((sum, n) => sum + n, 0));
出力 == 60
「集約する」という意味の英単語。第2引数が初期値。
reduce
第1引数 sum が積み上げていく箱、第2引数 n が今回の要素。最後の 0 が箱の初期値。
A. [2, 4] B. [undefined, undefined] C. [] D. エラー 選択★☆☆無料
解答 == B
波カッコを書いた場合、値を返すには何が必要か。
B
{ } を書いたら return が必須。この書き忘れは頻出のバグで、結果が undefined だらけになったらまずここを疑う。
出力 == [ 4, 16, 36 ]
nums.filter(...).map(...) のように続けて書ける。
const nums = [1, 2, 3, 4, 5, 6]; console.log(nums.filter(n => n % 2 === 0).map(n => n ** 2));
絞ってから変換するのが基本の順番。先に map すると不要な要素まで計算することになり無駄が出る。
const scores = [82, 45, 91]; console.log(scores.map(s => ____));
出力 == [ '合格', '不合格', '合格' ]
三項演算子を使う。条件 ? 真の値 : 偽の値。
s >= 60 ? "合格" : "不合格"
要素数は減らずに中身だけが変わる。D03の filter(要素が減る)との違いを見比べてほしい。
A. filter B. find C. map D. some 選択★★☆無料
解答 == B
1件だけ欲しいときのメソッド。
B
filter は配列を返し、find は要素そのものを返す。見つからない場合 find は undefined。1件だけ欲しいのに filter を使うと [0] を付ける必要が出て読みにくい。
出力 == [ '田中', '高橋' ]
filter で絞ってから map で名前を取り出す。
const users = [
{ name: "田中", score: 82 },
{ name: "鈴木", score: 45 },
{ name: "高橋", score: 91 },
];
console.log(users.filter(u => u.score >= 60).map(u => u.name));
オブジェクトの配列を絞って必要な項目だけ抜く——APIの応答を扱うときに最も頻繁に書く形。実務コードの定番中の定番。
出力 == 430
reduce の中で Number() に通す。初期値は 0。
const raw = [" 100 ", "250", " 80 "]; console.log(raw.reduce((sum, r) => sum + Number(r), 0));
変換と集計を同時に行えるのが reduce の強み。.map(Number).reduce(…) と2段階に分けても書けるが、1回で済むならこちらが速い。
4実践シナリオ(5問)
出力3行が完全一致
先に taxed 配列を作ってから、prices を map((p, i) => ...) で回すと i で対応が取れる。
const prices = [1200, 850, 3400];
const taxed = prices.map(p => Math.round(p * 1.1));
prices.forEach((p, i) => {
console.log(`${p.toLocaleString()}円 → ${taxed[i].toLocaleString()}円`);
});
map と forEach の使い分けがポイント。データを作るのが map、画面に出すだけなのが forEach。map の第2引数で番号(index)も受け取れる。
出力2行が完全一致
passed 配列を作り、人数は length、平均は reduce で合計してから割り、toFixed(1) で小数1桁にする。
const scores = [82, 45, 91, 67, 38, 76];
const passed = scores.filter(s => s >= 60);
const average = passed.reduce((sum, s) => sum + s, 0) / passed.length;
console.log(`合格者:${JSON.stringify(passed)}`);
console.log(`${passed.length}人 / 平均${average.toFixed(1)}点`);
JS05のS03ではループとカウンタで書いた処理がfilter 1行+reduce 1行になった。抽出した配列があれば件数も平均もすぐ出せる。
「id=2 → 鈴木」
「id=99 → 見つかりません」 コーディング★★☆無料
出力2行が完全一致
find は見つからないと undefined を返す。?. と ?? を組み合わせると安全に書ける(JS07)。
const users = [
{ id: 1, name: "田中" },
{ id: 2, name: "鈴木" },
{ id: 3, name: "高橋" },
];
const found = users.find(u => u.id === 2);
const notFound = users.find(u => u.id === 99);
console.log(`id=2 → ${found?.name ?? "見つかりません"}`);
console.log(`id=99 → ${notFound?.name ?? "見つかりません"}`);
find の戻り値は undefined になりうるので、そのまま .name を付けるとエラーで止まる。JS07のオプショナルチェーン ?. がここで必須になる。
商品別3行+最終行 合計:3,800円
reduce の初期値を {} にし、(acc, [name, amount]) => { acc[name] = (acc[name] || 0) + amount; return acc; } とする。{ } を書いたので return を忘れないこと。
const sales = [["A", 1200], ["B", 800], ["A", 500], ["C", 300], ["B", 1000]];
const totals = sales.reduce((acc, [name, amount]) => {
acc[name] = (acc[name] || 0) + amount;
return acc;
}, {});
Object.entries(totals).forEach(([name, amount]) => {
console.log(`${name}:${amount.toLocaleString()}円`);
});
const sum = Object.values(totals).reduce((s, v) => s + v, 0);
console.log(`合計:${sum.toLocaleString()}円`);
reduce の初期値は数値だけでなくオブジェクトでもよい。JS07で for ループを使って書いた集計が reduce 1つに収まる。ただし return acc; を忘れると次の周回で undefined になるので要注意。
出力3行が完全一致
[...products] でコピーしてから sort((a, b) => b.sales - a.sales)、slice(0, 3)、forEach((p, i) => ...) とつなぐ。
const products = [
{ name: "ノートPC", sales: 128000 },
{ name: "マウス", sales: 6400 },
{ name: "モニター", sales: 45000 },
{ name: "キーボード", sales: 8500 },
];
[...products]
.sort((a, b) => b.sales - a.sales)
.slice(0, 3)
.forEach((p, i) => {
console.log(`${i + 1}位 ${p.name}:${p.sales.toLocaleString()}円`);
});
メソッドを縦に並べて書くと処理の流れが上から読める。コピー → 並べ替え → 切り出し → 出力、という手順がそのままコードの形になっている。sort は元を壊すので [...products] は必須(JS06)。
5仕上げ課題
const orders = [
{ item: "ノートPC", price: 128000, qty: 1, dept: "法人" },
{ item: "マウス", price: 3200, qty: 2, dept: "個人" },
{ item: "モニター", price: 45000, qty: 1, dept: "法人" },
{ item: "キーボード", price: 8500, qty: 3, dept: "個人" },
{ item: "USBメモリ", price: 1500, qty: 5, dept: "法人" },
];
仕様(すべて map / filter / reduce / sort を使うこと):
① 各注文の小計(price × qty)を出し、「ノートPC:128,000円」の形式で全件出力
② 総売上を reduce で算出
③ 小計が10,000円以上の注文を filter で数える
④ 部門別の合計を reduce で集計して出力
⑤ 小計の多い順トップ2の商品名を出力
出力仕様(12行):
=== 受注分析 ===
ノートPC:128,000円
マウス:6,400円
モニター:45,000円
キーボード:25,500円
USBメモリ:7,500円
---
総売上:212,400円 / 5件
1万円以上:3件
[部門別]
法人:180,500円
個人:31,900円
---
トップ2:ノートPC、モニター
出力13行が完全一致
まず map で { item, dept, subtotal } の配列を作っておくと、以降の集計がすべてこの配列から書ける。部門別は reduce の初期値を {} にする。トップ2は [...lines].sort((a,b) => b.subtotal - a.subtotal).slice(0,2).map(l => l.item).join("、")。
const orders = [
{ item: "ノートPC", price: 128000, qty: 1, dept: "法人" },
{ item: "マウス", price: 3200, qty: 2, dept: "個人" },
{ item: "モニター", price: 45000, qty: 1, dept: "法人" },
{ item: "キーボード", price: 8500, qty: 3, dept: "個人" },
{ item: "USBメモリ", price: 1500, qty: 5, dept: "法人" },
];
const lines = orders.map(o => ({
item: o.item,
dept: o.dept,
subtotal: o.price * o.qty,
}));
console.log("=== 受注分析 ===");
lines.forEach(l => {
console.log(`${l.item}:${l.subtotal.toLocaleString()}円`);
});
const total = lines.reduce((sum, l) => sum + l.subtotal, 0);
const big = lines.filter(l => l.subtotal >= 10000);
const byDept = lines.reduce((acc, l) => {
acc[l.dept] = (acc[l.dept] || 0) + l.subtotal;
return acc;
}, {});
const top2 = [...lines]
.sort((a, b) => b.subtotal - a.subtotal)
.slice(0, 2)
.map(l => l.item)
.join("、");
console.log("---");
console.log(`総売上:${total.toLocaleString()}円 / ${lines.length}件`);
console.log(`1万円以上:${big.length}件`);
console.log("[部門別]");
Object.entries(byDept).forEach(([dept, amount]) => {
console.log(`${dept}:${amount.toLocaleString()}円`);
});
console.log("---");
console.log(`トップ2:${top2}`);
最初に map で「小計付きの配列」を1本作り、以降のすべての集計をそこから導く——これがこの課題の設計の要。同じ計算を何度も書かずに済み、元データも汚さない。
注目してほしいのは o => ({ item: …, subtotal: … }) の丸カッコだ。アロー関数でオブジェクトをそのまま返すときは ( ) で包む必要がある。包まないと { } が関数の本体だと解釈され、undefined が返ってしまう。
JS05では for ループで書いていたこのレポートが、map / filter / reduce / sort を並べるだけで書けるようになった。ループ変数も条件分岐も出てこず、「何をしたいか」がそのままコードになっている。これが現代のJavaScriptの標準的な書き方で、実務のコードレビューでもこの形が期待される。
次章ではエラー処理を学ぶ。ここまでのコードは「データが必ず正しい」前提だったが、現実のデータは壊れている。