JavaScriptの新しいArrayメソッドで配列操作を効率的に行えるようになった
配列のグループ化は Object.groupBy() と Map.groupBy() で書きます。ES2024で標準化され、2024年3月から全モダンブラウザで使えます。
この記事は2024年の公開後、2026年9月に見直しました。 公開当時のコードで使っていた array.groupBy() という配列のメソッドは存在しません。実行すると TypeError になります。正しい構文に差し替えたうえで、ES2023で追加された「元の配列を壊さない」メソッド群も追記しています。
Array.prototype.groupBy()は存在しません。 提案段階ではArray.prototype.group()という名前でしたが、Web互換性の問題で取り下げられ、静的メソッドのObject.groupBy()/Map.groupBy()として標準化されましたflatMap()(ES2019)とat()(ES2022)は2024年の新機能ではありません
いつ追加されたのかを整理する
| メソッド | 追加 | できること |
|---|---|---|
flatMap() |
ES2019 | 変換と1段階の平坦化を同時に行う |
at() |
ES2022 | 負のインデックスで末尾から取得 |
findLast() / findLastIndex() |
ES2023 | 末尾から探す |
toSorted() / toReversed() / toSpliced() / with() |
ES2023 | 元の配列を壊さずに新しい配列を返す |
Object.groupBy() / Map.groupBy() |
ES2024 | キーごとにグループ化する |
Array.fromAsync() |
ES2024 | 非同期イテラブルから配列を作る |
Object.groupBy():配列をキーごとにまとめる
第1引数に配列、第2引数にキーを返す関数を渡します。 配列のメソッドではなく、Object の静的メソッドである点に注意してください。
const users = [
{ name: 'Alice', age: 28 },
{ name: 'Bob', age: 22 },
{ name: 'Charlie', age: 22 },
{ name: 'David', age: 28 },
];
// users.groupBy(...) は TypeError になる
const grouped = Object.groupBy(users, (user) => user.age);
console.log(grouped);
/*
{
22: [{ name: 'Bob', age: 22 }, { name: 'Charlie', age: 22 }],
28: [{ name: 'Alice', age: 28 }, { name: 'David', age: 28 }]
}
*/
これまで reduce() で書いていた処理が1行になります。
// 従来の書き方
const grouped = users.reduce((acc, user) => {
(acc[user.age] ||= []).push(user);
return acc;
}, {});
// ES2024
const grouped = Object.groupBy(users, (user) => user.age);
知っておくべき3つの挙動
1. キーは必ず文字列になります。 数値を返しても、オブジェクトのキーとして文字列化されます。
const grouped = Object.groupBy(users, (user) => user.age);
grouped[22]; // 取れる("22" に変換されて一致する)
grouped['22']; // 同じもの
Object.keys(grouped); // ['22', '28'] ← 文字列
2. 戻り値のプロトタイプは null です。 通常のオブジェクトリテラルと違い、hasOwnProperty などのメソッドを持ちません。
const grouped = Object.groupBy(users, (u) => u.age);
grouped.hasOwnProperty('22'); // TypeError
Object.hasOwn(grouped, '22'); // true ← こちらを使う
これは意図的な設計です。プロトタイプが無いおかげで、"constructor" や "__proto__" のような値がキーになっても事故が起きません。
3. コールバックの第2引数にインデックスが渡ります。
const chunked = Object.groupBy(items, (item, index) => Math.floor(index / 3));
// 3件ずつのまとまりに分割できる
Map.groupBy():キーに文字列以外を使いたいとき
キーをオブジェクトや数値のまま保ちたい場合は Map.groupBy() を使います。戻り値は Map です。
const grouped = Map.groupBy(users, (user) => user.age);
grouped.get(22); // 数値のまま引ける
grouped.get('22'); // undefined
for (const [age, list] of grouped) {
console.log(age, list.length); // 22 2 / 28 2
}
オブジェクトそのものをキーにすることもできます。
const teamA = { id: 1, name: 'チームA' };
const teamB = { id: 2, name: 'チームB' };
const members = [
{ name: 'Alice', team: teamA },
{ name: 'Bob', team: teamB },
{ name: 'Carol', team: teamA },
];
const byTeam = Map.groupBy(members, (m) => m.team);
console.log(byTeam.get(teamA).length); // 2
Object.groupBy() |
Map.groupBy() |
|
|---|---|---|
| 戻り値 | プロトタイプなしのオブジェクト | Map |
| キーの型 | 文字列に変換される | そのまま保たれる |
| JSON化 | そのまま JSON.stringify() できる |
変換が必要 |
| 向く場面 | APIレスポンス、表示用の整形 | オブジェクトをキーにしたい、順序を保ちたい |
ES2023:元の配列を壊さないメソッド
実務で効き目が大きいのはこちらかもしれません。 sort() や reverse() は元の配列を書き換えてしまうため、Reactの状態など「変更してはいけない配列」を扱うときに事故の原因になります。
const scores = [3, 1, 4, 1, 5];
// 元の配列が書き換わる
scores.sort(); // scores 自体が並び替わる
const copy = [...scores].sort(); // これまでの回避策
// 元の配列はそのまま、新しい配列が返る
const sorted = scores.toSorted((a, b) => a - b); // [1, 1, 3, 4, 5]
const reversed = scores.toReversed(); // [5, 1, 4, 1, 3]
const replaced = scores.with(0, 99); // [99, 1, 4, 1, 5]
const spliced = scores.toSpliced(1, 2); // [3, 1, 5]
console.log(scores); // [3, 1, 4, 1, 5] ← 変わっていない
| 破壊的(元を変える) | 非破壊(新しい配列を返す) |
|---|---|
sort() |
toSorted() |
reverse() |
toReversed() |
splice() |
toSpliced() |
arr[i] = v |
with(i, v) |
toSorted() にも比較関数は必要です。 省略すると sort() と同じく文字列として比較されるため、[10, 9, 100] が [10, 100, 9] になります。ここは変わっていません。
findLast():末尾から探す
find() の逆方向です。reverse() してから探す必要がなくなりました。
const logs = [
{ level: 'info', msg: '開始' },
{ level: 'error', msg: '接続失敗' },
{ level: 'info', msg: '再試行' },
{ level: 'error', msg: 'タイムアウト' },
];
// 最後に起きたエラーを取る
const lastError = logs.findLast((log) => log.level === 'error');
console.log(lastError.msg); // 'タイムアウト'
const index = logs.findLastIndex((log) => log.level === 'error');
console.log(index); // 3
flatMap():変換と平坦化を同時に行う
ES2019の機能ですが、使用頻度が高いので整理しておきます。map() の結果を1段階だけ平坦化します。
const customers = [
{ name: 'Alice', purchases: ['Book', 'Pen'] },
{ name: 'Bob', purchases: ['Notebook', 'Pencil'] },
];
const all = customers.flatMap((c) => c.purchases);
console.log(all); // ['Book', 'Pen', 'Notebook', 'Pencil']
知られていない使い方として、空配列を返すことで「その要素を捨てる」ことができます。filter() と map() を1回で済ませられます。
const input = ['1', 'abc', '3', '', '5'];
// 数値に変換できるものだけを数値にして残す
const numbers = input.flatMap((s) => {
const n = Number(s);
return s !== '' && !Number.isNaN(n) ? [n] : [];
});
console.log(numbers); // [1, 3, 5]
平坦化されるのは1段階だけです。それ以上の入れ子には flat(Infinity) を使ってください。
at():末尾から要素を取る
const fruits = ['apple', 'banana', 'cherry', 'date'];
fruits.at(-1); // 'date'
fruits[fruits.length - 1]; // 従来の書き方
'hello'.at(-1); // 'o' ← 文字列にも使える
存在しないインデックスでは undefined が返ります。 空配列に at(-1) を呼んでもエラーにはならないので、その後の処理でオプショナルチェーンを使ってください。
const last = list.at(-1);
console.log(last?.name ?? '該当なし');
実践:組み合わせて使う
グループ化してから集計し、並び替える——という処理は頻出です。
const orders = [
{ id: 1, category: 'book', price: 1200 },
{ id: 2, category: 'food', price: 800 },
{ id: 3, category: 'book', price: 2400 },
{ id: 4, category: 'game', price: 5800 },
{ id: 5, category: 'food', price: 450 },
];
// カテゴリごとにまとめて、売上合計を出し、多い順に並べる
const byCategory = Object.groupBy(orders, (o) => o.category);
const summary = Object.entries(byCategory)
.map(([category, items]) => ({
category,
count: items.length,
total: items.reduce((sum, o) => sum + o.price, 0),
}))
.toSorted((a, b) => b.total - a.total);
console.table(summary);
/*
| category | count | total |
| game | 1 | 5800 |
| book | 2 | 3600 |
| food | 2 | 1250 |
*/
toSorted() を使っているので、途中で作った配列を壊しません。Reactの描画処理の中でそのまま書いても安全です。
対応状況
| 機能 | Chrome / Edge | Firefox | Safari | Node.js |
|---|---|---|---|---|
toSorted() 等(ES2023) |
110 | 115 | 16 | 20 |
Object.groupBy()(ES2024) |
117 | 119 | 17.4 | 21 |
いずれも2024年3月には全モダンブラウザで使える状態になっており、ポリフィルは不要です。
まとめ
array.groupBy()は存在しません。Object.groupBy(array, fn)かMap.groupBy(array, fn)Object.groupBy()のキーは文字列化され、戻り値はプロトタイプなし。Object.hasOwn()で判定する- キーの型を保ちたい、オブジェクトをキーにしたいなら
Map.groupBy() - ES2023の
toSorted()/toReversed()/toSpliced()/with()は元の配列を壊しません toSorted()でも比較関数は必要(省略すると文字列比較)findLast()で末尾から探せるflatMap()で空配列を返すと、その要素を捨てられるflatMap()(ES2019)とat()(ES2022)は新機能ではない
非同期処理の配列操作についてはPromise.any() の使い方、WeakMap の使いどころはJavaScriptのWeakMapの機能と活用例にまとめています。