読書メモを「ページ数が少ない順」に表示するつもりで`sort()`を実行したら、元の本の並びまで変わっていた。この原因は、**`sort()`が元の配列そのものを並べ替える**ためです。元の順序を残して表示だけを変えたいなら`toSorted()`を使えます。違いを手元で確かめてみましょう。
まず答え:sortとtoSortedは「元データが変わるか」が違う
JavaScriptの配列は複数の値を順番に持つ入れ物です。`sort()`はその入れ物の要素順を書き換えて、同じ配列を返します。`toSorted()`は並べ替えた新しい配列を返し、元の配列の要素順を維持します。
| 確認したい点 | sort() | toSorted() |
|---|---|---|
| 元の配列の順番 | 変更する | 維持する |
| 戻り値 | 元と同じ配列 | 新しい配列 |
| 比較関数を指定できるか | できる | できる |
| 中にあるオブジェクトまで複製するか | しない | しない(浅いコピー) |
一覧の表示用に新しい配列を作りたい場合、`toSorted()`のほうが意図を表しやすくなります。反対に、元の並びを変更してよい処理であれば、`sort()`が必ず間違いというわけではありません。
(標準APIの動作については『MDN公式:Array.prototype.sort()』をご参照ください)
例:読書メモの並びを変えたら元の一覧も変わる
架空の3冊の読書メモを使います。各本は`id`、タイトル、ページ数を持ちます。最初の順番は「海辺のノート」「光の散歩道」「丘の物語」です。ページ数だけを基準に小さい順へ並べます。
const books = [
{ id: 'sea', pages: 180 },
{ id: 'light', pages: 90 },
{ id: 'hill', pages: 180 },
];
const display = books.sort((a, b) => a.pages - b.pages);
console.log(books.map((book) => book.id)); // ['light', 'sea', 'hill']
console.log(display === books); // true
`display === books`が`true`なので、`display`という別の変数を作っても、指している配列は同じです。ページ数の少ない本が先頭に来るよう、元の`books`も書き換わっています。あとで入力順に戻したい画面では、この違いが不具合につながります。
toSortedで表示用の順番だけを作る
今度は`books.toSorted()`を使います。Node.js 20以降など、このメソッドに対応する実行環境で動かしてください。Node.jsのバージョンは`node –version`で確認できます。
const assert = require('node:assert/strict');
// 読書メモの架空データ。idは各本を区別するための印です。
const books = [
{ id: 'sea', title: '海辺のノート', pages: 180 },
{ id: 'light', title: '光の散歩道', pages: 90 },
{ id: 'hill', title: '丘の物語', pages: 180 },
];
const initial = books.map((book) => book.id);
const display = books.toSorted((a, b) => a.pages - b.pages);
console.log(display.map((book) => book.id)); // ['light', 'sea', 'hill']
console.log(books.map((book) => book.id)); // ['sea', 'light', 'hill']
assert.deepStrictEqual(display.map((book) => book.id), ['light', 'sea', 'hill']);
assert.deepStrictEqual(books.map((book) => book.id), initial);
assert.notStrictEqual(display, books);
// コピーされるのは配列の入れ物。中の本オブジェクトは共有です。
display[0].title = '変更後のタイトル';
assert.strictEqual(books[1].title, '変更後のタイトル');
// 対応していない環境では「コピーしてからsort」を使えます。
const fallback = [...books].sort((a, b) => a.pages - b.pages);
assert.deepStrictEqual(fallback.map((book) => book.id), ['light', 'sea', 'hill']);
assert.deepStrictEqual(books.map((book) => book.id), initial);
console.log('検証完了: 元の配列の並びは保持されています');
`display`の順は`light → sea → hill`、元の`books`の順は`sea → light → hill`のままです。ページ数180の2冊の前後関係も保持されています。`assert`という検査命令が失敗せず、最後に「検証完了」と表示されることまで、この例をNode.jsで実行して確認しています。
実行するには`book-sorting.cjs`というファイル名で保存し、ターミナルで次を入力します。
node book-sorting.cjs
(toSortedの仕様と対応状況については『MDN公式:Array.prototype.toSorted()』をご参照ください)
ページ数のような数字では比較関数を指定する
比較関数とは、2つの値のうち、どちらを先に置くかを決める処理です。`(a, b) => a.pages – b.pages`は、ページ数が小さい本を先へ置きます。
比較関数を省くと、`sort()`も`toSorted()`も数字を文字列として比較するため、数値の大小順にならないことがあります。
const points = [8, 100, 30];
console.log(points.toSorted()); // [100, 30, 8]
console.log(points.toSorted((a, b) => a - b)); // [8, 30, 100]
console.log(points); // [8, 100, 30]
この例の出力は、比較関数なしだと`[100, 30, 8]`、ありだと`[8, 30, 100]`です。数字を昇順にしたい場合は比較関数を省略しないでください。
「新しい配列」でも本の情報は共有されている
ここは初心者が見落としやすい点です。`toSorted()`が新しく作るのは配列の入れ物であり、配列の中のオブジェクトがそれぞれ丸ごと複製されるわけではありません。
さきほどのコードでは、`display[0].title`を書き換えると、元の`books[1].title`にも変更が反映されます。`books`の要素順は維持されても、本オブジェクト自体は両方の配列で共有しているためです。
表示順だけ変えたいのか、各本の情報も独立して編集したいのかを分けて考えましょう。後者なら、目的に合ったオブジェクトのコピー方法が別途必要です。
toSortedが使えない環境ではどう書く?
古いNode.jsやブラウザでは`toSorted()`が存在しない場合があります。そのときは、スプレッド構文で配列をコピーしてから`sort()`を使う方法があります。
const display = [...books].sort((a, b) => a.pages - b.pages);
`[…books]`が新しい配列を作るため、並び替えても元の`books`の要素順は変わりません。ただし、この方法も浅いコピーです。
対応しているかは`typeof [].toSorted === "function"`で確認できます。使う人の実行環境を確かめてから、どちらの方法を採用するか決めてください。
原因調査のときは3つの値を確認する
- 並び替える前の`books.map((book) => book.id)`を保存する。
- 表示用配列の順番を確認する。
- 並べ替えた後の元配列の順番が、変更前と同じかを確認する。
「見た目は並び替わった」という確認だけでは、元の配列が変わっていないとは言えません。順番と参照先の違いを分けて確かめると、後の画面で意図せず並びが変わる問題も追いやすくなります。
(配列の基本を振り返りたい場合については『Route Zeroの記事:JavaScriptの配列・JSON・map・Map』をご参照ください)
よくある疑問:toSortedなら何も壊れない?
toSortedを使えば、元データは絶対に変化しませんか?
元の配列の要素順は変えませんが、配列内で共有するオブジェクトのプロパティは別の操作で変更できます。用途に応じて追加のコピーが必要です。
同じページ数の本はどちらが先ですか?
比較関数が0を返す同じページ数の本は、標準の安定ソートにより元の前後関係を保ちます。実際の例でも180ページの2冊の順は維持されます。
最後に:並び順とデータそのものを区別する
並べ替えで意識したいのは、一覧の見え方だけを変えたいのか、持っているデータの順番まで変えてよいのかという違いです。必要な結果を先に決め、`sort()`と`toSorted()`の動きを確認してから選ぶと、AIが書いた処理も自分で読み解きやすくなります。
次は書名での並び替えや「ページ数が同じときは登録日時で比べる」といった条件を加え、元配列の順序が保たれるかテストしてみてください。