JavaScriptのsort()で数値の並びがおかしくなる理由:既定の文字列比較と比較関数のルール
[10, 9, 1, 100].sort()は[1, 10, 100, 9]を返します。既定のソートの文字列比較、比較関数の戻り値と一貫性の条件、元の配列の変更、undefinedと空きスロット、toSorted()をECMA-262の仕様に沿って整理しました。
[10, 9, 1, 100].sort()の結果は[1, 10, 100, 9]です。比較関数を渡さないと、Array.prototype.sort()は要素を文字列に変換して比べるためです。この記事は2026年10月11日に確認したECMA-262の編集者草案(ECMAScript 2027)に基づいています。
既定のソートは文字列比較
比較関数がない場合、仕様のCompareArrayElementsは2つの値をToStringで文字列にしてからIsLessThanで比べます。文字列の比較はUTF-16のコードユニット値を先頭から1つずつ比べ、一方がもう一方の接頭辞であれば短いほうを前にします。"1"は"10"の接頭辞で、"100"と"9"は1文字目で1(U+0031)が9(U+0039)より小さいので、上の結果になります。
[10, 9, 1, 100].sort(); // [1, 10, 100, 9]
[-1, -2, -10].sort(); // [-1, -10, -2]
["b", "a", "B", "A"].sort(); // ["A", "B", "a", "b"]
["e", "é", "f"].sort(); // ["e", "f", "é"]
大文字(U+0041から)が小文字(U+0061から)より前に来て、é(U+00E9)はf(U+0066)の後ろに回ります。辞書順ではなくコードユニット順です。コードユニットは文字数がツールごとに違う理由で扱いました。
比較関数の戻り値
数値は比較関数を渡してソートします。
[10, 9, 1, 100].sort((a, b) => a - b); // [1, 9, 10, 100]
[10, 9, 1, 100].sort((a, b) => b - a); // [100, 10, 9, 1]
仕様では、comparator(x, y)はxがyより小さければ負の数、大きければ正の数、それ以外は0を返すものとされています。戻り値はToNumberで数値に変換され、符号だけが使われるので、-1と1の代わりに-37や5を返しても構いません。NaNは0として扱われます。
文字列のキーは引き算で比べられないので、符号を自分で返します。
users.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
BigIntの配列にa - bの比較関数を使うとTypeErrorになります。BigInt同士の引き算自体は問題ありませんが、その結果、比較関数がBigIntを返し、CompareArrayElementsがその戻り値にToNumberを適用します。ToNumberはBigIntを受け取るとTypeErrorを投げます。
[3n, 1n, 2n].sort((a, b) => a - b); // TypeError
[3n, 1n, 2n].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); // [1n, 2n, 3n]
比較関数の位置に関数以外の値を渡してもTypeErrorです。[1, 2].sort(null)がその例で、undefinedだけが比較関数なしとして扱われます。
一貫した比較関数
仕様は比較関数に「一貫した比較関数(consistent comparator)」であることを求めています。同じ2つの値には常に同じ数値(NaN以外)を返し、ソート中のオブジェクトやそのプロトタイプチェーン上のどのオブジェクトも変更してはいけません。さらに「等しい」という判定は反射律・対称律・推移律を、「小さい」「大きい」という判定は推移律を満たす必要があります。これが崩れると、ソート順は実装定義(implementation-defined)になります。エンジンや入力によって結果が変わっても仕様違反ではない、ということです。
よく見かける誤りが、真偽値を返す比較関数です。
arr.sort((a, b) => a > b); // 誤った比較関数
trueは1、falseは0になります。aがbより小さいときも0、つまり「等しい」が返るので、(1, 2)は等しい、(2, 1)は大きいと判定され、対称律が崩れます。結果は実装定義で、ある入力でたまたま正しく見えても保証はありません。
元の配列が変わる
sort()は配列をその場でソートし、同じオブジェクトを返します。
const a = [3, 1, 2];
const b = a.sort((x, y) => x - y);
b === a; // true
a; // [1, 2, 3]
元の配列を残したいときはtoSorted()を使います。比較関数のルールと比べ方はsort()と同じで、結果を新しい配列として返します。
const a = [3, 1, 2];
const b = a.toSorted((x, y) => x - y);
a; // [3, 1, 2]
b; // [1, 2, 3]
undefinedと空きスロット
undefinedは比較関数を通らず、常に後ろに回ります。CompareArrayElementsが比較関数を呼ぶ前にundefinedを処理するため、比較関数がundefinedを引数として受け取ることはありません。
空きスロット(hole)は、lengthより小さいのにプロパティが存在しないインデックスです。sort()は空きスロットをソート対象から外し、ソートした値を先頭から書き込んだあと、残りのインデックスを削除して空きスロットの数を保ちます。そのため空きスロットはundefinedよりも後ろに残ります。
const a = [3, undefined, , 1];
a.sort((x, y) => x - y); // [1, 3, undefined, 空きスロット]
3 in a; // false
toSorted()は空きスロットをundefinedとして読むので、結果に空きスロットはありません。
[3, undefined, , 1].toSorted((x, y) => x - y); // [1, 3, undefined, undefined]
上の例は、プロトタイプから継承したインデックスのプロパティがない通常の配列を前提にしています。sort()は各インデックスをHasPropertyで確認し、HasPropertyはプロトタイプチェーンまでたどります。そのため、たとえばArray.prototype[2]があれば、インデックス2は空きスロットとして飛ばされず、その値が読み込まれてソートされます。
参考
- ECMA-262 編集者草案、23.1.3.30 Array.prototype.sort、23.1.3.30.1 SortIndexedProperties、23.1.3.30.2 CompareArrayElements、23.1.3.34 Array.prototype.toSorted
- ECMA-262 編集者草案、7.1.4 ToNumber、7.2.12 IsLessThan
- ECMA-262 編集者草案、10.4.2 Array Exotic Objects
節番号は確認時点の編集者草案に基づきます。リンクと内容は2026年10月11日に確認しました。