Q1比較関数が負の値を返したとき、sort はどちらの要素を前に置きますか?
並べ替えと検索 — sort / find / some / every
配列を並べ替える sort と、条件に合う要素を探す find を扱います。比較関数の戻り値で決まる並び順、元の配列を残す toSorted、真偽値だけを返す some と every まで確かめます。
一覧を並べ替えるたびに for で要素の位置を入れ替える処理を書くと、比べ方と入れ替えの手順を毎回自分で管理することになります。条件に合う 1 件を探すときも、for と break を組み合わせて、見つかった時点で抜ける形が残ります。
この記事では、並べ替える sort と、条件に合う要素を探す find を扱います。
並び順を関数で指定する — sort と比較関数
商品の一覧を価格の安い順に並べ替えて画面に出したいとします。並べ替えは配列のメソッドで行えますが、products.sort() とだけ書いても価格の順にはなりません。何を基準に比べるかを渡していないためです。
sort(配列の要素を並べ替えるメソッド)には 比較関数(2 つの要素を受け取って数値を返す関数)を渡します。受け取る 2 件を順に a・b と呼ぶと、返った数値が負なら a、正なら b を前に置きます。a.price - b.price と書けば価格の昇順です。
const products = [
{ name: "ワイヤレスマウス", price: 2980 },
{ name: "モバイルバッテリー", price: 4380 },
{ name: "USB-C ケーブル", price: 980 },
{ name: "マウスパッド", price: 2980 },
];
// 引き算の結果が負なら a が前、正なら b が前に並ぶ
products.sort((a, b) => a.price - b.price);
console.log(products.map((item) => item.price).join(", ")); // 980, 2980, 2980, 4380
// 価格が同じ 2 件の前後を確かめる
console.log(products[1].name, products[2].name); // ワイヤレスマウス マウスパッド
// 引く向きを入れ替えると降順になる
products.sort((a, b) => b.price - a.price);
console.log(products[0].name); // モバイルバッテリー
sort が見ているのは返った数値の符号だけで、-2000 と -1 は同じ扱いです。同じ価格の商品どうしにも順を付けたいときは、差が 0 になった場合に別の値を比べる処理を、比較関数の中に書き足します。
そのまま呼ぶと起きること — 既定の比較と toSorted
ページの滞在秒数を集めた数値の配列を、短い順に並べたいとします。比較関数を渡さずに sort() と書くと、25 秒より 1180 秒が前に来るような、大小と合わない並びが返ります。
比較関数を省いた sort は、要素をいったん文字列に直してから 1 文字ずつ比べます。1180 は "1180"、25 は "25" として扱われ、先頭の 1 と 2 で前後が決まります。sort は呼ばれた配列そのものを並べ替えるので、元の並びも残したいときは toSorted(並べ替えた新しい配列を返すメソッド)を使います。
const viewSeconds = [1180, 980, 25, 4380];
// 比較関数がないと文字列として比べられる
console.log(viewSeconds.sort().join(", ")); // 1180, 25, 4380, 980
// 比較関数を渡すと数値の大小で並ぶ
console.log(viewSeconds.sort((a, b) => a - b).join(", ")); // 25, 980, 1180, 4380
// toSorted は新しい配列を返し、元の配列は変えない
const stayTimes = [1180, 980, 25, 4380];
console.log(stayTimes.toSorted((a, b) => a - b).join(", ")); // 25, 980, 1180, 4380
console.log(stayTimes.join(", ")); // 1180, 980, 25, 4380
文字列にした時点で "1180" が "25" より前と決まるため、数値の配列を並べ替えるときは比較関数を渡します。上のコードの 2 回目の sort は、1 回目で並びが書き換わった viewSeconds を、さらに並べ替えています。
sort が返すのは、書き換えた viewSeconds そのものです。新しい配列ができるのは toSorted の側だけです。古いブラウザには toSorted がありません
toSorted は ES2023 で仕様に加わったメソッドで、対応していない古いブラウザでは、呼び出した行が TypeError で止まります。そうした環境でも動かすときは、[...stayTimes].sort((a, b) => a - b) のように、スプレッド構文でコピーした配列に sort を呼びます。
条件に合う 1 件を取り出す — find と findIndex
予約番号を指定して 1 件の予約を取り出したいとします。filter でも書けますが、返るのは配列なので [0] を付けて 1 件目を読むことになり、条件に合う予約が 1 件も無ければ空の配列が返ります。
find(条件に合う最初の要素を返すメソッド)は、コールバックが最初に true を返した要素そのものを返します。位置が知りたいときは findIndex(条件に合う最初の要素の位置を返すメソッド)を使います。
const reservations = [
{ code: "RSV-201", room: "会議室 A" },
{ code: "RSV-202", room: "会議室 B" },
{ code: "RSV-203", room: "会議室 C" },
];
// 最初に true が返った要素そのものが戻る
const target = reservations.find((item) => item.code === "RSV-202");
console.log(target.room); // 会議室 B
// 位置が欲しいときは findIndex
console.log(reservations.findIndex((item) => item.code === "RSV-202")); // 1
// 条件に合う予約が無いとき
console.log(reservations.find((item) => item.code === "RSV-999")); // undefined
console.log(reservations.findIndex((item) => item.code === "RSV-999")); // -1
true が返った時点で、3 件目のコールバックは呼ばれません。条件に合う要素が前にあるほど、呼び出す回数は少なくなります。find が返すのは条件に合う先頭の 1 件だけなので、該当する要素が複数あるときに全部を集める用途には使えません。すべて必要なときは配列を返す filter を使い、1 件で足りるときに find を選びます。
見つからないと undefined が返る
find が返した値をそのまま target.room のように読むと、条件に合う要素が無かったときに TypeError: Cannot read properties of undefined (reading 'room') で止まります。読む前に if (target) で確かめます。
配列全体を真偽で確かめる — some と every
注文の明細をまとめて出荷できるかを、在庫の状態から判断したいとします。在庫切れの明細を filter で集めてから件数を数えても判定できますが、欲しいのは配列ではなく true か false の 1 つの答えです。
some(条件に合う要素が 1 つでもあれば true を返すメソッド)と every(すべての要素が条件に合えば true を返すメソッド)は、要素ではなく真偽値を返します。if の条件にそのまま書ける形です。
const orderItems = [
{ name: "詰め替えボトル", stock: 12 },
{ name: "ハンドソープ", stock: 0 },
{ name: "泡立てネット", stock: 5 },
];
// 1 つでも条件に合えば true
console.log(orderItems.some((item) => item.stock === 0)); // true
// すべてが条件に合ったときだけ true
console.log(orderItems.every((item) => item.stock > 0)); // false
// 空の配列では some が false、every が true
console.log([].some((item) => item.stock > 0)); // false
console.log([].every((item) => item.stock > 0)); // true
some も every も、在庫 0 の 2 件目で答えが決まります。every は条件に合わない要素を見つけた時点で終わります。空の配列では、条件に合う要素が 1 つも無いので some は false、条件に反する要素も無いので every は true です。下の表は、この記事で扱った 4 つのメソッドが返す値をまとめたものです。
| メソッド | 返すもの | 条件に合う要素が無いとき |
|---|---|---|
| find | 最初に条件に合った要素そのもの | undefined |
| findIndex | 最初に条件に合った要素の位置 | -1 |
| some | 1 件でも合えば true | false |
| every | すべて合えば true | false(空の配列では true) |
理解度チェック
まずは1問ずつ答えてみましょう。
Q2[1180, 980, 25] を比較関数なしで sort() すると、どの並びになりますか?
Q3在庫がすべて 1 以上かどうかを 1 つの真偽値で確かめたいとき、使うメソッドはどれですか?