JavaScriptにおける配列の操作は、Webフロントエンド開発からサーバーサイドのNode.jsまで、あらゆる場面で頻繁に発生するタスクです。
中でも「ソート(並び替え)」は、ユーザーインターフェースにおけるデータの表示順を制御するために欠かせない技術です。
これまではArray.prototype.sort()メソッドが主に使われてきましたが、モダンなJavaScript開発においては新しい選択肢が登場しています。
この記事では、従来の手法から最新の非破壊的メソッドまで、実戦で役立つソートの知識を詳しく掘り下げていきます。
JavaScriptにおける配列ソートの基本概念
JavaScriptで配列を並び替える際、最も基本となるのはsort()メソッドの存在です。
このメソッドは、配列内の要素を適切な順序に並び替えるために使用されます。
しかし、JavaScriptのソートには、他のプログラミング言語とは異なる独自の仕様が含まれています。
初心者から中級者のエンジニアが最も混乱しやすいのは、デフォルトのソート順が「文字列の辞書順」であるという点です。
例えば、数値の配列をそのままソートしようとすると、意図しない結果が返ってくることがあります。
これは、JavaScriptが内部的に要素を文字列に変換してから比較を行うためです。
そのため、数値を数値として正しく並び替えるには「比較関数」という仕組みを理解する必要があります。
また、近年のJavaScript(ES2023以降)では、元の配列を書き換えない新しいメソッドも追加されました。
これらの最新機能を使いこなすことは、バグの少ないクリーンなコードを書くための第一歩となります。
破壊的メソッド:sort()の挙動と注意点
Array.prototype.sort()は、JavaScriptの誕生初期から存在する標準的なメソッドです。
このメソッドの最大の特徴は、元の配列を直接書き換える「破壊的な処理」であるという点にあります。
以下のコードを見て、その挙動を確認してみましょう。
const fruits = ["banana", "apple", "cherry"];
fruits.sort();
console.log(fruits); // 元の配列が書き換わる
["apple", "banana", "cherry"]
上記の例では、fruitsという変数の中身そのものがアルファベット順に変更されています。
この挙動は、メモリ効率の面では有利に働くことがありますが、予期せぬサイドエフェクト(副作用)を生む原因にもなります。
特にReactやVue.jsといったモダンなフロントエンドフレームワークでは、状態(State)の不変性(Immutability)が重要視されます。
元の配列を勝手に変更してしまうと、コンポーネントの再レンダリングが正しく検知されないなどの問題が発生します。
また、sort()メソッドは戻り値として「ソート済みの元の配列への参照」を返します。
つまり、新しい配列が作成されるわけではないということを常に意識しておく必要があります。
数値ソートにおける罠
先ほど触れた通り、sort()を引数なしで使用すると、数値であっても文字列として扱われます。
次の例は、多くの開発者が一度は経験する典型的なミスです。
const numbers = [1, 10, 2, 25, 5];
numbers.sort();
console.log(numbers);
[1, 10, 2, 25, 5]
期待していた結果は[1, 2, 5, 10, 25]だったはずですが、実際には「1」の次に「10」が来ています。
これは、文字列としての比較では「”10″」が「”2″」よりも先に来ると判定されるためです。
このような意図しない挙動を防ぐためには、比較関数(compareFunction)を明示的に渡すことが必須となります。
非破壊的メソッド:toSorted()のメリット
2023年に導入されたtoSorted()メソッドは、現代のJavaScript開発におけるスタンダードになりつつあります。
toSorted()の最大の特徴は、元の配列を変更せず、ソートされた新しい配列を返すという点にあります。
これにより、関数の外部にあるデータを汚染することなく、安全に並び替えを行うことが可能です。
const original = [3, 1, 4, 2];
const sorted = original.toSorted();
console.log("Original:", original);
console.log("Sorted:", sorted);
Original: [3, 1, 4, 2]
Sorted: [1, 2, 3, 4]
このように、original配列はそのままの状態で維持されています。
関数型プログラミングのスタイルを取り入れているプロジェクトでは、このメソッドの使用が強く推奨されます。
また、スプレッド構文を使って[...arr].sort()のように記述していた従来の回避策よりも、意図が明確で読みやすいコードになります。
2026年現在のブラウザ環境やNode.js環境であれば、ポリフィルなしでほぼ全ての環境で利用可能です。
比較関数の書き方とロジックの理解
sort()やtoSorted()を使いこなす鍵は、引数として渡す「比較関数」の理解にあります。
比較関数は2つの引数(通常は a と b と呼ばれます)を受け取り、数値を返却する関数です。
この返却される数値によって、要素の順序が決定されます。
| 比較関数の戻り値 | 並び替えの動作 |
|---|---|
| 0より小さい(負の値) | a を b より前に配置する |
| 0 | 順序を変更しない(または元の順序を維持) |
| 0より大きい(正の値) | b を a より前に配置する |
このルールを覚えるだけで、あらゆるデータ形式のソートに対応できるようになります。
数値の昇順・降順ソート
最も一般的な数値ソートは、引き算を利用した簡潔な記法で記述できます。
昇順(小さい順)に並べたい場合は、a - b を返します。
const numbers = [40, 100, 1, 5, 25];
// 昇順ソート
const ascending = numbers.toSorted((a, b) => a - b);
console.log(ascending);
[1, 5, 25, 40, 100]
逆に降順(大きい順)にしたい場合は、b - a を返します。
// 降順ソート
const descending = numbers.toSorted((a, b) => b - a);
console.log(descending);
[100, 40, 25, 5, 1]
この「引き算」のテクニックは非常に便利ですが、比較対象が数値であることが保証されている場合にのみ有効です。
オブジェクト配列のソート
実務で最も多いケースは、オブジェクトが格納された配列の特定のプロパティを基準にソートすることです。
例えば、商品のリストを価格の安い順に並べ替える処理を考えてみましょう。
const products = [
{ name: "Laptop", price: 120000 },
{ name: "Mouse", price: 3000 },
{ name: "Keyboard", price: 15000 }
];
const cheapFirst = products.toSorted((a, b) => a.price - b.price);
console.log(cheapFirst);
[
{ "name": "Mouse", "price": 3000 },
{ "name": "Keyboard", "price": 15000 },
{ "name": "Laptop", "price": 120000 }
]
オブジェクトのプロパティにアクセスし、その値を比較関数の計算に使用します。
文字列のプロパティ(名前など)で比較したい場合は、localeCompare()メソッドの使用を推奨します。
文字列ソートの決定版:localeCompare
単純なアルファベットの比較であれば不等号(< や >)でも可能ですが、日本語や特殊文字を含む場合は不十分です。
String.prototype.localeCompare()を使用すると、言語の設定を考慮した正しい順序でソートが可能になります。
const names = ["田中", "安藤", "佐藤", "伊藤"];
// 日本語の辞書順でソート
const sortedNames = names.toSorted((a, b) => a.localeCompare(b, "ja"));
console.log(sortedNames);
["安藤", "伊藤", "佐藤", "田中"]
localeCompareは、文字列が前にあるか後ろにあるかを -1, 0, 1 のいずれかで返してくれるため、そのまま比較関数として利用できます。
大文字と小文字を区別しないソートや、数値を含む文字列を自然な順番(1, 2, 10の順)で並べるオプションも存在します。
const items = ["Item 10", "Item 2", "Item 1"];
// 自然な順序(numeric: true)を指定
const naturalSorted = items.toSorted((a, b) =>
a.localeCompare(b, undefined, { numeric: true })
);
console.log(naturalSorted);
["Item 1", "Item 2", "Item 10"]
ユーザーに提供するリストを並び替える際は、常にこのlocaleCompareを検討するべきです。
複雑なソート条件:多段階ソートの実装
複数の条件を組み合わせてソートしたい場面もよくあります。
例えば、「まずはカテゴリーで分類し、同じカテゴリー内では価格が安い順にする」といったケースです。
このような多段階ソートは、比較関数の中で論理演算子を活用することで実現できます。
const items = [
{ category: "Food", price: 500 },
{ category: "Electronics", price: 10000 },
{ category: "Food", price: 200 },
{ category: "Electronics", price: 5000 }
];
const complexSorted = items.toSorted((a, b) => {
// 第一条件:カテゴリー名で比較
const categoryDiff = a.category.localeCompare(b.category);
// カテゴリーが異なる場合はその結果を返す
if (categoryDiff !== 0) return categoryDiff;
// 第二条件:同じカテゴリーなら価格で比較
return a.price - b.price;
});
このロジックを応用すれば、3つ以上の条件を重ねることも可能です。
比較関数の中で 0 が返されたときだけ次の条件を評価するという流れが基本となります。
ソートの安定性(Stability)について
JavaScriptのソートアルゴリズムに関して知っておくべき重要な概念が「安定性(Stability)」です。
安定なソートとは、比較結果が等しい要素同士の、元の相対的な順序が維持されることを指します。
かつてのJavaScriptエンジンの実装(古いV8など)では、配列の長さによって不安定なクイックソートが使われることがありました。
しかし、現在のECMAScript仕様(ES2019以降)では、Array.prototype.sortは安定(stable)でなければならないと定められています。
これにより、多段階ソートを複数の sort() 呼び出しに分けて行うといったテクニックも確実性を増しています。
ブラウザ間の挙動の違いを心配する必要がなくなったのは、現代のエンジニアにとって大きな恩恵と言えるでしょう。
パフォーマンスに関する考慮事項
非常に大きな配列(数万件以上のデータ)をソートする場合、パフォーマンスが問題になることがあります。
JavaScriptのソートは一般的に「Timsort」と呼ばれるアルゴリズムで実装されており、平均的な時間計算量は O(n log n) です。
ソート自体の速度も重要ですが、ボトルネックになりやすいのは「比較関数内での重い処理」です。
例えば、比較のたびに複雑な正規表現を実行したり、外部APIの値を参照したりすることは避けるべきです。
もし比較の基準となる値の計算に時間がかかる場合は、あらかじめ「ソート用のキー」を計算しておく「Schwartzian Transform(シュワルツ変換)」のような手法が有効です。
具体的には、まず map で計算済みの値を持つ一時的なオブジェクト配列を作り、それをソートしてから再び map で元の形式に戻すという手順を踏みます。
これにより、コストの高い計算が要素数分(n回)だけで済み、比較の回数(n log n回)分実行されるのを防ぐことができます。
sort()とtoSorted()の使い分け:どちらを使うべきか
最終的にどちらのメソッドを使うべきか迷った際は、以下の基準を参考にしてください。
基本的には、モダンな開発においては「toSorted()」をデフォルトの選択肢にするのが賢明です。
- toSorted() を選ぶべき場面:
- ReactやReduxなどのステート管理下にある配列を操作する場合。
- 元のデータを保護しつつ、新しい変数にソート結果を格納したい場合。
- 関数型プログラミングのパラダイムを優先したい場合。
- sort() を選ぶべき場面:
- メモリ消費を極限まで抑えたい、非常に大規模なデータ処理を行う場合。
- 元の配列を使い捨てにするスクリプトで、新しい配列を生成するオーバーヘッドを避けたい場合。
- 2023年以前の古いブラウザ環境をサポートしなければならず、ポリフィルの導入も制限されている場合。
コードの可読性とメンテナンス性を重視するなら、非破壊的な toSorted() が圧倒的に優れています。
まとめ
JavaScriptの配列ソートは、単純なアルファベット順の並び替えから、複雑なオブジェクトの多段階ソートまで、非常に奥が深いテーマです。
デフォルトの挙動が文字列ベースであることを忘れず、常に明示的な比較関数を提供することがバグを防ぐコツです。
また、toSorted()メソッドの登場により、不変性を保ちながら直感的にソートを行えるようになりました。
今回解説した比較関数のロジックや localeCompare の活用、そしてパフォーマンスへの意識を持つことで、より高度なデータ操作が可能になります。
データの並び替えはユーザー体験に直結する重要な機能です。
ぜひこの記事で紹介した手法を日々の開発に取り入れ、正確で効率的なコードを実現してください。
