目次
sort()とlocaleCompare()はバグの原因になりやすい
JavaScriptで配列を並び替えるとき、多くの人が真っ先に思い浮かべるのがsort()や localeCompare()でしょう。
しかし、これらを使うとバグの原因になりやすいことは、あまり知られていません。
sort()をそのまま使う危険性
デフォルトのsort()は数値を数値として扱いません。
const arr = [10, 2, 1]
arr.sort()
console.log(arr)
// [1, 10, 2]内部的にはすべて文字列に変換され、Unicode順(辞書順)で比較されます。
この配列をUnicodeコードポイント(10進表記)に変換すると以下のようになります。
const items = [10, 2, 1]
const unicodeDecimal = items.map(n =>
[...String(n)].map(ch => ch.codePointAt(0))
)
console.log(unicodeDecimal)
// [[49, 48], [50], [49]] ( [10, 2, 1] )コードの通り、'10' は '2' より前 (49 < 50) なので、sort() だと [1, 10, 2] になります。
これでは数値の昇順にはならないのは当然ですね。
初心者だけでなく中級者でもうっかり踏みがちな挙動になります。
比較関数に a - b を使う落とし穴
前述のような並び替えを避けるために、sort()の比較関数に「a - b」を使用して、Unicode順ではなく数値順で並び替えているケースをよく見かけます。
const arr = [10, 2, 1]
arr.sort((a, b) => a - b)
console.log(arr)
// [1, 2, 10]sort()の比較関数に「a - b」を使用した並び替えは、すべてが数値(または数値文字列)なら問題ありません。
しかし、実際のWebサイトの配列がこんなに単純とは限りません。
下記のようにすべてが数値または数値文字列ではなく、「A2」のような文字列も含まれることがあります。
このケースだと「'10' - 'B1'」などが「NaN」となるため、正しく並び替えができません。
const arr = ['10', '1', 'B1', 'A2', '3']
arr.sort((a, b) => a - b)
console.log(arr)
// ['1', '10', 'B1', 'A2', '3']sort()の比較関数に「a - b」を使用した場合は
- 負の値 → aが前 ( '1' - '3' は -2 で負の値なので '1' が前)
- 正の値 → bが前 ( '10' - '1' は 9 で正の値なので '1' が前)
- 0 → 同じ (aとbはそのまま)
という数値を返す前提で動いているため、NaNだと正しく並び替えができません。
この問題を回避するには、sort()の比較関数に「a - b」ではなく、localeCompare()を使用します。
localeCompare()とは
localeCompare()は文字列を「言語・地域のルール」に基づいて比較するためのメソッドです。
Unicodeコードポイント順(辞書順)ではなく、人間が期待する並び順に近づけるための仕組みだと思ってください。
console.log('a'.localeCompare('b'))
// -1
console.log('x'.localeCompare('f'))
// 1
console.log('Z'.localeCompare('Z'))
// 0戻り値は次の意味を持ちます。
| 戻り値 | 意味 |
|---|---|
| 負の数 | 左の文字列が小さい |
| 正の数 | 左の文字列が大きい |
| 0 | 同じ |
localeCompareを使用する際はカタカナなども入る可能性を考慮して言語に「ja-JP」を指定して、数値の並べ替えにするために { numeric: true } のオプションを追加すると良いです。
sort()と併用すると以下のようなコードとなり、人間が期待する並び順になります。
const arr = ['10', '1', 'データ2', 'B1', 'データ1', 'A2', '3']
arr.sort((a, b) => a.localeCompare(b, 'ja-JP', {
numeric: true
}))
console.log(arr)
// ['1', '3', '10', 'A2', 'B1', 'データ1', 'データ2']sort()は破壊的メソッド
sort()は破壊的メソッドなので、元の配列を変更してしまいます。
破壊的メソッドだとReactやVueなどとの相性が非常に悪く、バグの温床になりやすいです。
const arr = ['10', '1', '3']
const sortedArr = arr.sort((a, b) => a.localeCompare(b, 'ja-JP', {
numeric: true
}))
console.log(arr)
// ['1', '3', '10']
console.log(sortedArr)
// ['1', '3', '10']そのため、並び替えには元の配列を変更する「sort()」ではなく、元の配列を変更しない「toSorted()」を使用するのが好ましいです。
const arr = ['10', '1', '3']
const sortedArr = arr.toSorted((a, b) => a.localeCompare(b, 'ja-JP', {
numeric: true
}))
console.log(arr)
// ['10', '1', '3']
console.log(sortedArr)
// ['1', '3', '10']元の変更を防ぐために「[…arr].sort」のような書き方をしているケースもありますが、冗長な書き方です。
[…arr] にし忘れて arr のまま書いてバグを発生させるリスクもあるので、以下のような代入のコードで使用するのは非推奨です。
const arr = ['10', '1', '3']
const sortedArr = [...arr].sort((a, b) => a.localeCompare(b, 'ja-JP', {
numeric: true
}))
console.log(arr)
// ['10', '1', '3']
console.log(sortedArr)
// ['1', '3', '10']localeCompare()はパフォーマンスが悪い
localeCompare()は毎回実行するたびにロケール処理を行うため、要素数が多いとパフォーマンスが悪く、処理が遅くなります。
また、何箇所かで使用していると、numeric: true の付け忘れが発生して、意図した結果にならないバグを発生させてしまう可能性があります。
const arr = ['10', '1', '3']
// numeric: true を付け忘れ
const sortedArr = arr.toSorted((a, b) => a.localeCompare(b, 'ja-JP'))
console.log(arr)
// ['10', '1', '3']
console.log(sortedArr)
// ['1', '10', '3']Intl.Collatorでロケール処理を共通化
Intl.Collatorを使用すればロケール処理を共通化できて、ロケール処理は1回だけなので複数箇所でlocaleCompare()を使用するよりも処理が早くなります。
そのため、特に理由がなければlocaleCompare()ではなく、Intl.Collatorを使用することをオススメします。
コードの可読性や保守性も良くなります。
const collator = new Intl.Collator('ja-JP', {
numeric: true
})
const arr1 = ['10', '1', '3']
const arr2 = ['5', '2', 'データ2', 'B1', 'データ1', 'A2', '7']
const sortedArr1 = arr1.toSorted(collator.compare)
const sortedArr2 = arr2.toSorted(collator.compare)
console.log(sortedArr1)
// ['1', '3', '10']
console.log(sortedArr2)
// ['2', '5', '7', 'A2', 'B1', 'データ1', 'データ2']sensitivity: 'base' について
Intl.Collator (localeCompare)でオプションに「sensitivity: 'base'」を追加すると、大文字小文字・濁点などを無視します。
Webサイトの仕様によっては並び替えで大文字小文字・濁点などを無視するケースがあるので注意が必要です。
const collatorVariant = new Intl.Collator('ja-JP', {
numeric: true,
// デフォルトは「sensitivity: 'variant'」
// sensitivity: 'variant'
})
const collatorBase = new Intl.Collator('ja-JP', {
numeric: true,
sensitivity: 'base'
})
const arr = ['Apple', 'apple', 'APPLE']
const sortedArr1 = arr.toSorted(collatorVariant.compare)
const sortedArr2 = arr.toSorted(collatorBase.compare)
console.log(sortedArr1)
// sensitivity: 'variant'
// ['apple', 'Apple', 'APPLE']
console.log(sortedArr2)
// sensitivity: 'base'
// ['Apple', 'apple', 'APPLE']const collatorVariant = new Intl.Collator('ja-JP', {
numeric: true,
// デフォルトは「sensitivity: 'variant'」
// sensitivity: 'variant'
})
const collatorBase = new Intl.Collator('ja-JP', {
numeric: true,
sensitivity: 'base'
})
const arr = ['ごばん', 'こはん', 'ごはん']
const sortedArr1 = arr.toSorted(collatorVariant.compare)
const sortedArr2 = arr.toSorted(collatorBase.compare)
console.log(sortedArr1)
// sensitivity: 'variant'
// ['こはん', 'ごはん', 'ごばん']
console.log(sortedArr2)
// sensitivity: 'base'
// ['ごはん', 'こはん', 'ごばん']ちなみにChatGPTなどの一部生成AIでは「sensitivity: 'base'」を追加したコードを生成することがありますが、Intl.Collator (localeCompare)のデフォルトは「sensitivity: 'variant'」です。
前述のように「sensitivity: 'base'」を追加して区別せずに並び替える処理だと、人間が見た場合は順序に違和感を覚える人が多いので、特に理由がなければ「sensitivity: 'base'」を付ける必要はないです。
配列が期待通りの並び替えか確認する方法
配列が期待通りの並び替えか確認するには、「期待する順序の配列」と「配列をシャッフルしたものを並び替えた配列」が同じか確認します。
コード内に並び替えの処理を入れる際は以下のようなチェックが必須です。
VitestやPlaywrightを使用している場合はテストコードに組み込むことで、将来の仕様変更やリファクタリングによる並び順の崩れを自動で検知できます。
function shuffle(array) {
const result = [...array]
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[result[i], result[j]] = [result[j], result[i]]
}
return result
}
const collator = new Intl.Collator('ja-JP', {
numeric: true
})
// 期待する順序の配列
const arr1 = ['10', '3', '1']
const arr2 = ['1', '3', '10', 'A-2', 'A-10', 'apple', 'apple', 'Apple', 'APPLE', 'banana', 'File1', 'file2', 'file10', 'Zebra', 'あい', 'あい', 'アイ', 'アイ', 'アップル', 'いちご', 'さくら', 'サクラ', 'ファイル2', 'ファイル10', '大阪', '東京']
// シャッフルした配列
const shuffledArr1 = shuffle(arr1)
const shuffledArr2 = shuffle(arr2)
// 並び替えた配列
const sortedArr1 = arr1.toSorted(collator.compare)
const sortedArr2 = arr2.toSorted(collator.compare)
if (JSON.stringify(arr1) === JSON.stringify(sortedArr1)) {
console.log('✅️ 並び替えの順序が正しいです。')
} else {
console.error('❌️ 並び替えの順序が正しくないです。')
}
console.log(arr1)
console.log(sortedArr1)
if (JSON.stringify(arr2) === JSON.stringify(sortedArr2)) {
console.log('✅️ 並び替えの順序が正しいです。')
} else {
console.error('❌️ 並び替えの順序が正しくないです。')
}
console.log(arr2)
console.log(sortedArr2)清音、濁音、半濁音は並び替えされない
前述のコードだと清音、濁音、半濁音(例: ハ、バ、パ)は区別して並び替えされません。
もし、清音 → 濁音 → 半濁音の順する特殊な並び替えの場合は濁音、半濁音の有無を判定して以下のように並び替えます。
const collator = new Intl.Collator('ja-JP', { numeric: true })
const arr = [
'バナナ',
'パンダ',
'テスト',
'デスマ',
'データ',
'ハナ',
'テック'
]
// 清音=0 / 濁音=1 / 半濁音=2 / その他=3
const soundGroup = (v) => {
const s = String(v)
const first = s[0] ?? ''
// ひらがな・カタカナ以外は「その他」
if (!/[\u3040-\u309F\u30A0-\u30FF]/.test(first)) return 3
const decomposed = first.normalize('NFD')
// 濁点: U+3099 / 半濁点: U+309A
if (decomposed.includes('\u309A')) return 2
if (decomposed.includes('\u3099')) return 1
return 0
}
const sorted = arr.toSorted((a, b) => {
const sa = String(a)
const sb = String(b)
const ga = soundGroup(sa)
const gb = soundGroup(sb)
// 第1キー:清音 → 濁音 → 半濁音 → その他
if (ga !== gb) return ga - gb
// 第2キー:同グループ内の並びは通常の日本語ソート
return collator.compare(sa, sb)
})
console.log(sorted)
// ['テスト', 'テック', 'ハナ', 'データ', 'デスマ', 'バナナ', 'パンダ']ほかにもWebサイトの仕様によっては、特殊な並び替えにしたいケースがあると思いますが、toSorted()の中で比較して並び替えることで実装可能です。
まとめ
JavaScriptの並び替えで「sort()」および「localeCompare()」を使用するのは安全ではありません。
この2つを使用すると可読性、保守性、パフォーマンスが低下し、バグが発生する可能性が高くなります。
JavaScriptの並び替えは「toSorted()」および「Intl.Collator()」を使用してください。
const collator = new Intl.Collator('ja-JP', {
numeric: true
})
const arr = ['10', '1', '3']
const sortedArr = arr.toSorted(collator.compare)
console.log(sortedArr)
// ['1', '3', '10']Intl.Collator - JavaScript | MDN

