今回はArray.prototype.sort()について学びます。
早速解説を見ていきます!
sort() は Array のメソッドで、配列の要素をその場 (in-place) でソートし、ソートされた同じ配列の参照を返します。既定のソート順は昇順で、要素を文字列に変換してから、 UTF-16 コード単位の値の並びとして比較します。
疑問が出たポイントを調べてみます。
- その場 (in-place) でソート
- 元の配列を変更すること。(新しい配列を作らない破壊的変更)
- 同じ配列の参照を返します
- 返り値が配列なので、そのままメソッドチェーンが可能。
- 既定のソート順は昇順
- 小さい順から並び替えます
- 要素を文字列に変換してから
- 数値10は文字列"10"に変換されます。
- UTF-16 コード単位
- UTF-16 コード単位は、Unicode文字をエンコードするために使用される16ビット(2バイト)の値のこと。
- 数値の10と2は文字列として比較すると、"10" は "2" よりも小さいと判断されます。
試してみましょう。
// 公式より抜粋
const months = ['March', 'Jan', 'Feb', 'Dec'];
months.sort();
console.log(months); // ["Dec", "Feb", "Jan", "March"]
アルファベット順に並んでいることがわかりました。
小文字
が混ざるとどうなるんでしょうか??
const months = ['a', 'Jan', 'b', 'Dec'];
months.sort();
console.log(months); // ["Dec", "Jan", "a", "b"]
小文字が後ろに追いやられました!
半角大文字のほうが優先度が高いのですね。
もっと混ぜてみましょう
const months = ['a', 'A', 'ア', 'あ', '亜', '1'];
months.sort();
console.log(months); // ["1", "A", "a", "あ", "ア", "亜"]
なるほど、法則性はありますが、文字と数字を混ぜてsortするのはバグに繋がりそうなのでやめたいですね。
日本語を含む配列
日本語を含む配列ソートする場合、特に複数の種類の文字が混在している場合(ラテン文字、ひらがな、カタカナ、漢字など)、localeCompareメソッドを使用し、適切なロケールを指定する方が望ましいですね!
// localeCompare未使用
const months = ['み', 'カ', 'ん', 'ノ', 'か', 'ワ'];
months.sort();
console.log(months); // ["か", "み", "ん", "カ", "ノ", "ワ"] // カナが後にきた
// localeCompare使用
const months = ['み', 'カ', 'ん', 'ノ', 'か', 'ワ'];
months.sort((a, b) => a.localeCompare(b, 'ja'));
console.log(months); // ["カ", "か", "ノ", "み", "ワ", "ん"] // 日本語の読み方通り
引数
引数に関数を受け取れるようです、これは要素同士を比較するために使用されます。
ぱっと思いつくのは数字の並び替えですね。
let numbers = [30, 10, 50, 20, 40];
numbers.sort((a, b) => {
return a - b; // 昇順で並び替え
});
console.log(numbers); // 出力: [10, 20, 30, 40, 50]
numbers.sort((a, b) => {
return b - a; // 降順で並び替え
});
console.log(numbers); // 出力: [50, 40, 30, 20, 10]
日付もできますよ
let dates = ['2020/01/10', '2020/01/01', '2020/01/05'];
dates.sort((a, b) => new Date(a) - new Date(b));
console.log(dates) // ["2020/01/01", "2020/01/05", "2020/01/10"]
objectでもOK
let books = [
{ title: 'Book A', pages: 200 },
{ title: 'Book B', pages: 150 }
];
books.sort((a, b) => a.pages - b.pages);
// [{ title: "Book B", pages: 150 }, { title: "Book A", pages: 200 }]
複数組み合わせもOK
let employees = [
{ name: 'John', age: 25, salary: 50000 },
{ name: 'Jane', age: 25, salary: 70000 },
{ name: 'Sam', age: 30, salary: 60000 },
{ name: 'Sam', age: 30, salary: 61000 },
{ name: 'Sam', age: 10, salary: 361000 }
];
// 若い順で年齢同じであれば給与順。
employees.sort((a, b) => a.age - b.age || a.salary - b.salary);
//[
// { name: "Sam", age: 10, salary: 361000 },
// { name: "John", age: 25, salary: 50000 },
// { name: "Jane", age: 25, salary: 70000 },
// { name: "Sam", age: 30, salary: 60000 },
// { name: "Sam", age: 30, salary: 61000 }
//]
map を利用したソート
compareFn (比較関数) は、配列内の要素毎に複数回呼び出されることがあります。ただ compareFn の性質によっては、これが多大なオーバーヘッドをもたらす可能性もあります。compareFn がたくさんの処理を行えば行うほど、そしてソート対象の要素数が多ければ多いほど、ソートに map を利用すると効率が上がるでしょう。すなわち、対象の配列を一度だけ走査してソート対象の実際の値を取り出し、一時的な配列に格納した上でソートを行い、その上で一時的な配列を走査して正しい並び順を実現するやりかたです。
つまり、sort中に重めの関数を挟むのはよくない、map関数を使用して事前に処理してからsortしよう!
非効率な例
function 重い関数(arg){
// 計算コストが高い処理
return hoge
}
let items = [1, 2, 3, 4, 5];
items.sort((a, b) => {
let A = 重い関数(a);
let B = 重い関数(b);
return A - B;
});
効率的な例
function 重い関数(arg){
// 計算コストが高い処理
return hoge
}
// 配列の各要素に対して一度だけ計算を行う
let items = [1, 2, 3, 4, 5];
let mapped = items.map((el, i) => {
return { index: i, value: 重い関数(el) };
});
mapped.sort((a, b) => {
return a.value - b.value;
});
// ソートされた順序で元の配列の要素を取得する
let sortedItems = mapped.map(el => items[el.index]);
*重い関数はただのsort条件なので、元の配列の値は変更しない
疎配列における sort() の使用
//公式から抜粋
console.log(["a", "c", , "b"].sort());
// ['a', 'b', 'c', empty]
console.log([, undefined, "a", "b"].sort());
// ["a", "b", undefined, empty]
// nullがある場合、null,undefinedの順になる
console.log([, undefined,null, undefined, "a", "b", null].sort())
// ["a", "b", null, null, undefined, undefined, empty]
// ちなみに空っぽチェックはこんな感じ
let array = [, 1, 2, undefined , 3, 4, 56];
for (let i = 0; i < array.length; i++) {
if (!(i in array)) { // i番目のものはarrayに存在しますか?
console.log(`要素 ${i} は空っぽです。`);
}
}
今回はArray.prototype.sort() についてみていきました!
仕様書を読むと新たな発見や疑問が出てきて面白いですね!
さぁ、どんどん習得していきましょう!!