つなげて組み立てる
このレッスンで分かること
- filter から sort、map へとつなぐと処理の流れがそのまま読める
- つなぐ順番を変えると計算量も結果も変わる
- 途中で新しい配列ができるので、元のデータは壊れない
つなげて組み立てる とは
filter と sort と map を 1 本の流れにつないで、表示用のデータを組み立てる書き方です。それぞれのメソッドが新しい配列を返す性質を利用します。
一時変数を並べる書き方から始める
まずは 1 行ずつ変数に受けて書いてみます。作品のうち公開済みで一定以上の表示回数のものを、多い順に並べ、順位付きの文字列にする、という流れです。
const visible = works.filter((work) => work.published && work.views >= 50);
const sorted = [...visible].sort((a, b) => b.views - a.views);
const items = sorted.map((work, index) => (index + 1) + ". " + work.title);このコードは正しく動きます。ただし visible や sorted という変数は、次の行に渡すためだけに存在しています。名前を考える手間の割に、何も説明していません。
メソッドが配列を返すので、そのまま次のメソッドを呼べます。
const items = works
.filter((work) => work.published && work.views >= 50)
.sort((a, b) => b.views - a.views)
.map((work, index) => (index + 1) + ". " + work.title);上から順に「絞る」「並べる」「文字にする」と読めます。この形をパイプラインと呼びます。1 行が長くなるので、点の前で改行して縦にそろえるのが読みやすい書き方です。
ここでは sort の前に複製が要りません。filter がすでに新しい配列を返しているからです。もし filter を外して works.sort(...) から始めると、元の works が並び替わります。パイプラインの先頭が元の配列そのものかどうかを毎回確かめます。
順番で結果が変わる
つなぐ順番には意味があります。同じ 3 つでも、入れ替えると結果が変わることがあります。
// 絞ってから順位を振る。順位は 1 から始まる
works.filter((w) => w.published).map((w, i) => (i + 1) + ". " + w.title);
// 順位を振ってから絞る。抜けた番号が残る
works.map((w, i) => ({ label: (i + 1) + ". " + w.title, published: w.published }))
.filter((w) => w.published);順位のような添字を使う処理は、絞り込みの後に置きます。逆にすると、非公開の作品の分だけ番号が飛びます。
計算量の面でも順番は効きます。filter を先に置けば、そのあとの sort と map は減った件数だけを見ます。5 件では差が出ませんが、件数が増えるほど効いてきます。
| 並び | 結果 | 見る件数 |
|---|---|---|
| filter から map | 番号が連続する | 絞った後の件数 |
| map から filter | 番号が飛ぶ | 全件を map する |
| filter から sort | 元の配列は無傷 | 絞った後の件数 |
| sort から filter | 元の配列が壊れる | 全件を並べ替える |
1 つの関数にまとめる
パイプラインは、引数を受けて配列を返す関数の中に入れると使い回せます。表示条件を引数にすると、画面の絞り込みボタンからそのまま呼べます。
function buildGallery(works, minViews) {
return works
.filter((work) => work.published && work.views >= minViews)
.sort((a, b) => b.views - a.views)
.map((work, index) => (index + 1) + ". " + work.title + " (" + work.views + "回)");
}この関数は引数だけを見て結果を決め、外の何も書き換えません。同じ引数なら何度呼んでも同じ結果が返るので、テストも書きやすくなります。
つなげすぎると読みにくくなります。目安として 4 段を超えたら、意味のあるまとまりで一度変数に受けます。名前が付けられない切れ目なら、それはまだ 1 つの処理だということです。
最後に文字列にする
一覧を 1 本の文字列にしたいときは、map のあとに join を足します。join は配列を返さないので、必ず最後に置きます。
const text = works
.filter((work) => work.published)
.map((work) => work.title)
.join(" / ");
// "プロフィールサイト / 自己紹介カード / 学習ログ"区切り文字を省略するとカンマになります。空配列に join を呼ぶと空文字が返るので、0 件のときの分岐を書かずに済みます。
集計値も同じ流れの中で出せます。ただし reduce も配列を返さないので、items と total を両方ほしいときは途中で変数に受けます。
const visible = works.filter((work) => work.published);
const items = visible.map((work) => work.title);
const total = visible.reduce((sum, work) => sum + work.views, 0);同じ filter を 2 回書かずに済むよう、絞った結果を 1 回だけ変数に置くのがこつです。呼び出しの回数が減るだけでなく、条件が 1 か所になるので、あとで直すときに片方だけ直す事故が起きません。
途中を確かめる
つなげた式が期待どおりに動かないとき、どの段で崩れたかを見る必要があります。一時的に段を分けて変数に受け、1 つずつ中身を見るのが確実です。
const step1 = works.filter((work) => work.published);
console.log(step1.length);
const step2 = [...step1].sort((a, b) => b.views - a.views);
console.log(step2.map((work) => work.views));段の途中に console.log を挟みたいときは、map の中で値を返しつつログを出す形にもできます。ただし戻り値を返し忘れると次の段が undefined だらけになるので、慣れないうちは変数に分けるほうが安全です。
| 症状 | 疑うところ |
|---|---|
| 件数が減らない | filter の条件が常に truthy になっている |
| 並びがおかしい | 比較関数を渡し忘れている |
| undefined が混ざる | map の中で return を書き忘れている |
| 元データが変わる | 先頭で元の配列に sort を呼んでいる |
よくある間違い
1 つ目は、途中で undefined を混ぜてしまう間違いです。map の中で条件分岐して片方だけ return すると、次の sort や join がおかしくなります。
// 非公開の位置が undefined になる
works.map((w) => { if (w.published) return w.title; }).sort();
// 絞ってから形を変える
works.filter((w) => w.published).map((w) => w.title).sort();2 つ目は、forEach をパイプラインの途中に挟む間違いです。forEach は undefined を返すので、そこで鎖が切れます。
// TypeError になる
works.filter((w) => w.published).forEach((w) => w.title).join(", ");
// map を使う
works.filter((w) => w.published).map((w) => w.title).join(", ");3 つ目は、パイプラインの先頭で元の配列に sort を呼ぶ間違いです。filter が後ろにあると、絞り込みより先に並べ替えが起きて元データが壊れます。並べ替えは必ず、新しい配列になった後に置きます。
要件
- 関数名は buildGallery とし、引数は作品の配列 works と数値 minViews の 2 つを受け取る
- published が true かつ views が minViews 以上の作品だけを対象にし、views の多い順に並べる
- 要素の文字列は 1 から始まる順位を付けて 1. 学習ログ (200回) の形にそろえる
- 元の works は書き換えず、対象が 0 件のときは空配列を返す
入出力例
buildGallery([{"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}], 0) → ["1. 学習ログ (200回)","2. プロフィールサイト (120回)","3. 自己紹介カード (48回)","4. スキル一覧の表 (32回)"]
buildGallery([{"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}], 50) → ["1. 学習ログ (200回)","2. プロフィールサイト (120回)"]
buildGallery([{"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}], 1000) → []
buildGallery([{"id":4,"published":false,"tags":["JavaScript"],"title":"作品ギャラリー","views":8,"year":2025}], 0) → []
buildGallery([], 0) → []