sort と比較関数
このレッスンで分かること
sortは元の配列をその場で並べ替えます。複製してから呼ぶのが既定の書き方です- 比較関数を書かないと、数値でも文字列として比べられます
並べ替えたら、元の並びが二度と戻らない
map も filter も新しい配列を返しました。sort は違います。呼び出した配列そのものを並べ替え、同じ配列を返します。
JavaScript
const views = [120, 48, 200];
const sorted = views.sort((a, b) => b - a);
console.log(sorted); // [200, 120, 48]
console.log(views); // [200, 120, 48] 元も変わっている
console.log(views === sorted); // truesorted と views は同じ配列です。別の変数に入れたから安心、とはなりません。表示用に並べ替えたつもりで、元データの並びまで変わっています。並べ替えの結果を画面に出すだけなら、一見は困りません。困るのは、そのあとで「登録した順に戻したい」となったときです。元の順を取り戻す手段はもうありません。
対策は 1 つで、並べ替える前に複製します。
JavaScript
const sorted = [...views].sort((a, b) => b - a);
// views は [120, 48, 200] のまま| 書き方 | 元の配列 | 戻り値 |
|---|---|---|
list.sort(fn) | 並べ替わる | 同じ配列 |
[...list].sort(fn) | 変わらない | 新しい配列 |
list.slice().sort(fn) | 変わらない | 新しい配列 |
複製の 3 文字を惜しまないのが結局は早道です。なお filter のあとに sort をつなぐときは複製が要りません。filter がすでに新しい配列を返しているので、そこを並べ替えても元は無傷です。複製が必要なのは、元の配列に直接 sort を呼ぶときだけです。
比較関数を書かないと、数値が文字列で並ぶ
比較関数を省略すると、要素はいったん文字列に直されてから辞書順に並べられます。
JavaScript
const views = [120, 48, 200, 8, 32];
console.log([...views].sort()); // [120, 200, 32, 48, 8]文字列として比べているので、先頭の文字が 1 の 120 が最初に来て、8 が最後に来ます。桁数がそろっていないほど結果はおかしくなります。要素が 1 桁だけのテストデータでは正しく見えるので、桁が増えて初めて気付きます。
数値として並べたいときは比較関数を渡します。引数を 2 つ受け取り、返した数値の正負で順番が決まります。負なら a を前に、正なら b を前に、0 なら順番を変えません。
JavaScript
[...views].sort((a, b) => a - b); // 昇順 [8, 32, 48, 120, 200]
[...views].sort((a, b) => b - a); // 降順 [200, 120, 48, 32, 8]a - b なら昇順、b - a なら降順、と形で覚えてしまって構いません。既定が文字列比較なのは、sort が数値以外の何が入っているか分からない配列も並べる必要があるからです。共通の物差しとして文字列を選んだ結果、数値の配列では意図しない並びになります。物差しは自分で渡すもの、と考えるとつじつまが合います。
オブジェクトの配列は、比べたい値を取り出す
要素がオブジェクトなら、比べたい項目を両側から取り出して引き算します。
JavaScript
const skills = [
{ name: "CSS", years: 2 },
{ name: "JavaScript", years: 5 }
];
const byExperience = [...skills].sort((a, b) => b.years - a.years);文字列で並べたいときは引き算が使えません。日本語を含む文字列なら a.name.localeCompare(b.name) を使うと自然な順になります。真偽値もそのままでは引けないので、Number(...) で数値に直してから比べます。
よくある間違い
- 比較関数で真偽値を返す —
(a, b) => a > bはtrueとfalseしか返さず、数値に直すと1と0にしかなりません。負の値が出ないので、要素数が増えると並びが崩れます。必ず数値を返します - 戻り値だけを見て元が無事だと思い込む — 同じ関数を 2 回呼ぶと 1 回目と 2 回目で結果が変わり、原因の分からない不具合になります。関数の中で並べ替えるなら、引数として受け取った配列には触らないと決めてください
要件
- 関数名は sortByViewsDesc とし、引数は作品の配列 works を 1 つだけ受け取る
- 戻り値は titles と firstOriginal の 2 つのキーを持つオブジェクトにする
- titles は views の多い順に並べたタイトルの配列とし、published は条件に使わない
- firstOriginal は関数を抜ける時点での works の先頭のタイトルとし、works が空なら null にする
入出力例
sortByViewsDesc([{"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}]) → {"firstOriginal":"プロフィールサイト","titles":["学習ログ","プロフィールサイト","自己紹介カード","スキル一覧の表","作品ギャラリー"]}
sortByViewsDesc([{"id":4,"published":false,"tags":["JavaScript"],"title":"作品ギャラリー","views":8,"year":2025},{"id":2,"published":true,"tags":["HTML"],"title":"自己紹介カード","views":48,"year":2026},{"id":1,"published":true,"tags":["HTML","CSS"],"title":"プロフィールサイト","views":120,"year":2026}]) → {"firstOriginal":"作品ギャラリー","titles":["プロフィールサイト","自己紹介カード","作品ギャラリー"]}
sortByViewsDesc([{"id":3,"published":true,"tags":["HTML","CSS"],"title":"スキル一覧の表","views":32,"year":2025}]) → {"firstOriginal":"スキル一覧の表","titles":["スキル一覧の表"]}
sortByViewsDesc([]) → {"firstOriginal":null,"titles":[]}