つくる 作品一覧の絞り込みと並び替え
このレッスンで分かること
- この章で覚えた 5 つのメソッドを 1 つの関数にまとめる
- 絞り込みの条件をオブジェクトで受け取り、画面から切り替えられるようにする
- 表示用の配列と集計値を同時に返す形を作る
つくる 作品一覧の絞り込みと並び替え とは
map と filter と reduce と sort と分類をすべて使い、作品一覧の表示データを 1 本の関数で組み立てる回です。次の章から先で、この関数が画面の描画とつながります。
ここまでで何ができるようになったか
第 2 章の 6 本で、配列を扱う道具がひととおりそろいました。並べると次のようになります。
| 覚えたこと | やること | 戻り値 |
|---|---|---|
| map | 1 件ずつ形を変える | 同じ長さの新しい配列 |
| filter | 条件で件数を減らす | 短くなった新しい配列 |
| find | 最初の 1 件を取る | 要素または undefined |
| reduce | 1 つの値にたたむ | 数値でもオブジェクトでもよい |
| sort | 順番を決める | 元を壊すので複製してから呼ぶ |
| 分類 | キーごとに束ねる | reduce で作るオブジェクト |
入門の最後に書いた pickWorks は、for 文と continue で条件を書き分けていました。あの処理は、この章の道具を使うと 1 本のパイプラインになります。今回はそこに集計と分類も足して、画面が必要とするデータを一度に返す関数にします。
何を返すか先に決める
画面が必要とするものを先に書き出します。一覧に出す文字列の配列、件数、表示回数の合計、年ごとの件数の 4 つです。
{
items: ["1. 学習ログ (200回)", "2. プロフィールサイト (120回)"],
count: 2,
total: 320,
byYear: { "2024": 1, "2026": 1 }
}戻り値の形を先に決めておくと、途中の書き方を変えても呼び出し側は影響を受けません。あとの章で描画関数を書くとき、この形をそのまま受け取ります。
関数の中で document を触らない、と決めておきます。この関数は配列を受け取って値を返すだけにしておくと、画面が無い状態でもテストできます。第 3 章で扱う純粋な関数の考え方の先取りです。
条件はオブジェクトで受け取る
絞り込みの条件が増えるたびに引数を足していくと、呼び出し側が読めなくなります。条件はまとめて 1 つのオブジェクトで受け取ります。
buildWorksView(works, {}); // 全部
buildWorksView(works, { minViews: 50 }); // 50 回以上
buildWorksView(works, { tag: "CSS" }); // CSS タグ付き
buildWorksView(works, { minViews: 100, tag: "JavaScript" }); // 両方指定されなかったキーは条件なしとして扱います。minViews が無ければ 0 として、tag が無ければタグを見ない、という形です。
const minViews = typeof options.minViews === "number" ? options.minViews : 0;
const tag = options.tag ? options.tag : null;条件を「無い」と判断する書き方には注意が要ります。minViews に 0 が渡ったとき、options.minViews || 0 でも結果は同じですが、意図しない値が来たときの挙動が変わります。型で確かめておくと安心です。
組み立てる
絞る、並べる、形にする、たたむ、の順に書きます。filter が新しい配列を返すので、sort の前の複製は無くても壊れませんが、意図を明示するために変数に受けてから複製します。
const visible = works.filter((work) => {
if (!work.published) {
return false;
}
if (work.views < minViews) {
return false;
}
if (tag !== null && !work.tags.includes(tag)) {
return false;
}
return true;
});条件が 3 つ以上あるときは、1 行の論理式にせず早期 return で並べたほうが読めます。あとから条件が増えても、行を 1 つ足すだけで済みます。
並べ替えと集計はここまでのレッスンのとおりです。sorted を作ってから、items と total と byYear をそれぞれ組み立てます。順位を items に振るので、並べ替えは必ず map より前に置きます。
const sorted = [...visible].sort((a, b) => b.views - a.views);
const items = sorted.map((work, index) => {
return (index + 1) + ". " + work.title + " (" + work.views + "回)";
});
const total = sorted.reduce((sum, work) => sum + work.views, 0);
const byYear = sorted.reduce((groups, work) => {
const key = String(work.year);
groups[key] = (groups[key] || 0) + 1;
return groups;
}, {});
return { items, count: sorted.length, total, byYear };最後の行はプロパティ名と変数名が同じなので、省略した書き方をしています。items と書けば items の値が入ります。count だけは sorted.length を入れたいので、名前を書いています。
byYear のキーの並びは、sorted の順に現れた年の順になります。表示に使うときは、取り出したあとで自分で並べ替えます。
動かして確かめる
作った関数は、引数だけを見て結果を決めます。画面が無くても、値を渡して戻り値を見れば動きが確かめられます。
const works = [
{ id: 1, title: "プロフィールサイト", year: 2026, views: 120, published: true, tags: ["HTML", "CSS"] },
{ id: 5, title: "学習ログ", year: 2024, views: 200, published: true, tags: ["JavaScript"] }
];
console.log(buildWorksView(works, {}));
console.log(buildWorksView(works, { minViews: 150 }));
console.log(buildWorksView(works, { tag: "CSS" }));2 回目と 3 回目で結果が変わっても、1 回目をもう一度呼べば同じ結果に戻ります。戻らないなら、どこかで元の配列を書き換えています。同じ引数で 2 回呼んで結果を比べるのは、壊していないかを見る手軽な確かめ方です。
0 件になる条件も必ず試します。minViews を大きくする、存在しないタグを渡す、空配列を渡す、の 3 つを通しておけば、画面につないだあとで落ちる場面はほとんど無くなります。
よくある間違い
1 つ目は、works に直接 sort を呼んでしまう間違いです。関数を 2 回呼ぶと 1 回目と 2 回目で結果が変わり、原因が分からなくなります。
// 呼ぶたびに元の works が並び替わる
const sorted = works.sort((a, b) => b.views - a.views);
// 絞った後の配列を複製してから並べる
const sorted = [...visible].sort((a, b) => b.views - a.views);2 つ目は、0 件のときに空でない値を返してしまう間違いです。items は空配列、byYear は空オブジェクト、count と total は 0 になるのが正しい形です。null を返すと、呼び出し側が毎回確かめる必要が出ます。
// 呼び出し側が困る
if (visible.length === 0) {
return null;
}
// 空のまま返す
return { items: [], count: 0, total: 0, byYear: {} };3 つ目は、順位を振ってから並べ替える間違いです。map で番号を付けたあとに sort すると、並びと番号がずれた一覧ができあがります。並べ替えが先、番号は最後です。
要件
- 関数名は buildWorksView とし、引数は作品の配列 works と条件のオブジェクト options の 2 つを受け取る
- published が true の作品だけを対象にし、options.minViews があれば views がその値以上、options.tag があれば tags にその値を含むものに絞る
- 対象を views の多い順に並べ、items は 1. 学習ログ (200回) の形の文字列の配列にする
- 戻り値は items と count と total と byYear の 4 つのキーを持ち、byYear は年を文字列にしたキーと件数の数値のオブジェクトにする
- 元の works は書き換えず、0 件のときは空配列と空オブジェクトと 0 を返す
入出力例
buildWorksView([{"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}], {}) → {"byYear":{"2024":1,"2025":1,"2026":2},"count":4,"items":["1. 学習ログ (200回)","2. プロフィールサイト (120回)","3. 自己紹介カード (48回)","4. スキル一覧の表 (32回)"],"total":400}
buildWorksView([{"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}], {"minViews":50}) → {"byYear":{"2024":1,"2026":1},"count":2,"items":["1. 学習ログ (200回)","2. プロフィールサイト (120回)"],"total":320}
buildWorksView([{"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}], {"tag":"CSS"}) → {"byYear":{"2025":1,"2026":1},"count":2,"items":["1. プロフィールサイト (120回)","2. スキル一覧の表 (32回)"],"total":152}
buildWorksView([{"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}], {"minViews":100,"tag":"JavaScript"}) → {"byYear":{"2024":1},"count":1,"items":["1. 学習ログ (200回)"],"total":200}
buildWorksView([], {}) → {"byYear":{},"count":0,"items":[],"total":0}