つくる 作品一覧の絞り込みと並び替え

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

  • この章で覚えた 5 つのメソッドを 1 つの関数にまとめる
  • 絞り込みの条件をオブジェクトで受け取り、画面から切り替えられるようにする
  • 表示用の配列と集計値を同時に返す形を作る

つくる 作品一覧の絞り込みと並び替え とは

map と filter と reduce と sort と分類をすべて使い、作品一覧の表示データを 1 本の関数で組み立てる回です。次の章から先で、この関数が画面の描画とつながります。

ここまでで何ができるようになったか

第 2 章の 6 本で、配列を扱う道具がひととおりそろいました。並べると次のようになります。

覚えたことやること戻り値
map1 件ずつ形を変える同じ長さの新しい配列
filter条件で件数を減らす短くなった新しい配列
find最初の 1 件を取る要素または undefined
reduce1 つの値にたたむ数値でもオブジェクトでもよい
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 すると、並びと番号がずれた一覧ができあがります。並べ替えが先、番号は最後です。

要件

  1. 関数名は buildWorksView とし、引数は作品の配列 works と条件のオブジェクト options の 2 つを受け取る
  2. published が true の作品だけを対象にし、options.minViews があれば views がその値以上、options.tag があれば tags にその値を含むものに絞る
  3. 対象を views の多い順に並べ、items は 1. 学習ログ (200回) の形の文字列の配列にする
  4. 戻り値は items と count と total と byYear の 4 つのキーを持ち、byYear は年を文字列にしたキーと件数の数値のオブジェクトにする
  5. 元の 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}

ヒント

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

メモ

つくる 作品一覧の絞り込みと並び替え

⌘S で保存