JavaScriptである程度のコードを書いたことがある人なら、配列を.sort()メソッドでソートしたことがある人も多いかと思います。ただ、調べれば調べるほど、複雑な事情が生じてきます。
まずは導入として、以下のコードはどのような配列を生成するか、予想してみてください。
[, null, undefined, 'jkr_2255', 'qiita'].sort()
これがピタリと正解できた場合には、この記事を読む必要はないかもしれません。なお、この文書はECMA-262 第6版(いわゆる「ES6」)を前提としています。数字だけのリンクは、仕様書の各セクションにつながっています。
ソート関数を指定しない場合
まずは、ソート関数を指定しない場合の挙動について考えていきます。この場合、「中身を文字列化して順に並べ替える」ということはよく知られているかと思います(数値が順にならなかった経験をしたことがある人もいるかと思います)。この中身を掘り下げてみます。
ToString内部操作
よく、値を文字列化するのに、String(value)やvalue + ''のような手法が使われますが、標準で行われるソートの比較時に使われるのは、内部名がToStringとなっている操作です(22.1.3.24.1)。
このToStringは、以下のような値を返します(7.1.12)。なお、説明の都合上、一部の型のみ書いています。
-
null→"null" - ブール値→
"true"または"false" - シンボル→**
TypeErrorを起こす**
というわけで、シンボルの入った配列を比較関数なしで.sort()しようとすると、TypeErrorになってしまいます。また、nullは文字列の"null"と同様の位置に並ぶこととなります。
ちなみに、value + ''は左辺についてToPrimitive、それからToStringが行われる一方(12.7.3.1)、String(value)はシンボルだけ特殊なハンドリングが入っています(21.1.1)。標準関数でシンプルにToStringと同じ結果を得ようとすれば、''.concat(value)という方法があります。
※ ここについては、@think49 さんからの指摘を元に修正しました。
とはいえ、このあたりはソート関数を適宜定義すればどうにかなる問題です。
ソート関数を指定しても起きる問題…undefined、値なし
上のToStringのリストからundefinedを省略しましたが、undefinedは比較関数で評価されません。どのような比較関数を使って.sort()を動かそうとも、undefinedは最後に来ることとなります(22.1.3.24.1)。
そして、値のないプロパティ(Object.prototype.hasOwnProperty.call(arr, index) === falseとなるもの)があった場合、undefinedよりさらにあとに来ることとなります。この挙動も、変更できません(ふつうにコードを書いていて、「配列の途中のインデックスが抜けたままになっている」ものに遭遇する機会も、あまり多くはないかとは思いますが)。
実装依存の挙動
Array.prototype.sortは配列以外にも使える汎用的なメソッドとなっていますが、以下のような場合はソート順が実装依存(≒どうなるかわからない)となります。便宜上、以下の文ではソート対象となるオブジェクトを「配列」としています。
- 渡した比較関数が一貫しない(反射律・対称律・推移律を満たさない、あるいは配列自身あるいはそのプロトタイプを書き換える)場合
- 疎な配列(一部の値がない配列)について、
Object.preventExtensionsがかかっている、あるいはソート範囲内のプロパティで、configurable: falseとなっているものがある場合 -
Proxyオブジェクトなど、内部挙動が特殊なオブジェクトをthisとして.sort()を実行した場合 - ソート範囲内のプロパティに
writable: falseとなったプロパティ、あるいはアクセサプロパティがある場合 - デフォルトの比較関数を使っていて、
-
ToStringの結果が一貫しないオブジェクトが配列に入っている場合 -
ToStringを実行すると、配列自身あるいはそのプロトタイプを書き換えるようなオブジェクトが配列に入っている場合
-
最初の答え
["jkr_2255", null, "qiita", undefined, /* 空き */]