JavaScriptの配列は、モダンなWeb開発におけるデータ操作の基盤です。ES2023で非破壊的な代替メソッドが導入された今、配列メソッドの全体像を再確認する絶好の機会です。本記事では、おなじみのmap、filter、reduceから最新の追加機能まで、実践的なコード例とパフォーマンスの知見を交えて解説します。
配列メソッドの全体像
配列メソッドは大きく分けて、破壊的メソッドと非破壊的メソッドに分類されます。push、pop、splice、sort、reverseは元の配列を変更しますが、map、filter、slice、そしてES2023の新メソッドは新しい配列を生成します。業界全体では不変性(イミュータビリティ)へのシフトが進んでおり、意図しない副作用によるバグを減少させる効果が期待されています。
ES2023が導入した4つの非破壊的メソッドは以下の通りです:
const arr = [3, 1, 4, 1, 5, 9];
const sorted = arr.toSorted(); // [1, 1, 3, 4, 5, 9] — 新しい配列
const reversed = arr.toReversed(); // [9, 5, 1, 4, 1, 3] — 新しい配列
const spliced = arr.toSpliced(1, 2); // [3, 1, 5, 9] — 新しい配列
const updated = arr.with(2, 42); // [3, 1, 42, 1, 5, 9] — 新しい配列
console.log(arr); // [3, 1, 4, 1, 5, 9] — 元の配列は不変
ReactやReduxのような状態管理において、不変性を保証するこれらのメソッドは特に有用です。
map、filter、reduceの詳細
関数型配列メソッドの三本柱であるmap、filter、reduceは、明示的なループを使わずに表現力豊かなデータ変換を可能にします。
mapは各要素にコールバック関数を適用し、同じ長さの新しい配列を生成します:
const numbers = [1, 2, 3, 4, 5];
const squared = numbers.map(n => n * n);
// [1, 4, 9, 16, 25]
filterは条件を満たす要素だけを抽出します:
const even = numbers.filter(n => n % 2 === 0);
// [2, 4]
reduceは最も汎用性が高く、他のメソッドを実装することも可能です:
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15
よくあるアンチパターンとして、flatMapで代用できる場面での不必要なmap+filterのチェーンが挙げられます。各メソッドは中間配列を生成するため、大規模データではパフォーマンスに影響します。
| メソッド | 戻り値 | 元の配列の変更 | 用途 |
|---|---|---|---|
map | 新しい配列 | なし | 全要素の変換 |
filter | 新しい配列 | なし | 条件に合う要素の抽出 |
reduce | 任意の値 | なし | 単一値への集約 |
sort | 同じ配列 | あり | その場での並び替え |
toSorted | 新しい配列 | なし | 非破壊的な並び替え |
flatMapとgroupBy
flatMapはマッピングとフラット化を1回のパスで実行します。各入力要素が0個、1個、または複数の出力要素にマッピングされる場合に特に有効です:
const sentences = ["hello world", "foo bar"];
const words = sentences.flatMap(s => s.split(" "));
// ["hello", "world", "foo", "bar"]
Object.groupBy(ES2024)はネイティブのグルーピング機能を提供します:
const users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
];
const grouped = Object.groupBy(users, user => user.role);
これにより、従来必要だったreduceベースの手動グルーピングが不要になります。
関数型チェーンパターン
メソッドチェーンはパイプライン形式で操作を構成します:
const result = data
.filter(item => item.isActive)
.map(item => ({ ...item, score: item.value * 2 }))
.filter(item => item.score > 10)
.reduce((acc, item) => acc + item.score, 0);
各メソッドで中間配列が生成されますが、1万要素未満であればオーバーヘッドは無視できます。それ以上のサイズでは、単一のreduceパスやtransducerライブラリの使用を検討します。
findメソッドは早期終了を活用し、大規模配列ではfilter+インデックスアクセスよりも桁違いに高速です。
実践レシピ
重複の除去はSetで簡潔に記述できます:
const unique = [...new Set([1, 2, 2, 3, 3, 4])];
// [1, 2, 3, 4]
配列を指定サイズでチャンク分割するユーティリティ:
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
Fisher-Yatesアルゴリズムによる偏りのないシャッフル:
const shuffle = arr => {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
};
TypeScript統合
filterでの型述語により、型の絞り込みが可能です:
const items: (string | null)[] = ["a", null, "b"];
const strings: string[] = items.filter((x): x is string => x !== null);
複雑なreduceでは明示的なアキュムレータ型を指定します:
const grouped = items.reduce<Record<string, number[]>>((acc, item) => {
(acc[item.key] ??= []).push(item.value);
return acc;
}, {});
不変性パターン
ES2023の非破壊的メソッド(toSorted、toReversed、toSpliced、with)は、パフォーマンスが重要でないコードではデフォルトとして採用すべきです。これらは変更に伴うバグのカテゴリ全体を排除しつつ、可読性を維持します。
配列メソッドの習得は、クリーンで効率的なJavaScriptを書くための必須スキルです。日常的なタスクではmap、filter、reduceから始め、特定のパターンではflatMapやgroupByを活用し、可能な限りES2023の非破壊的メソッドをデフォルトにしましょう。
