JavaScriptで配列を並べ替えるには、sort()(元の配列を書き換える)か toSorted()(新しい配列を返す)に「比較関数」を渡します。 比較関数は (a, b) => a - b のように、a を前にしたいなら負の数、b を前にしたいなら正の数、同じなら 0 を返す関数です。

この記事では、数値・文字列・日本語・ファイル名の自然順・日付・オブジェクト・複数キー・null を末尾へ・真偽値・指定した順番まで、比較関数をすべて Node.js 22.22.2 の実行結果つきのコピペ表 にまとめました。あわせて、引数なし sort() で [10, 9, 1] が [1, 10, 9] になる理由、ES2019 以降で保証された安定ソートの確認、Intl.Collator を使い回したときの速度差(5万件で約48倍)、React の state を並べ替えるときの書き方も扱います。


配列ソートの早見表(やりたいこと別コピペ)

比較関数をこの表から選んで arr.toSorted(ここ) に入れれば、ほとんどの並べ替えができます。 結果はすべて Node.js 22.22.2 で実行して確認しています。

やりたいこと 比較関数(コピペ用) 実行結果の例
数値を小さい順 (a, b) => a - b [10, 9, 1] → [1, 9, 10]
数値を大きい順 (a, b) => b - a [1, 9, 10] → [10, 9, 1]
文字列をアルファベット順(大文字小文字まとめて) (a, b) => a.localeCompare(b, "en") apple, Apple, banana, Banana, cherry
日本語(かな)を五十音順 new Intl.Collator("ja").compare アイス, アイス, あめ, かき, がっこう, ガム…
ファイル名を自然順(file2 < file10) new Intl.Collator("ja", { numeric: true }).compare file1, file2, File3, file10
ISO形式の日付文字列 (a, b) => a.localeCompare(b) 2025-12-31, 2026-01-15, 2026-10-01
Date オブジェクトを新しい順 (a, b) => b - a 新しい日時が先頭
オブジェクトのプロパティで (a, b) => a.priority - b.priority priority の小さい順
複数キー(優先度→期限) (a, b) => a.priority - b.priority || a.due.localeCompare(b.due) 第1キーが同じとき第2キーで比較
null・undefined を末尾へ (a, b) => (a == null) - (b == null) || … 伊藤, 佐藤, 鈴木, null, undefined
真偽値(false を先) (a, b) => Number(a.done) - Number(b.done) 未完了 → 完了
決めた順番(ステータス順など) (a, b) => rank.get(a) - rank.get(b) 進行中, 未着手, 保留, 完了
元の配列を変えずに並べ替え arr.toSorted(cmp) / [...arr].sort(cmp) 元の配列はそのまま
逆順にする arr.toReversed() 元の配列はそのまま

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript配列ソートの使い方|数値・日付・複数キーとtoSorted例」


sort・toSorted・reverse・toReversed の違い(破壊・非破壊)

sort() と reverse() は元の配列を書き換え、toSorted() と toReversed() は元を残して新しい配列を返します。 toSorted などは ES2023 で追加され、MDN では 2023年7月から主要ブラウザすべてで使える(Baseline)と案内されています。Node.js は 20 以降で使えます。

メソッド 元の配列 戻り値 追加された版 主な用途
sort(cmp) 書き換える 元の配列そのもの ES1 一時的な配列をその場で並べ替える
toSorted(cmp) 変えない 新しい配列 ES2023 state・引数・共有データの並べ替え
reverse() 書き換える 元の配列そのもの ES1 その場で逆順
toReversed() 変えない 新しい配列 ES2023 元を残して逆順
toSpliced() / with() 変えない 新しい配列 ES2023 要素の削除・置き換え(非破壊)
// s1.mjs  実行: node s1.mjs(Node.js 22.22.2)
const a = [3, 1, 2];
const r1 = a.sort();
console.log("sort:", a, "戻り値===元配列:", r1 === a);
const b = [3, 1, 2];
const r2 = b.toSorted();
console.log("toSorted:", b, "→", r2, "同じ配列?:", r2 === b);
const c = [1, 2, 3];
console.log("reverse:", c.reverse(), "元:", c);
const d = [1, 2, 3];
console.log("toReversed:", d.toReversed(), "元:", d);
const e = [1, 2, 3];
console.log("toSpliced:", e.toSpliced(1, 1), "with:", e.with(0, 9), "元:", e);
sort: [ 1, 2, 3 ] 戻り値===元配列: true
toSorted: [ 3, 1, 2 ] → [ 1, 2, 3 ] 同じ配列?: false
reverse: [ 3, 2, 1 ] 元: [ 3, 2, 1 ]
toReversed: [ 3, 2, 1 ] 元: [ 1, 2, 3 ]
toSpliced: [ 1, 3 ] with: [ 9, 2, 3 ] 元: [ 1, 2, 3 ]

const sorted = arr.sort() と書いても、sorted と arr は同じ配列です。 元を残したいときは toSorted()、古い実行環境に合わせるなら [...arr].sort() か arr.slice().sort() を使います。

凍結した配列・TypeScript での注意

try { Object.freeze([2, 1]).sort(); } catch (err) { console.log(err.constructor.name + ": " + err.message); }
console.log(Object.freeze([2, 1]).toSorted());
TypeError: Cannot assign to read only property '0' of object '[object Array]'
[ 1, 2 ]

Object.freeze した配列や、Redux・Immer などで凍結された state は sort() でエラーになりますが、toSorted() なら並べ替えられます。 TypeScript で toSorted を使うと、lib が ES2022 以前のとき次のエラーになります(TypeScript 7.0.2 で確認)。

error TS2550: Property 'toSorted' does not exist on type 'number[]'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2023' or later.

tsconfig.json の lib(または target)を ES2023 以上にすると解消します。


引数なし sort の落とし穴:[10, 9, 1] が文字列順になる

比較関数を渡さない sort() は、要素を文字列に変換して UTF-16 のコード順で並べます。 そのため数値は「辞書順」になります。

console.log([10, 9, 1].sort());
console.log([10, 9, 1, 100, 25].sort());
console.log([10, 9, 1, 100, 25].sort((x, y) => x - y));
console.log(["b", "B", "a", "A", "あ", "ア", "1", "10", "2"].sort());
console.log([true, false, true].sort(), [-1, -10, 2].sort());
[ 1, 10, 9 ]
[ 1, 10, 100, 25, 9 ]
[ 1, 9, 10, 25, 100 ]
[
  '1', '10', '2',
  'A', 'B',  'a',
  'b', 'あ', 'ア'
]
[ false, true, true ] [ -1, -10, 2 ]
引数なし sort の並び 理由
10 が 9 より前 文字列 "10" と "9" の1文字目 "1" < "9" で比べる
大文字が小文字より前 "A"(U+0041)< "a"(U+0061)
ひらがなが カタカナより前 "あ"(U+3042)< "ア"(U+30A2)
-1 が -10 より前 文字列 "-1" は "-10" の先頭部分なので短い方が前

数値を並べるときは、必ず (a, b) => a - b を渡します。

よくある間違いと実際の結果

// 比較関数が boolean を返す(負の数を返さない)
console.log([3, 1, 2, 5, 4, 10, 8, 7, 6, 9, 11, 12].sort((x, y) => x > y));
// 文字列の配列を a - b で並べる(暗黙の数値変換で動く)
console.log(["10", "9", "1"].sort((x, y) => x - y));
// 比較関数に関数以外を渡す
try { [1, 2].toSorted("desc"); } catch (err) { console.log(err.constructor.name + ": " + err.message); }
[
   3,  1, 2, 5, 4,
  10,  8, 7, 6, 9,
  11, 12
]
[ '1', '9', '10' ]
TypeError: The comparison function must be either a function or undefined: desc
書き方 起きること 直し方
(x, y) => x > y true/false は 1/0 になり負の数を返さないので、並ばない (x, y) => x - y
["10","9"].sort((x, y) => x - y) 文字列のまま数値順に並ぶ(値は文字列のまま) 必要なら先に map(Number)
toSorted("desc") TypeError 降順は比較関数 (a, b) => b - a で
NaN を含む配列で a - b 比較結果が NaN になり位置が不定 下の「NaN を末尾へ」を使う

比較関数のしくみ

比較関数は「a を b より前に置くなら負、後ろなら正、同じなら 0」を返します。

戻り値 意味 降順にするには
負の数 a を前に a と b を入れ替える(b - a)
正の数 b を前に 同上
0 順番を変えない(安定ソートで元の順を保つ) ―
flowchart LR
  A["比較関数 cmp(a, b)"] --> B{"戻り値"}
  B -- "負(例: -1)" --> C["a → b の順"]
  B -- "正(例: 1)" --> D["b → a の順"]
  B -- "0" --> E["元の順のまま(安定)"]

複数キーで並べるときは、|| で「0 なら次の比較へ」とつなげられます。 0 は falsy なので、第1キーが同じときだけ第2キーが評価されます。


比較関数のコピペ表(Node.js 22 実行結果つき)

以下は1つのファイル(s2.mjs)で実行したものです。出力には node:util の inspect を使っています(実行結果は、ラベルの空白と配列の折り返しだけ読みやすく整えています)。

import { inspect } from "node:util";
const show = (label, v) => console.log(label.padEnd(26), inspect(v, { breakLength: Infinity, depth: 3 }));

数値(昇順・降順・NaN・BigInt)

const nums = [10, 9, 1, 100, 25, -3, 0.5];
show("数値 昇順", nums.toSorted((a, b) => a - b));
show("数値 降順", nums.toSorted((a, b) => b - a));
show("NaN混在", [3, NaN, 1, 2].toSorted((a, b) => a - b));
show("NaN末尾", [3, NaN, 1, 2].toSorted((a, b) => (Number.isNaN(a) - Number.isNaN(b)) || a - b));
show("Infinity", [1, Infinity, -Infinity, 0].toSorted((a, b) => a - b));
show("BigInt", [10n, 2n, 33n].toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0)));
数値 昇順    [ -3, 0.5, 1, 9, 10, 25, 100 ]
数値 降順    [ 100, 25, 10, 9, 1, 0.5, -3 ]
NaN混在      [ 3, NaN, 1, 2 ]
NaN末尾      [ 1, 2, 3, NaN ]
Infinity     [ -Infinity, 0, 1, Infinity ]
BigInt       [ 2n, 10n, 33n ]

NaN が混じると a - b が NaN を返し、配列がほとんど並びません。 Number.isNaN(a) - Number.isNaN(b) を先に比べると末尾に集められます。BigInt は a - b の結果が BigInt になり比較関数の戻り値として使えないため、< > で -1/0/1 を返します。

文字列(localeCompare・大文字小文字・降順)

const words = ["banana", "Apple", "cherry", "apple", "Banana"];
show("文字列 既定sort", words.toSorted());
show("文字列 < >", words.toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0)));
show("localeCompare en", words.toSorted((a, b) => a.localeCompare(b, "en")));
show("localeCompare ja", words.toSorted((a, b) => a.localeCompare(b, "ja")));
show("文字列 降順", words.toSorted((a, b) => b.localeCompare(a, "en")));
文字列 既定sort      [ 'Apple', 'Banana', 'apple', 'banana', 'cherry' ]
文字列 < >           [ 'Apple', 'Banana', 'apple', 'banana', 'cherry' ]
localeCompare en     [ 'apple', 'Apple', 'banana', 'Banana', 'cherry' ]
localeCompare ja     [ 'apple', 'Apple', 'banana', 'Banana', 'cherry' ]
文字列 降順          [ 'cherry', 'Banana', 'banana', 'Apple', 'apple' ]

< > での比較はコード順(大文字が先)、localeCompare は辞書順(同じ文字の大文字小文字が隣り合う)です。 画面に表示する一覧は localeCompare / Intl.Collator、IDやキーのように機械的な順でよいものは < > が速くて確実です。

ファイル名・番号を自然順に(numeric: true)

const files = ["file10.txt", "file2.txt", "file1.txt", "File3.txt"];
show("ファイル名 既定", files.toSorted());
const natural = new Intl.Collator("ja", { numeric: true, sensitivity: "base" });
show("自然順 numeric", files.toSorted(natural.compare));
ファイル名 既定   [ 'File3.txt', 'file1.txt', 'file10.txt', 'file2.txt' ]
自然順 numeric    [ 'file1.txt', 'file2.txt', 'File3.txt', 'file10.txt' ]

numeric: true は、文字列の中の数字を数値として比べます。 sensitivity: "base" は大文字小文字・濁点などの違いを同じとみなす設定です。

Intl.Collator のオプション 効果
numeric: true "2" < "10"(自然順)
sensitivity: "base" a と A、か と が を同じ扱いに
caseFirst: "upper" 同じ文字なら大文字を先に
ignorePunctuation: true 記号を無視して比べる

日本語(五十音順・漢字は読み仮名で)

const kana = ["さくら", "アイス", "かき", "あめ", "ガム", "カメラ", "がっこう", "アイス", "1番", "2番", "10番"];
show("日本語 既定sort", kana.toSorted());
show("日本語 ja Collator", kana.toSorted(new Intl.Collator("ja").compare));
show("日本語 numeric", kana.toSorted(new Intl.Collator("ja", { numeric: true }).compare));
const kanji = ["東京", "大阪", "愛知", "北海道", "沖縄"];
show("漢字 ja Collator", kanji.toSorted(new Intl.Collator("ja").compare));
const items = [
  { name: "東京", kana: "とうきょう" }, { name: "大阪", kana: "おおさか" }, { name: "愛知", kana: "あいち" },
  { name: "北海道", kana: "ほっかいどう" }, { name: "沖縄", kana: "おきなわ" },
];
const ja = new Intl.Collator("ja");
show("読み仮名で", items.toSorted((a, b) => ja.compare(a.kana, b.kana)).map((x) => x.name));
日本語 既定sort      [ '10番', '2番', 'あめ', 'かき', 'がっこう', 'さくら', 'アイス', 'カメラ', 'ガム', '1番', 'アイス' ]
日本語 ja Collator   [ '10番', '1番', '2番', 'アイス', 'アイス', 'あめ', 'かき', 'がっこう', 'ガム', 'カメラ', 'さくら' ]
日本語 numeric       [ '1番', '2番', '10番', 'アイス', 'アイス', 'あめ', 'かき', 'がっこう', 'ガム', 'カメラ', 'さくら' ]
漢字 ja Collator     [ '愛知', '沖縄', '大阪', '東京', '北海道' ]
読み仮名で           [ '愛知', '大阪', '沖縄', '東京', '北海道' ]
並べ方 ひらがな・カタカナ 全角・半角 漢字
引数なし sort() ひらがな → カタカナ(別々) 全角数字・半角カナは末尾 コード順
Intl.Collator("ja") 五十音で混ざる(ア と あ が近い) 全角 1 と半角 1 を同じ数字として扱う 読みとは一致しない
numeric: true を追加 同上 1番 < 2番 < 10番 同上
読み仮名のプロパティで比較 五十音順 ― 読みどおり

Intl.Collator("ja") は漢字の読みを知らないため、「大阪(おおさか)」が「沖縄(おきなわ)」より後ろになりました。 漢字の名前を五十音順に並べたいときは、データに読み仮名(kana など)を持たせて、それで比べます。

日付(ISO文字列・ゼロ埋めなし・Date)

const isoDates = ["2026-10-01", "2025-12-31", "2026-01-15"];
show("ISO文字列 昇順", isoDates.toSorted());
show("ISO文字列 降順", isoDates.toSorted((a, b) => b.localeCompare(a)));
const slashDates = ["2026/1/5", "2026/10/1", "2026/2/20"];
show("ゼロ埋めなし 既定", slashDates.toSorted());
show("ゼロ埋めなし Date化", slashDates.toSorted((a, b) => new Date(a) - new Date(b)));
const dateObjs = [new Date("2026-10-01T09:00:00+09:00"), new Date("2025-12-31T00:00:00Z"), new Date("2026-01-15")];
show("Date 新しい順", dateObjs.toSorted((a, b) => b - a).map((d) => d.toISOString()));
show("Date getTime", dateObjs.toSorted((a, b) => a.getTime() - b.getTime()).map((d) => d.toISOString()));
ISO文字列 昇順        [ '2025-12-31', '2026-01-15', '2026-10-01' ]
ISO文字列 降順        [ '2026-10-01', '2026-01-15', '2025-12-31' ]
ゼロ埋めなし 既定     [ '2026/1/5', '2026/10/1', '2026/2/20' ]
ゼロ埋めなし Date化   [ '2026/1/5', '2026/2/20', '2026/10/1' ]
Date 新しい順         [ '2026-10-01T00:00:00.000Z', '2026-01-15T00:00:00.000Z', '2025-12-31T00:00:00.000Z' ]
Date getTime          [ '2025-12-31T00:00:00.000Z', '2026-01-15T00:00:00.000Z', '2026-10-01T00:00:00.000Z' ]
日付の形式 比較関数 注意点
"2026-10-01"(ゼロ埋めISO) (a, b) => a.localeCompare(b) または引数なし 文字列順=日付順なので速い
"2026-10-01T09:00:00+09:00"(タイムゾーン混在) (a, b) => Date.parse(a) - Date.parse(b) オフセットが違うと文字列順と時刻順がずれる
"2026/1/5"(ゼロ埋めなし) (a, b) => new Date(a) - new Date(b) 文字列順だと 10月 が 2月 より前になる
Date オブジェクト (a, b) => a - b / a.getTime() - b.getTime() - で自動的に数値(ミリ秒)になる

件数が多いときは、比較のたびに new Date() するより、先に数値へ変換しておく方が速くなります(後述の「事前に変換する」参照)。日付ライブラリや Temporal の扱いは既存記事「JavaScript Temporal 使い方」で解説しています。

オブジェクト・複数キー・真偽値・null を末尾へ

サンプルデータ(タスク一覧)です。

const tasks = [
  { id: 1, title: "請求書作成",   priority: 2, due: "2026-10-05", done: false, assignee: "佐藤" },
  { id: 2, title: "備品発注",     priority: 1, due: "2026-10-03", done: true,  assignee: null },
  { id: 3, title: "会議準備",     priority: 2, due: "2026-10-02", done: false, assignee: "鈴木" },
  { id: 4, title: "在庫確認",     priority: 1, due: "2026-10-03", done: false, assignee: undefined },
  { id: 5, title: "記事レビュー", priority: 3, due: "2026-10-01", done: true,  assignee: "伊藤" },
];
const ids = (arr) => arr.map((t) => t.id);
const ja = new Intl.Collator("ja");

show("priority 昇順", ids(tasks.toSorted((a, b) => a.priority - b.priority)));
show("複数キー pri→due", ids(tasks.toSorted((a, b) => a.priority - b.priority || a.due.localeCompare(b.due))));
show("複数キー pri降→due→id", ids(tasks.toSorted((a, b) =>
  b.priority - a.priority || a.due.localeCompare(b.due) || a.id - b.id)));
show("真偽値 未完了を先", ids(tasks.toSorted((a, b) => Number(a.done) - Number(b.done))));
show("真偽値 完了を先", ids(tasks.toSorted((a, b) => Number(b.done) - Number(a.done))));

const nullsLast = (a, b) => (a == null) - (b == null) || (a == null ? 0 : ja.compare(a, b));
show("null/undefined 末尾", tasks.toSorted((a, b) => nullsLast(a.assignee, b.assignee)).map((t) => t.assignee ?? String(t.assignee)));
const nullsFirst = (a, b) => (b == null) - (a == null) || (a == null ? 0 : ja.compare(a, b));
show("null/undefined 先頭", tasks.toSorted((a, b) => nullsFirst(a.assignee, b.assignee)).map((t) => t.assignee ?? String(t.assignee)));
show("プロパティ欠落のまま", [{ v: 2 }, {}, { v: 1 }].toSorted((a, b) => a.v - b.v));
priority 昇順            [ 2, 4, 1, 3, 5 ]
複数キー pri→due         [ 2, 4, 3, 1, 5 ]
複数キー pri降→due→id    [ 5, 3, 1, 2, 4 ]
真偽値 未完了を先        [ 1, 3, 4, 2, 5 ]
真偽値 完了を先          [ 2, 5, 1, 3, 4 ]
null/undefined 末尾      [ '伊藤', '佐藤', '鈴木', 'null', 'undefined' ]
null/undefined 先頭      [ 'null', 'undefined', '伊藤', '佐藤', '鈴木' ]
プロパティ欠落のまま     [ { v: 2 }, {}, { v: 1 } ]

a.priority - b.priority || a.due.localeCompare(b.due) は「優先度が同じ(0)なら期限で比べる」という意味です。 第3キーに一意な id を置くと、並び順が常に1通りに決まり、テストや画面表示が安定します。

プロパティが欠けた要素があると a.v - b.v が NaN になり、まったく並ばないことがあります(上の最後の結果)。(a == null) - (b == null) を先に比べると、true - false = 1 で null 側が後ろに回ります。

指定した順番(優先順リスト)で並べる

const statusOrder = ["進行中", "未着手", "保留", "完了"];
const rank = new Map(statusOrder.map((s, i) => [s, i]));
const list = ["完了", "保留", "未着手", "進行中", "未着手", "その他"];
show("優先順リスト", list.toSorted((a, b) => (rank.get(a) ?? Infinity) - (rank.get(b) ?? Infinity)));
show("indexOf版(未知が先頭)", list.toSorted((a, b) => statusOrder.indexOf(a) - statusOrder.indexOf(b)));
優先順リスト            [ '進行中', '未着手', '未着手', '保留', '完了', 'その他' ]
indexOf版(未知が先頭)   [ 'その他', '進行中', '未着手', '未着手', '保留', '完了' ]

indexOf は見つからないと -1 を返すため、リストにない値が先頭に来ます。 Map で順位を引き、見つからなければ Infinity にすると末尾に回せます。Map は検索も速いので、件数が多いときにも向いています。

2次元配列・連想配列(オブジェクト)・Map を値で並べる

show("2次元 2列目", [["b", 2], ["a", 3], ["c", 1]].toSorted((a, b) => a[1] - b[1]));
const score = { alice: 80, bob: 95, carol: 70 };
show("Object.entries 値降順", Object.entries(score).toSorted(([, a], [, b]) => b - a));
show("Object.fromEntries", Object.fromEntries(Object.entries(score).toSorted(([, a], [, b]) => b - a)));
const m = new Map([["x", 3], ["y", 1], ["z", 2]]);
show("Map 値昇順", [...new Map([...m].toSorted((a, b) => a[1] - b[1]))]);
console.log(Object.fromEntries([["10", 1], ["2", 2], ["b", 0], ["a", 9]].toSorted(([, x], [, y]) => x - y)));
2次元 2列目            [ [ 'c', 1 ], [ 'b', 2 ], [ 'a', 3 ] ]
Object.entries 値降順  [ [ 'bob', 95 ], [ 'alice', 80 ], [ 'carol', 70 ] ]
Object.fromEntries     { bob: 95, alice: 80, carol: 70 }
Map 値昇順             [ [ 'y', 1 ], [ 'z', 2 ], [ 'x', 3 ] ]
{ '2': 2, '10': 1, b: 0, a: 9 }

オブジェクトに戻すと、"2" "10" のような整数に見えるキーは常に数値の小さい順で先頭に並びます(最後の結果)。並び順を保ちたいときは、配列か Map のまま扱います。オブジェクト操作の全体像は既存記事「JavaScriptオブジェクト操作まとめ」にあります。

汎用の sortBy 関数(複数キー・降順・null末尾をまとめて)

キーを並べるだけで複数キーのソートができる関数です。 文字列は日本語の Collator、null/undefined は末尾に回します。

const ja = new Intl.Collator("ja");
const by = (...keys) => (a, b) => {
  for (const k of keys) {
    const [key, dir] = typeof k === "string" ? [k, 1] : [k.key, k.desc ? -1 : 1];
    const x = a[key], y = b[key];
    if (x == null || y == null) { const d = (x == null) - (y == null); if (d) return d; continue; }
    const r = typeof x === "string" ? ja.compare(x, y) : x < y ? -1 : x > y ? 1 : 0;
    if (r) return r * dir;
  }
  return 0;
};
show("by(priority desc, due)", ids(tasks.toSorted(by({ key: "priority", desc: true }, "due"))));
show("by(assignee, id)", ids(tasks.toSorted(by("assignee", "id"))));
by(priority desc, due)   [ 5, 3, 1, 2, 4 ]
by(assignee, id)         [ 5, 1, 3, 2, 4 ]

(assignee は 伊藤=5、佐藤=1、鈴木=3、null=2、undefined=4 の順)


安定ソートを確認する(ES2019 以降は保証)

安定ソートとは、比較結果が 0(同じ)の要素どうしが、元の順番を保ったまま並ぶことです。 ES2019 で Array.prototype.sort の安定性が仕様で保証され、toSorted も同じです。

// s3.mjs
const members = [
  { name: "佐藤", team: "B" }, { name: "鈴木", team: "A" }, { name: "高橋", team: "B" },
  { name: "田中", team: "A" }, { name: "伊藤", team: "B" }, { name: "渡辺", team: "A" },
];
console.log(members.toSorted((a, b) => a.team.localeCompare(b.team)).map((m) => `${m.team}:${m.name}`).join(" "));
// 1万件・キー10種類で、同じキーの中で元の順(seq)が保たれているか
const big = Array.from({ length: 10000 }, (_, i) => ({ key: i % 10, seq: i }));
const sorted = big.toSorted((a, b) => a.key - b.key);
let stable = true;
for (let i = 1; i < sorted.length; i++)
  if (sorted[i - 1].key === sorted[i].key && sorted[i - 1].seq > sorted[i].seq) stable = false;
console.log("10000件 stable:", stable);
A:鈴木 A:田中 A:渡辺 B:佐藤 B:高橋 B:伊藤
10000件 stable: true

チーム A の中では「鈴木・田中・渡辺」と元の順が保たれています。 この性質を使うと、「まず名前で並べ、次にチームで並べる」と2回ソートしても、チーム内は名前順のまま残ります。ただし1回の比較関数で || をつなぐ方が意図が読みやすく、処理も1回で済みます。


Intl.Collator を使い回すと速い(5万件で実測)

日本語の文字列を5万件並べる時間を、比較関数の書き方ごとに測りました(Node.js 22.22.2、2コア、各5回の中央値)。

// bench.mjs
const kana = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをんアイウエオカキクケコガギグゲゴ";
let seed = 42; const rnd = () => (seed = (seed * 1103515245 + 12345) % 2 ** 31) / 2 ** 31;
const data = Array.from({ length: 50000 }, () =>
  Array.from({ length: 6 }, () => kana[Math.floor(rnd() * kana.length)]).join(""));
const median = (xs) => xs.toSorted((a, b) => a - b)[Math.floor(xs.length / 2)];
function bench(label, cmp) {
  const times = [];
  for (let i = 0; i < 5; i++) { const t = performance.now(); data.toSorted(cmp); times.push(performance.now() - t); }
  console.log(label.padEnd(44), median(times).toFixed(1), "ms");
}
const collator = new Intl.Collator("ja");
bench("a.localeCompare(b)(引数なし)", (a, b) => a.localeCompare(b));
bench('a.localeCompare(b, "ja")', (a, b) => a.localeCompare(b, "ja"));
bench('毎回 new Intl.Collator("ja").compare', (a, b) => new Intl.Collator("ja").compare(a, b));
bench("Collator を使い回す collator.compare", collator.compare);
bench("< > の単純比較(コードポイント順)", (a, b) => (a < b ? -1 : a > b ? 1 : 0));
比較関数 5万件の時間(中央値) 使い回し比
a.localeCompare(b)(ロケール指定なし) 115.6 ms 1.2倍
a.localeCompare(b, "ja") 235.9 ms 2.5倍
比較のたびに new Intl.Collator("ja") 4568.4 ms 48倍
Collator を作って使い回す 94.7 ms 1倍
< > の単純比較 40.6 ms 0.4倍

Intl.Collator は一度作ってから collator.compare を渡すのが最速でした。 localeCompare(b, "ja") のようにロケールやオプションを渡すと、内部で比較のたびに準備が入るぶん時間がかかります。モジュールの先頭で const ja = new Intl.Collator("ja") と作っておくのがおすすめです。

事前に変換する(Schwartzian transform)

比較関数の中で重い処理(new Date()、toLowerCase()、読み仮名の生成など)をすると、比較の回数(n log n 回)だけ繰り返されます。 先にキーを1回だけ計算しておくと効率的です。

const logs = [{ at: "2026/10/1 9:00" }, { at: "2026/2/20 18:30" }, { at: "2026/1/5 7:15" }];
const sortedLogs = logs
  .map((x) => ({ x, key: new Date(x.at).getTime() }))  // キーを1回だけ計算
  .toSorted((a, b) => a.key - b.key)
  .map(({ x }) => x);
console.log(sortedLogs.map((l) => l.at));
// 実行結果: [ '2026/1/5 7:15', '2026/2/20 18:30', '2026/10/1 9:00' ]

React の state を並べ替えるときの注意

React は state が「別のオブジェクトになったか」で再描画を判断するため、sort() で元の配列を書き換えると変更が伝わりません。 state の並べ替えは toSorted() を使います。以下は書き方の例です(このコードはブラウザ上の React では実行していません。Object.is による判定は React の公式ドキュメントの説明に基づきます)。

import { useMemo, useState } from "react";

const ja = new Intl.Collator("ja");  // コンポーネントの外で1回だけ作る

export function TaskList({ tasks }) {
  const [sortKey, setSortKey] = useState("due");

  // 表示用に並べ替える:元の props は変えない
  const sorted = useMemo(
    () => tasks.toSorted((a, b) =>
      sortKey === "title" ? ja.compare(a.title, b.title) : a.due.localeCompare(b.due)),
    [tasks, sortKey]
  );

  return (
    <>
      <select value={sortKey} onChange={(e) => setSortKey(e.target.value)}>
        <option value="due">期限順</option>
        <option value="title">タイトル順</option>
      </select>
      <ul>{sorted.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
    </>
  );
}
書き方 起きること おすすめ
items.sort(cmp); setItems(items) 同じ配列なので再描画されないことがある setItems(items.toSorted(cmp))
props.tasks.sort(cmp) を描画中に実行 親の配列まで書き換わる toSorted + useMemo
key={index} で並べ替える 並べ替え後に入力欄の内容がずれる key={t.id} など一意な値
並べ替えた結果も state に入れる 元データと二重管理になる 元データ+並べ替え条件だけ state にし、表示時に計算
new Intl.Collator を描画のたびに作る 毎回作成コストがかかる コンポーネントの外で作る

「元データ」と「並べ替えの条件」だけを state に持ち、並べ替えた配列は描画時に toSorted で作る と、データの更新と並び順の変更が自然にそろいます。


よくある質問

Q. sort と toSorted はどちらを使えばよいですか? A. 元の配列を残したいとき(React の state、関数の引数、共有している配列)は toSorted、その場で作った一時的な配列なら sort で十分です。迷ったら toSorted を選ぶと、意図しない書き換えを防げます。

Q. 降順にするには sort してから reverse すればよいですか? A. 結果は降順になりますが、比較が0の要素の順番まで逆になります(安定ソートの性質が逆向きになる)。比較関数で (a, b) => b - a のように書く方が、1回の処理で済み、同じ値の要素は元の順を保ちます。

Q. 日本語の漢字を五十音順に並べるには? A. Intl.Collator("ja") は漢字の読みを判定しないため、読み仮名のデータを用意して、その読みで比較します。データベースから取得するなら、読み仮名の列で ORDER BY してから受け取る方法もあります。

Q. ソートの計算量はどのくらいですか? A. 仕様ではアルゴリズムは決められていませんが、V8(Node.js・Chrome)は TimSort を使っており、比較回数はおおむね n log n です。数万件程度なら比較関数を軽くすれば数十〜百ミリ秒で終わることを上の実測で確認できます。


まとめ

  • sort() は元の配列を書き換え、toSorted() は新しい配列を返す(ES2023、Node 20 以降・主要ブラウザで利用可)
  • 引数なし sort() は文字列順なので、数値は必ず (a, b) => a - b を渡す
  • 文字列は localeCompare、日本語やファイル名の自然順は Intl.Collator("ja", { numeric: true })、漢字は読み仮名で比べる
  • 複数キーは || でつなぐ。最後に一意な id を置くと並びが1通りに決まる
  • null・NaN・リスト外の値は先に判定して末尾へ。indexOf より Map + Infinity が扱いやすい
  • 安定ソートは ES2019 から保証され、1万件でも元の順が保たれることを確認
  • Intl.Collator は使い回すと最速(5万件で 94.7ms、毎回作ると 4568.4ms)。React の state は toSorted + useMemo で

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript配列ソートの使い方|数値・日付・複数キーとtoSorted例」

株式会社RJCでは、配列操作やソートの考え方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?