JavaScriptのsort()のデフォルト順序について考えてた
- コードポイントの昇順じゃなくて、文字コード (UTF-16コード) の昇順だと思う。
- もっと細かく言うと、
undefined
以外の値をString
型にキャストして、比較演算子で比較した結果の昇順だと思う。
Array
の sort()
を使うと、配列の内容をソートできます。このメソッドは、引数に比較用の関数を渡すとソート順をカスタマイズできますが、何も渡さないとデフォルトの順序でソートされます。
console.log([1, 0, 11, 10, '01', '00'].sort());
// -> [0, "00", "01", 1, 10, 11]
デフォルトの順序では、数値でも文字列として比較されてるのが特徴です。これは『Unicodeコードポイント順になる』という表現で解説されてるサイトが多い気がします。
配列の要素をin placeでソートします。このソートは stable ではありません(訳注:同じ序列を持つ値の順番が保証されません)。 デフォルトではUnicodeコードポイントの昇順にソートされます。
が、コードポイントの昇順と言われると、個人的には↓の結果が引っかかります。
// コードポイントの昇順だと _→🍅→🍆→🐤
console.log('_'.codePointAt(0)); // -> 65343
console.log('🍅'.codePointAt(0)); // -> 127813
console.log('🍆'.codePointAt(0)); // -> 127814
console.log('🐤'.codePointAt(0)); // -> 128036
// UTF-16コードの昇順だと 🍅→🍆→🐤→_
console.log('🍅'.charCodeAt(0), '🍅'.charCodeAt(1)); // -> 55356 57157
console.log('🍆'.charCodeAt(0), '🍆'.charCodeAt(1)); // -> 55356 57158
console.log('🐤'.charCodeAt(0), '🐤'.charCodeAt(1)); // -> 55357 56356
console.log('_'.charCodeAt(0)); // -> 65343
// sortのデフォルト順は…
console.log(['_','🐤','🍆','🍅'].sort());
// -> ["🍅", "🍆", "🐤", "_"]
これは単に文字コード順じゃないのか??
内部的にUTF-16で処理されてる言語だから、サロゲートペア使ってる文字を渡しても1バイト目で比較されるのは、そういうものだと思うのですが。
仕様ではどういうことになってるんだろ? と思って仕様書見に行ってみたら、やはりコードポイント順とは書かれてませんでした。英語読めないのですが、comparefn
が undefined
のときは ToString
して比較するって書いてある気がします。
ECMAScript® 2018 Language Specification
ToString
して比較されてることを確認するために、気持ち悪い配列を sort
してみます。
console.log([
null,
'nall',
undefined,
'undafined',
Infinity,
'Infanity',
'',
'_',
'🐤',
'🍆',
'🍅',
'NuN',
NaN, // .toString() -> NaN
[], // .toString() -> (空文字)
'1,1',
[1,0], // .toString() -> 1,0
[0,1], // .toString() -> 0,1
'[object Object]_',
{}, // .toString() -> [object Object]
function a01() {}, // .toString() -> function a01() {}
function a1() {}, // .toString() -> function a1() {}
function a0() {}, // .toString() -> function a0() {}
'function a00() {}',
].sort());
/* -> [
0: ""
1: []
2: (2) [0, 1]
3: (2) [1, 0]
4: "1,1"
5: "Infanity"
6: Infinity
7: NaN
8: "NuN"
9: {}
10: "[object Object]_"
11: ƒ a0()
12: "function a00() {}"
13: ƒ a01()
14: ƒ a1()
15: "nall"
16: null
17: "undafined"
18: "🍅"
19: "🍆"
20: "🐤"
21: "_"
22: undefined
] */
確かに toString
後に比較してソートされたような結果になってました。しかし null
が『null』という文字列として比較されてるっぽいのは、初めて知りました。undefined
はそうではないみたいなのですが。
調べてみたところ、null
に toString
は無いけど、String(null)
の結果は文字列の null
になるようです。でも、それなら String(undefined)
も文字列の undefined
になります。が、undefined
が文字列として比較されてたらすごく嫌な気がするので、そういうものかもしれないです。
この結果をテストケースとして、sort
のデフォルトの比較関数を再現してみたのが次のコードです。
/**
* Array.prototype.sort のデフォルト比較関数を再現
* @param {unknown} x
* @param {unknown} y
* @return {Number} x < y なら -1, x > y なら 1, x == y なら 0
*/
const compareFn = (x, y) => {
if (x === undefined && y === undefined) {
return 0;
} else if (x === undefined) {
return -1;
} else if (y === undefined) {
return 1;
}
const strX = String(x);
const strY = String(y);
if (strX < strY) {
return -1;
} else if (strX > strY) {
return 1;
}
return 0;
};
// さっきと同じ配列
console.log([
null, 'nall', undefined, 'undafined', Infinity, 'Infanity', '', '_', '🐤', '🍆', '🍅',
'NuN', NaN, [], '1,1', [1,0], [0,1], '[object Object]_', {},
function a01() {}, function a1() {}, function a0() {}, 'function a00() {}',
].sort(compareFn));
/* -> [
0: ""
1: []
2: (2) [0, 1]
3: (2) [1, 0]
4: "1,1"
5: "Infanity"
6: Infinity
7: NaN
8: "NuN"
9: {}
10: "[object Object]_"
11: ƒ a0()
12: "function a00() {}"
13: ƒ a01()
14: ƒ a1()
15: "nall"
16: null
17: "undafined"
18: "🍅"
19: "🍆"
20: "🐤"
21: "_"
22: undefined
] */
こうして見ると、思ったよりシンプルです。
というわけでこの記事の結論では、sort()
のデフォルト順は、undefined
以外の値を String
型にキャストして比較演算子で比較した結果の昇順です。一言で言うと、文字コード (UTF-16コード) の昇順です。