JavaScriptで数字を並び替えしたい時があってsort関数を使う機会があったのですが、どういう原理で並び替えをしているのかイマイチよくわからなかったので調べまくりました。
一通り理解できたので僕のように「何でそうなるの?」という人の助けに少しでもなればいいかなと思って記事を書きます。役に立たなかったらごめん。
また、この記事では数値の並び替えで使われるコールバック関数付きのsort関数に絞って解説します(文字列のソートは比較関数を必要とせずsort()
だけでソートできるので説明を割愛します)。
sort関数が並び替えをする関数ってことはみんな知ってると思うので、「何で並び替えができるの?」「どういう原理で並び替えをしているの?」といったことを見ていきます。ぜひ時間を書けてじっくり読むことをおすすめします。
sort関数で数値の順番で並び替えができる原理
まずは以下のサンプルを見てください。このサンプルは配列の中の数字を小さい順に並び替えます。
const numbers = [2, 5, 100, 4];
numbers.sort(function (a, b) {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
});
console.log(numbers); // 結果:[2, 4, 5, 100]
こんな感じのコードはいろんな記事で紹介されていますが、僕は最初これを見た時にわからないことがいくつもありました。
- 引数にある
a, b
って何? - if文の
a < b
とかa > b
って何を比較しているの? return -1
とかreturn 1
って何?
まずはこのあたりを見ていきます。
引数にあるa, bって何?
→配列の中の要素のうちの1つがa
、次の1つがb
です。sort関数で数値を比較する際には引数a
とb
を必ず取ります(名前は慣例的に大体a
とb
です)。
例えば配列numbers
にある2をa
、次の要素である5をb
に入れてみます。
2と5を比較すると2 < 5となり、a < b
の条件を満たすのでreturn -1
の方を通ります。
また100をa
、4をb
とすると100 > 4となり、a > b
の条件を満たすのでreturn 1
の方を通ります。
このようにa
には配列のうちの1つの要素、b
には次の要素が入ることでその2つの数字の大小を比較しているわけです。この比較が順番を並べ替えるために使われます。
まずは「a
とb
は配列の中の要素なんだなー」ということを頭に入れておいてください。
if文のa < bとかa > bって何を比較しているの?
これは上で解説しましたね。
a
には配列のうちの1つの要素、b
には次の要素が入り、それぞれの数値を比較しています。
return -1とかreturn 1って何?
return -1
やreturn 1
が何なのか知るために、まずはsort関数について知っておく必要があります。ここめっちゃ重要です。
sort関数は以下のルールに従って並び替えをしています。
- コールバック関数が0未満(例えば-1)を返した場合:
a
はb
の前に来る(順番は変わらない) - コールバック関数が0より大きい値(例えば1)を返した場合:
b
はa
の前に来る(順番が変わる) - コールバック関数が0を返した場合:何もしない
例えばa
に100, b
に4が入っている場合、正しい順番に並び替えるにはb
をa
の前に持ってくる必要があります。そして、そのためにはコールバック関数で0より大きい値を返せばいいということになります。
この「0より大きい値を返せばいい」という部分がelse if (a > b) { return 1; }
です。
a
とb
を比較し、a
の方が大きければreturn 1
を返すことでsort関数がb
をa
の前に持ってきてくれるというわけです。これにより4, 100
という順番に並び替わります。
またa
が2、b
が5の場合はif (a < b) { return -1; }
を通るので、「コールバック関数が0未満(例えば-1)を返した場合:a
はb
の前に来る(順番は変わらない)」にある通り、そのままの順番になります。
なお、a
が2、b
も2といった場合はelse { return 0; }
を通るのでsort関数は何もしません。
これが繰り返されることによって数値の順番が並び替わっていくわけです。
なんだかごっちゃになりそうな人はとりあえず「正の数が返ったらソート」これだけ頭に入れておけばokです。これがsort関数のルールです。
つまり、最初に言った「return -1
やreturn 1
って何?」に対する答えは、「sort関数が並び替え処理をするために必要な戻り値」ということになります。return -1
やreturn 1
自体に特別な意味があるわけではなく、正の数と負の数のどちらかを返しているか?を判断するために-1や1を返しているだけです。
ということは、別にreturn
の値は1や-1じゃなくて3とか-100でも全然いいわけです。これが後述するreturn a - b;
といった書き方の考え方になります。
まだイマイチよくわからない人はここを何回も読み直して具体的にイメージを膨らませてみてください。ここがsort関数で数値を並び替える上で一番重要です。
sort関数で降順で並び替える
さっきは昇順で並び替えをしましたが、降順で並び替えることもできます。
以下のサンプルは数値を降順で並び替えるコードです。
const numbers = [2, 5, 100, 4];
numbers.sort(function (a, b) {
if (a > b) {
return -1;
} else if (a < b) {
return 1;
} else {
return 0;
}
});
console.log(numbers); // 結果:[100, 5, 4, 2]
前のコードと違う部分はif文の比較演算子が逆になっていることです。
例えばa
が2、b
が5の場合、else if (a < b) { return 1; }
の処理を通ります。
sort関数はコールバック関数が0より大きい値を返した場合、b
はa
の前に来るので5, 2
という順番に並び替わります。これを繰り返していくことで大きい数字がどんどん前に来て最終的に降順に並び替わります。
sort関数を簡潔に書く方法
上に書いたようなsort関数で並び替えをするコードはかなり簡潔に書くことができます。
以下のような書き方です。もしかしたらこっちのほうが見たことあるかもしれないですね。
const numbers = [2, 5, 100, 4];
numbers.sort(function (a, b) {
return a - b;
});
console.log(numbers); // 結果:[2, 4, 5, 100]
このような書き方でも結果は全く同じになります。
やっていることは今までと同じで、引数a
には配列のうちの1つの要素、b
には次の要素が入ります。そしてsort関数は戻り値が0未満なら順番は変わらない、0より大きいなら順番を入れ替えるという処理をします。
仮にa
が2、b
が5の場合、2 – 5 = -3となり、戻り値は0未満なので順番は変わりません。
また、a
が100、b
が4の場合、100 – 4 = 96となり、戻り値は0より大きいので順番が入れ替わります。
これを繰り返すことで並び替えができます。配列の中に負の数が入っていても正しくソートできます。
「return -1
やreturn 1
自体に特別な意味があるわけではない」と前述した理由はこれです。要は正の数と負の数のどちらが返っているかがわかればいいので上のような書き方ができるわけです。
降順も簡潔に書く
上のような簡潔な書き方で降順にソートしたい場合は以下のように書けばokです。
const numbers = [2, 5, 100, 4];
numbers.sort(function (a, b) {
return b - a;
});
console.log(numbers); // 結果:[100, 5, 4, 2]
上のコードと違うのはa - b
がb - a
に変わっている点ですね。
これも今までと全く同じ考え方で理解できます。
仮にa
が2、b
が5の場合、5 – 2 = 3となり、戻り値は0より大きいので順番が入れ替わります。
また、a
が100、b
が4の場合、4 – 100 = -96となり、戻り値は0未満なので順番は変わりません。
これを繰り返すことで降順で並び替えができます。
どうでしょう?だんだん分かってきましたか?一度仕組みがわかってしまえば簡単です。
まだイマイチ理解できない人は本記事の最初の方を具体的な数値を当てはめて考えてみてくださいね。
アロー関数でさらに簡潔に書く
ES6記法のアロー関数を使うとなんと1行で書けます。
const numbers = [2, 5, 100, 4];
numbers.sort((a, b) => a - b);
console.log(numbers); // 結果:[2, 4, 5, 100]
やっていることは上の.sort(function(a, b) { return a - b })
と全く同じです(アロー関数はreturn
を返すだけの場合、return
も省略できます)。書き方が簡潔になったというだけですね。
フロントエンド開発ではもっぱらこの書き方が使われますが、Web制作で使われるのかは微妙ですね。初見殺し感がすごいので…。
アロー関数で降順で並び替え
アロー関数で降順で並び替えをするときもやはりa - b
をb - a
に変えるだけです。
const numbers = [2, 5, 100, 4];
numbers.sort((a, b) => b - a);
console.log(numbers); // 結果:[100, 5, 4, 2]
これもやっていることは.sort(function(a, b) { return b - a })
と全く同じですね。
sort関数を非破壊的(イミュータブル)に扱う
これは中級者向けの内容になりますが、sort関数はいわゆる「破壊的メソッド」の1つで、コピーした値を使っても元の配列まで書き換わってしまうという残念な特徴があります(破壊的メソッドはsort()
以外にも色々あります)。
const numbers = [2, 5, 100, 4];
const copiedNumbers = numbers; // 元の配列をコピー
copiedNumbers.sort((a, b) => a - b); // コピーした方をソート
console.log(numbers); // 結果:[2, 4, 5, 100] ←元の配列までソートされてしまっている!!
console.log(copiedNumbers); // 結果:[2, 4, 5, 100]
上記のように元の配列をコピーした方にsort()
をかけたのに、元の配列までソートされてしまっています。
こういう事があると意図せず元のデータが並び替えられてしまったり、並び替える前の配列を使いたいのにうまく扱えないといったことが起こります。
というわけでsort関数を非破壊的に扱えるようにひと工夫します。
スプレッド構文で一度配列を展開して再度配列に入れるという方法で非破壊的に扱えます。
const numbers = [2, 5, 100, 4];
const copiedNumbers = [...numbers]; // スプレッド構文で一度展開して再度配列に入れる
copiedNumbers.sort((a, b) => a - b); // コピーした方をソート
console.log(numbers); // 結果:[2, 5, 100, 4] ←元の配列はそのままになっている!
console.log(copiedNumbers); // 結果:[2, 4, 5, 100] ←コピーした方は並び替わっている!
これもフロントエンド開発ではもっぱら使われる書き方なので頭の中に入れておくといいです。
まあこのへんの話は色々奥が深いので、気になる人は「js イミュータブル」「js 非破壊的」とかでググってみるといいかもです。
jQueryでも並び替える
sort関数はjQueryでも使えます。
より実践的な使い方をするならHTMLの要素を並び替えたい場面とかですね。
以下のサンプルはボタンクリック時にHTMLのdata-index
属性をもとにリストを並び替えるものです(右下のReturnでリロードできます)
See the Pen
by wagashi000327 (@wagashi000327)
on CodePen.
上はコードが若干見づらいのでjsのコードを載せておきます。
$(function() {
$('button').click(function() {
$('li').sort(function (a, b) {
return $(a).data('index') - $(b).data('index');
}).appendTo('ul');
});
});
sort関数を使ってdata-index
属性の順番通りに並び替わっているのが確認できます。
これも今までと考え方は同じで、$(a).data('index')
が5、$(b).data('index')
が2の場合、5 – 2 = 3となり、戻り値は0より大きいので順番が入れ替わります。
注意点として、sort関数終了後にそのままappendTo()
をつなげてソートした要素を挿入しているのに気をつけましょう。
「sort関数が効かない」という場合はappendTo()
しているか確認してみてくださいね。
sort関数は配列の数値じゃなくてもいい
上のサンプルを見て分かる通りsort関数で並び替えをする場合、比較するのは数値の大小だけじゃなくて要素のインデックス番号や文字列の長さなど、正直何でもokです。
というわけで色々ソートしてみましょう。
以下のコードは配列の要素を文字列の長さでソートするサンプルです。
const array = ['hoge', 'foobar', 'fugafuga', 'bar'];
array.sort(function (a, b) {
return a.length - b.length;
});
console.log(array); // 結果:["bar", "hoge", "foobar", "fugafuga"]
文字数が少ない順に並び替えができていますね。
また、以下はオブジェクトのid
をもとにソートするサンプルです。
const people = [
{ name: '山田', id: 8 },
{ name: '田中', id: 2 },
{ name: '佐藤', id: 1 },
{ name: '鈴木', id: 5 },
];
people.sort(function (a, b) {
return a.id - b.id;
});
console.log(people);
/* 結果:
[
{name: "佐藤", id: 1}
{name: "田中", id: 2}
{name: "鈴木", id: 5}
{name: "山田", id: 8}
]
*/
こちらもオブジェクトのid
の数値を使ってソートできました。
またこんなこともできてしまいます。
以下は配列classes
の要素の順番通りにオブジェクトをソートするサンプルです。
const classes = ['部長', '課長', '係長', '社員'];
const members = [
{ name: '山田', clazz: '係長' },
{ name: '鈴木', clazz: '部長' },
{ name: '田中', clazz: '社員' },
{ name: '佐藤', clazz: '課長' },
];
members.sort(function (a, b) {
return classes.indexOf(a.clazz) - classes.indexOf(b.clazz);
});
console.log(members);
/* 結果:
[
{name: "鈴木", clazz: "部長"}
{name: "佐藤", clazz: "課長"}
{name: "山田", clazz: "係長"}
{name: "田中", clazz: "社員"}
]
*/
配列にindexOf()
をかけるとその要素の配列内でのインデックス番号を取得できます。'係長'
なら2ですね(インデックス番号は0から始まるので)。
a
が最初のオブジェクト、b
が次のオブジェクトだとするとclasses.indexOf(a.clazz)
はイコールclasses.indexOf('係長')
なので2、classes.indexOf(b.clazz)
はイコールclass.indexOf('部長')
なので0となります。2 – 0 = 2で戻り値は0より大きいので順番が入れ替わります。
こんな感じであらかじめ「こういう順番でソートしたい」という配列を作っておけば、そのインデックス番号を使って配列やオブジェクトをソートしたりできます。
ちなみにclazz
という名前にしている理由はclass
は予約語だからです。
まとめ
sort関数はぱっと見どんな処理をしているのかよくわからないし、詳しく解説している記事がなかなか見つからなくて仕組みを理解するのに少し手間取りました。でも1度仕組みを理解してしまえば使うのは簡単です。
この記事でsort関数を理解する手助けになれば幸いです。
参考動画(英語)
YouTubeで探すと結構わかりやすい動画が出てきたりするのでググっても出てこなかったらツベってみるのがおすすめです。