分類してまとめる groupBy

このレッスンで分かること

  • 分類は reduce の初期値を空オブジェクトにすると書ける
  • キーは文字列になるので、数値の年もそのまま使えない場面がある
  • Object.groupBy という新しい書き方も知っておく

分類してまとめる groupBy とは

配列の要素を、ある値ごとのまとまりに分けて 1 つのオブジェクトにする処理です。年ごとやタグごとに作品を束ねる、といった集計に使います。

欲しい形を先に決める

作品を年ごとに分けたいとき、出したい形はこうなります。キーが年、値がその年の作品の配列です。

// 欲しい形 { "2026": ["プロフィールサイト", "自己紹介カード"], "2025": ["スキル一覧の表"], "2024": ["学習ログ"] }

配列から 1 つのオブジェクトを作るので、担当は reduce です。初期値を空のオブジェクトにして、1 件ずつキーを決めて押し込みます。

const groups = works.reduce((acc, work) => { const key = String(work.year); if (!acc[key]) { acc[key] = []; } acc[key].push(work.title); return acc; }, {});

大事なのは 3 行目です。そのキーがまだ無いときだけ空の配列を用意します。これを書かないと、初回の push で undefined に対して push を呼ぶことになり落ちます。

存在の確認は acc[key] を真偽値として見る書き方で足ります。値が配列なので、あるときは必ず truthy になるからです。値が 0 や空文字になり得る集計では、この判定は使えません。

件数だけ数えるとき

配列ではなく件数だけ欲しいなら、値を数値にします。無いときは 0 から始める、と書ければ 3 行が 1 行になります。

const counts = works.reduce((acc, work) => { const key = String(work.year); acc[key] = (acc[key] || 0) + 1; return acc; }, {}); // { "2026": 2, "2025": 1, "2024": 1 }

同じ考え方で、年ごとの表示回数の合計も出せます。分類のキーを決める式と、値をどう足すかの 2 か所を差し替えるだけです。

const viewsByYear = works.reduce((acc, work) => { const key = String(work.year); acc[key] = (acc[key] || 0) + work.views; return acc; }, {});

分類の軸を年からタグに変えたいときも、キーを決める 1 行を差し替えるだけです。ただしタグは 1 件の作品が複数持つので、そのままではキーが 1 つに決まりません。この場合は内側でもう 1 回まわします。

const byTag = works.reduce((acc, work) => { work.tags.forEach((tag) => { if (!acc[tag]) { acc[tag] = []; } acc[tag].push(work.title); }); return acc; }, {});

1 件が複数の分類に入るので、値の合計は元の件数より多くなります。件数を出したときに「合わない」と思ったら、まずこの形になっていないかを疑います。

欲しいもの初期の値足し方
その分類の作品名空配列push でタイトルを足す
その分類の件数01 を足す
その分類の合計0views を足す
その分類の最大-Infinity大きいほうを残す

キーは必ず文字列になる

オブジェクトのキーは文字列です。数値を書いても、内部では文字列に直されます。だから acc[2026] と acc["2026"] は同じ場所を指します。

const acc = {}; acc[2026] = 1; console.log(acc["2026"]); // 1 console.log(Object.keys(acc)); // ["2026"]

意図せず数値のまま扱うと、あとで比較したときに型が合わずに困ります。キーを作る時点で String で包んでおくと、読む人にも「ここは文字列だ」と伝わります。

キーになる値が undefined のときも、そのまま文字列の undefined というキーができてしまいます。分類の軸に使う項目は、必ず値が入っているかを先に確かめるか、無いときの受け皿になるキーをこちらで決めておきます。

const key = work.year ? String(work.year) : "未設定";

数値のキーを持つオブジェクトは、Object.keys で取り出したときに昇順に並び替えられます。作品を年の新しい順に見せたいなら、並び順はキーに頼らず、取り出したあとで自分で sort します。

分類したあとに使う

作ったオブジェクトから中身を取り出すには、Object.keys と Object.entries を使います。keys はキーだけ、entries はキーと値の組を配列で返します。

Object.keys(groups); // ["2024", "2025", "2026"] Object.values(groups); // [[...], [...], [...]] Object.entries(groups); // [["2024", [...]], ["2025", [...]], ...]

entries は配列を返すので、そのまま map や sort につなげます。年の新しい順に見出しを作るなら、こう書けます。

const lines = Object.entries(groups) .sort((a, b) => Number(b[0]) - Number(a[0])) .map((entry) => entry[0] + "年 " + entry[1].length + "件");

キーは文字列なので、数値として並べたいところで Number に直しています。ここを忘れると、また文字列の辞書順で並びます。分割代入を使うと、entry[0] と entry[1] に名前が付いて読みやすくなります。この書き方は第 4 章で扱います。

Object.groupBy という書き方

分類はよくある処理なので、標準のメソッドが用意されました。Object.groupBy にキーを返す関数を渡すと、同じ形が 1 行で作れます。

const groups = Object.groupBy(works, (work) => String(work.year)); // { "2026": [作品オブジェクト, ...], ... }

ただし値は作品オブジェクトのままで、タイトルだけにはなりません。また比較的新しい機能なので、動かない環境がまだあります。仕組みを理解したうえで、使える環境かどうかを確かめてから使うのが安全です。この教材の課題では reduce で書きます。

よくある間違い

1 つ目は、キーが無いときの初期化を忘れる間違いです。

// 初回に落ちる acc[key].push(work.title); // 無ければ用意してから push する if (!acc[key]) { acc[key] = []; } acc[key].push(work.title);

2 つ目は、reduce のコールバックで acc を返し忘れる間違いです。push は追加した後の長さを返すので、返す値を間違えると数値が acc になります。

// 次の回の acc が数値になり落ちる const groups = works.reduce((acc, work) => acc[key].push(work.title), {}); // 最後に acc を返す const groups = works.reduce((acc, work) => { acc[key].push(work.title); return acc; }, {});

3 つ目は、空配列を渡したときの戻り値を勘違いする間違いです。初期値が空オブジェクトなので、返るのは空オブジェクトであって null でも undefined でもありません。呼び出した側で Object.keys の長さを見れば 0 件かどうか分かります。

要件

  1. 関数名は groupWorksByYear とし、引数は作品の配列 works を 1 つだけ受け取る
  2. 戻り値はオブジェクトで、キーは year を文字列にしたもの、値はその年の title を並べた配列にする
  3. published は条件に使わず、すべての作品を分類に含める
  4. 同じ年の作品は元の配列に出てきた順に並べ、空配列を渡されたときは空オブジェクトを返す

入出力例

groupWorksByYear([{"id":1,"published":true,"tags":["HTML","CSS"],"title":"プロフィールサイト","views":120,"year":2026},{"id":2,"published":true,"tags":["HTML"],"title":"自己紹介カード","views":48,"year":2026},{"id":3,"published":true,"tags":["HTML","CSS"],"title":"スキル一覧の表","views":32,"year":2025},{"id":4,"published":false,"tags":["JavaScript"],"title":"作品ギャラリー","views":8,"year":2025},{"id":5,"published":true,"tags":["JavaScript"],"title":"学習ログ","views":200,"year":2024}]){"2024":["学習ログ"],"2025":["スキル一覧の表","作品ギャラリー"],"2026":["プロフィールサイト","自己紹介カード"]} groupWorksByYear([{"id":1,"published":true,"tags":["HTML","CSS"],"title":"プロフィールサイト","views":120,"year":2026},{"id":2,"published":true,"tags":["HTML"],"title":"自己紹介カード","views":48,"year":2026}]){"2026":["プロフィールサイト","自己紹介カード"]} groupWorksByYear([{"id":5,"published":true,"tags":["JavaScript"],"title":"学習ログ","views":200,"year":2024}]){"2024":["学習ログ"]} groupWorksByYear([]){}

ヒント

生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア
編集 LuaGate編集部
main.js
main.js
学習モード

メモ

分類してまとめる groupBy

⌘S で保存