カスタム sort とは何か
カスタム sort は「比較関数(compareFn)を自分で定義して、並べ替えの“ルール”をコントロールする」ことです。ここが重要です:compareFn は負・ゼロ・正の“数値”を返します(true/false は禁止)。負なら a が先、正なら b が先、0 なら順序を保つ。数値・文字列・日付・複合条件・欠損(null/undefined)など、現実のデータに合わせて“順序の定義”を明示するのが安全です。
比較関数の基本(昇順・降順の設計)
数値の昇順・降順
const nums = [10, 2, 5];
// 昇順
nums.slice().sort((a, b) => a - b); // [2, 5, 10]
// 降順
nums.slice().sort((a, b) => b - a); // [10, 5, 2]
JavaScriptここが重要です:sort は“破壊的”。共有配列は必ず slice() やスプレッドでコピーしてから並べ替えましょう。
文字列を“人に自然な順序”で
const names = ["ä", "a", "Z", "z"];
const collator = new Intl.Collator("ja", { sensitivity: "base" });
names.slice().sort((a, b) => collator.compare(a, b));
JavaScriptここが重要です:localeCompare でも可、頻繁に比較するなら Intl.Collator を使い回す方が高速で安定です。
日付の並べ替え
const rows = [{d:"2025-12-01"}, {d:"2024-01-01"}];
rows.slice().sort((a, b) => new Date(a.d) - new Date(b.d)); // 古い→新しい
JavaScriptここが重要です:文字列日付は Date に変換して“数値として比較”。大量なら事前にタイムスタンプへ正規化しておくと速いです。
複合キーのソート(第一キー→第二キー)
価格昇順、同価格は名前の辞書順
const products = [
{ name: "B", price: 200 },
{ name: "A", price: 200 },
{ name: "C", price: 150 }
];
const collator = new Intl.Collator("ja", { sensitivity: "base" });
const sorted = products.slice().sort((p, q) => {
const byPrice = p.price - q.price;
if (byPrice !== 0) return byPrice;
return collator.compare(p.name, q.name); // 同点は名前で
});
JavaScriptここが重要です:比較は“推移的”に。第一キーで決まらなければ次のキーへ、という階段構造にすると不定な結果を避けられます。
多数キーは関数合成で読みやすく
const collator = new Intl.Collator("ja", { sensitivity: "base" });
const by = {
priceAsc: (p, q) => p.price - q.price,
nameAsc: (p, q) => collator.compare(p.name ?? "", q.name ?? "")
};
const cmp = (a, b) => by.priceAsc(a, b) || by.nameAsc(a, b);
const out = products.slice().sort(cmp);
JavaScriptここが重要です:小さな比較関数を“OR”で合成すると、優先順が明確で保守しやすくなります。
欠損値・異常値の扱い(null/undefined/NaN を明示)
欠損は“どこへ置くか”をルール化
const rows = [{v:3},{v:null},{v:1},{v:undefined}];
const sorted = rows.slice().sort((a, b) => {
const an = Number(a.v), bn = Number(b.v);
const aBad = !Number.isFinite(an);
const bBad = !Number.isFinite(bn);
if (aBad && bBad) return 0;
if (aBad) return 1; // 欠損は末尾へ
if (bBad) return -1;
return an - bn;
});
JavaScriptここが重要です:欠損や NaN が混じる現実データでは、“順序の定義”を比較関数に必ず書く。これで結果が安定します。
文字列の欠損も同様に
const collator = new Intl.Collator("ja", { sensitivity: "base" });
const cmpName = (a, b) => {
const as = a?.name ?? "", bs = b?.name ?? "";
if (!as && !bs) return 0;
if (!as) return 1; // 空は後ろ
if (!bs) return -1;
return collator.compare(as, bs);
};
JavaScriptカスタム順序(ランク付け・優先度の定義)
任意の順序を配列や Map で定義
const order = ["low", "medium", "high"];
const rank = new Map(order.map((v, i) => [v, i]));
const tasks = [
{ prio: "high" },
{ prio: "low" },
{ prio: "medium" },
{ prio: "unknown" }
];
const sorted = tasks.slice().sort((a, b) => {
const ra = rank.get(a.prio) ?? Infinity; // 未知は最後
const rb = rank.get(b.prio) ?? Infinity;
return ra - rb;
});
JavaScriptここが重要です:カスタム順序は“スコア化”して数値比較に落とすと、シンプルで高速になります。
安全・高速のための“前計算”(Schwartzian transform)
const items = ["ä", "a", "z", "Z"];
const collator = new Intl.Collator("de", { sensitivity: "base" });
const keyed = items.map(s => ({ s, k: collator.compare(s, s) && s })); // 例: 実用なら別のキー化
// 実際はキーを事前計算する:length, lowerCase, parsedDate など
const sorted = items
.map(s => ({ s, key: s.toLowerCase() }))
.sort((a, b) => a.key.localeCompare(b.key))
.map(x => x.s);
JavaScriptここが重要です:高コストな“キー(比較用値)”は事前計算してから 1 回の sort に使うと速いです。
安定ソート・非推移性・破壊性の注意
安定ソート前提で“同点の相対順”を保つ
現代の JS 実装は安定ソートです。同点(0)の要素は元の相対順を保つ前提で書けます。ただし比較関数が非推移的だと、安定性が崩れ不定な並びになります。
非推移性の罠を避ける
「A < B、B < C だが A < C が成り立たない」ような比較はバグの温床です。複合条件は“第一→第二→第三”の直列に、各段が推移的になるよう設計してください。
破壊的であることを忘れない
sort は元配列を書き換えます。UI 状態・共有配列は const sorted = [...arr].sort(cmp) の“非破壊パターン”を徹底しましょう。
実践レシピ(現場でよく使う並べ替え)
「在庫ありを先、在庫なしを後」+価格昇順
const collator = new Intl.Collator("ja", { sensitivity: "base" });
const cmp = (a, b) => {
if (!!a.stock !== !!b.stock) return b.stock - a.stock; // true を先に
const byPrice = a.price - b.price;
if (byPrice !== 0) return byPrice;
return collator.compare(a.name ?? "", b.name ?? "");
};
const sorted = products.slice().sort(cmp);
JavaScript「最新更新日時が新しい順」+「同時刻はタイトル順」
const collator = new Intl.Collator("ja", { sensitivity: "base" });
const cmp = (a, b) => {
const ta = Date.parse(a.updatedAt), tb = Date.parse(b.updatedAt);
const byTime = tb - ta; // 新しい順
if (byTime !== 0) return byTime;
return collator.compare(a.title ?? "", b.title ?? "");
};
JavaScript「カテゴリごとの順序」→「カテゴリ内は名前順」
const order = ["fruit", "veg", "other"];
const rank = new Map(order.map((v, i) => [v, i]));
const collator = new Intl.Collator("ja", { sensitivity: "base" });
const cmp = (a, b) => {
const ra = rank.get(a.cat) ?? Infinity;
const rb = rank.get(b.cat) ?? Infinity;
if (ra !== rb) return ra - rb;
return collator.compare(a.name ?? "", b.name ?? "");
};
JavaScriptパフォーマンス指針(コストを抑えて安定に)
- 前処理で正規化する: 型変換、小文字化、日付のパース、欠損の置換を“比較前”に済ませると、比較が軽くなり安定します。
- Collator は再利用:
new Intl.Collator()は高コスト。外で作って比較関数内で使い回します。 - 必要部分だけ並べる: 上位 N 件が欲しいなら、
sort+slice(N)か、ヒープ/Array.prototype.toSortedがある環境なら非破壊の選択肢も検討。 - キー化(Schwartzian)で 1 回に: 高コストのキー計算を事前に行い、
sortはキー比較だけに集中させる。
まとめ
カスタム sort の核心は「比較関数で順序を明示する」ことです。数値は引き算、文字列は localeCompare/Intl.Collator、日付は Date/タイムスタンプ、複合条件は“第一→第二”の直列、欠損は“どこへ置くか”を定義。sort は破壊的なのでコピーしてから並べる癖をつけ、非推移性を避け、前処理で正規化・キー化してコストを下げる。これらを押さえれば、初心者でも現場仕様に耐える“意図通りで壊れにくい並べ替えロジック”を短く正確に書けます。
