reduce でキー別にグルーピングする
reduce でキー別にグルーピングする とは
オブジェクト配列を特定のキーでグルーピングする実用パターンを reduce で実装する。本レッスンでは、reduce でキー別にグルーピングする の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。
reduce でキー別にグルーピングする
データを「ある属性ごとにまとめたい」というのは、アプリ開発でよく出てくる要望です。商品をカテゴリ別に、ユーザーを国別に、注文を月別に集計する、という具合です。SQL でいう GROUP BY の JavaScript 版を、reduce で実装するパターンを身につけましょう。ES2024 で正式に Object.groupBy が追加されましたが、まだ古いランタイムでも動く reduce 版は実務で必須です。
グルーピングは 「キーを計算 → 既存配列に push」 のシンプルなパターン。理解すれば応用が効く。
想定する入力と出力
例えば次のようなユーザー配列を、role (役職) ごとに分けてみます。
JavaScript
const users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Carol", role: "admin" },
{ name: "Dave", role: "user" },
];欲しい結果は次の通りです。
JavaScript
{
admin: [{ name: "Alice", role: "admin" }, { name: "Carol", role: "admin" }],
user: [{ name: "Bob", role: "user" }, { name: "Dave", role: "user" }],
}reduce で実装する
reduce の初期値を 空オブジェクト {} にして、配列を 1 周しながらキーごとに push していけば完成です。
JavaScript
function groupBy(arr, key) {
return arr.reduce((acc, item) => {
const k = item[key];
if (!acc[k]) acc[k] = [];
acc[k].push(item);
return acc;
}, {});
}肝は if (!acc[k]) acc[k] = [] の行です。そのキーが初登場なら空配列を作り、すでにあれば既存配列に追加します。配列を作る用意さえできれば、あとは push するだけです。
JavaScript
const grouped = groupBy(users, "role");
console.log(grouped.admin.length); // 2
console.log(grouped.user.length); // 2動きを追ってみる
図のとおり、要素ごとにキーを計算し、acc にそのキーがなければ空配列を新設、そして push する流れの繰り返しです。
よりモダンな書き方
スプレッドで acc を毎回再生成するパターンも見かけますが、毎回新オブジェクトを作る分やや遅いです。要素数が多くなければ可読性で選んでも OK です。
JavaScript
function groupBy(arr, key) {
return arr.reduce((acc, item) => ({
...acc,
[item[key]]: [...(acc[item[key]] || []), item],
}), {});
}ES2024 環境なら標準の Object.groupBy が使えます。引数の順序が逆 (関数を後に取る) なので注意です。
JavaScript
// Object.groupBy は callback を取る
const grouped = Object.groupBy(users, (u) => u.role);速度を取るか、不変性を取るか。ホットパスでは元 acc を変更、関数型に揃えたいなら spread、という基準で使い分ける。
よくある間違い
第 1 に、reduce の初期値 {} を忘れる間違いです。初期値なしだと、最初の item を初期値として使ってしまい、acc[k] のアクセスでエラーになります。第 2 に、キーが undefined の要素があると、acc[undefined] という変なキーが生まれます。事前に filter で除くか、デフォルト値を入れます。第 3 に、グルーピング後のオブジェクトは順序が保証されないという思い込みがありますが、現代の JavaScript は文字列キーの挿入順を保持するので、最初に登場した順で並びます。
応用 - 件数だけ集計
push の代わりに ++ すれば、グルーピングではなく件数集計になります。
JavaScript
const countByRole = users.reduce((acc, u) => {
acc[u.role] = (acc[u.role] || 0) + 1;
return acc;
}, {});
console.log(countByRole); // { admin: 2, user: 2 }こちらも同じ「キー → 既存値を取り出して更新」のパターンです。
グルーピングと件数集計は 同じ骨格 のコードで書ける。慣れたら使い分けの判断は一瞬。
やってみよう
オブジェクト配列 arr とキー名 key を受け取り、key の値ごとにグルーピングしたオブジェクトを返す groupBy 関数を書きましょう。reduce の初期値を {} にし、if (!acc[k]) acc[k] = [] で配列を用意して push するだけです。これは現場でも頻繁に書くテクニックなので、手で何度か書いて指に覚えさせると良いです。
よくある質問
Q. reduce はどんなときに使うべきですか?
A. 合計・最大値・グルーピングなど、配列を 1 つの値や別の構造にまとめたいときに有効です。単純な合計なら sum / Array.prototype.reduce((a,b)=>a+b,0) で OK ですが、複雑なロジックでは for ループの方が読みやすいこともあります。
Q. 初期値は省略しても良いですか?
A. 省略すると配列の先頭要素が初期値になり、空配列のときに TypeError になります。明示的に 0 や [] を渡しておくと安全で、型推論にも優しくなります。可読性と安全性の両面から初期値は付ける派が多数です。
Q. reduce でオブジェクトを作るには?
A. items.reduce((acc, x) => ({ ...acc, [x.id]: x }), {}) のように初期値に {} を渡し、各反復で展開構文を使います。要素数が多い場合は scatter で性能劣化するため、acc[x.id] = x; return acc; のように mutate する形も検討してください。
次のレッスン
次は JavaScript 入門 最終総まとめクイズ で、オブジェクト配列を特定のキーでグルーピングする実用パターンを reduce で実装する を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- groupBy の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. groupBy とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- Array.prototype.reduce を使うこと
- reduce の初期値を空オブジェクト
{}にすること - 各グループは配列で保持すること
入出力例
test-cases.txt
groupBy([{"name":"Alice","role":"admin"},{"name":"Bob","role":"user"},{"name":"Carol","role":"admin"}], "role") → {"admin":[{"name":"Alice","role":"admin"},{"name":"Carol","role":"admin"}],"user":[{"name":"Bob","role":"user"}]}
groupBy([{"id":1,"type":"a"},{"id":2,"type":"b"},{"id":3,"type":"a"}], "type") → {"a":[{"id":1,"type":"a"},{"id":3,"type":"a"}],"b":[{"id":2,"type":"b"}]}
groupBy([], "role") → {}
groupBy([{"x":"p"}], "x") → {"p":[{"x":"p"}]}