分類してまとめる 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 でタイトルを足す |
| その分類の件数 | 0 | 1 を足す |
| その分類の合計 | 0 | views を足す |
| その分類の最大 | -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 件かどうか分かります。
要件
- 関数名は groupWorksByYear とし、引数は作品の配列 works を 1 つだけ受け取る
- 戻り値はオブジェクトで、キーは year を文字列にしたもの、値はその年の title を並べた配列にする
- published は条件に使わず、すべての作品を分類に含める
- 同じ年の作品は元の配列に出てきた順に並べ、空配列を渡されたときは空オブジェクトを返す
入出力例
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([]) → {}