JavaScriptでの配列操作において、もはやforループを書く必要はほとんどありません。現在のJavaScriptには、ネストされた煩雑なデータを、クリーンで読みやすい変換へと変える表現力豊かなツールが備わっています。このガイドでは、基礎を終えた次のステップとして、平坦化(flattening)、集約(reducing)、クエリ(querying)、そして元のデータを変更しない新しいイミュータブル(不変)なパターンに焦点を当てて解説します。

flat()flatMap() によるネストされた構造の平坦化

実世界のデータはネストされがちです。APIからレコードを取得したり、CSVをパースしたり、設定ファイルを読み込んだりすると、配列の中に配列が入っている状態によく遭遇します。flat() は、それらの階層を単一の線形リストにまとめるために存在します。

ネストされた配列に対して flat() を呼び出すと、デフォルトでは1レベル分だけ平坦化されます。

const nested = [[1, 2], [3, 4]];
const flat = nested.flat();
// [1, 2, 3, 4]

元の nested 配列は、そのままの状態に保たれます。ネストがより深い場合は、flat(2) のように深さを指定する引数を渡すか、階層数が不明な場合は flat(Infinity) を指定します。これは、再帰的なヘルパー関数を手書きするよりもずっと効率的です。

要素の変換と結果の平坦化を同時に行いたい場合は、flatMap() を使うことで一度のパスで両方を実行できます。これは各要素に対してマッピング関数を実行した後、返された配列を1レベル分だけ平坦化します。.map().flat() と別々に呼び出すことでも実現可能ですが、flatMap() は中間配列をスキップするため、メモリを節約でき、2回目の反復も不要になります。典型的なユースケースは、文章の配列を個々の単語に分割することです。

const lines = ['hello world', 'foo bar'];
const words = lines.flatMap(line => line.split(' '));
// ['hello', 'world', 'foo', 'bar']

フィルタリングとマッピング:その違いを理解する

filter() は、テストを通過したアイテムのみを含む新しい配列を作成します。コールバック関数は、要素を保持する場合はtruthyな値を、破棄する場合はfalsyな値を返す必要があります。元の配列を破壊(mutate)することはありません。

const users = [
  { name: 'Ajay', active: true },
  { name: 'Bina', active: false }
];
const activeUsers = users.filter(u => u.active);

filter()map() は、どちらも新しい配列を返し、コールバックを受け取るため、混同しがちです。しかし、その「契約(contract)」に違いがあります。map()同じ長さの新しい配列を返します。すべての要素を変換し、条件チェックを実行すると、すべてのスロットに true または false が入ることになります。なぜなら、map() は要素を破棄せず、変更するだけだからです。一方、filter() は、テストを通過した要素のみを含む、より短い配列を返す可能性があります。データを削除したい場合は filter() を、要素数を維持したまますべてのデータを変換したい場合は map() を使用してください。

reduce() による配列の集約

map()filter() が配列を返すのに対し、reduce() はリスト全体を1つの値へと集約します。これは左から右へと進みながら、各ステップでアキュムレータ(累積値)を引き継いでいきます。

const totals = [10, 20, 30];
const sum = totals.reduce((acc, current) => acc + current, 0);
// 60

上記の例にある 0 は、第2引数である初期のアキュムレータの値です。これを省略すると、JavaScriptは最初の要素をスタート地点として使用し、インデックス1から反復を開始します。配列が空の場合、初期値なしで reduce() を呼び出すと TypeError が発生するため、ここでつまずく人が多くいます。

reduce は算術計算のためだけのものではありません。出現頻度テーブルの構築、オブジェクトの結合、最大値の検索、あるいは複数の変換をパイプラインのように通すことにも利用できます。非常に柔軟であるため、コールバック関数は小さく保ち、変数名を明確にすることが重要です。複雑すぎる reduce() ブロックは、それが置き換えたはずのループよりも読みづらくなってしまいます。

真偽値によるクエリ:every()some()

新しい配列が必要なわけではなく、コレクションに対して「はい」か「いいえ」の答えが必要な場合があります。every() は、すべての要素が条件を満たす場合にのみ true を返します。最初の要素が条件を満たさない場合、即座に処理を停止して false を返します。

const scores = [85, 92, 78];
const allPassed = scores.every(s => s >= 50);

some() はその鏡合わせのような存在です。少なくとも1つの要素がテストを通過すれば true を返します。一致するものが見つかった時点で、その場でショートサーキット(処理を中断)します。

const hasPerfectScore = scores.some(s => s === 100);

これらのメソッドは、バリデーションチェックのための手動ループに代わるものです。すべてのフォームフィールドが入力されているか確認したい場合は every() を、いずれかのユーザーに管理者権限があるか知りたい場合は some() を使用してください。

ミューテーション(破壊的変更)なしでの検査と更新

モダンなJavaScriptは、イミュータビリティ(不変性)を非常に重視しています。これらのメソッドを使用すると、元の配列に触れることなく、配列の検査や更新を行うことができます。

keys() は、配列のインデックスを含むイテレータオブジェクトを返します。entries() は、[index, value] のペアのイテレータを返します。これらは、クリーンな分割代入を行いたい for...of ループの中で真価を発揮します。

const fruits = ['apple', 'banana'];
for (const [index, fruit] of fruits.entries()) {
  console.log(index, fruit);
}

with() は言語に追加された比較的新しい機能で、指定したインデックスの要素を置き換え、新しい配列を返します。これはブラケットによる代入(arr[index] = value)と同じ目的を果たしますが、元の配列を変更せずに(不変性を保ちながら)処理を行います。直接的なミューテーション(変更)が変更検知を妨げる React のようなフレームワークでは、with() を使う方が安全な方法です。

const original = [10, 20, 30];
const updated = original.with(1, 99);
// original: [10, 20, 30]
// updated:  [10, 99, 30]

スプレッドとレスト:... の2つの方向性

スプレッド構文とレストパラメータは同じ3つのドット(...)を使用しますが、データの動きは逆方向です。

スプレッド演算子は、配列を展開して個々の要素にします。配列を結合したり、クローンを作成したりする際に最もクリーンな方法です。

const front = [1, 2];
const back = [3, 4];
const merged = [...front, ...back];

const copy = [...merged];

また、以下のように追加の要素を簡単に挟み込むこともできます:[0, ...front, 99, ...back]

レストパラメータは、複数の要素を単一の配列にまとめます。関数の引数定義の中で使用すると、残りのすべての引数を収集します。

function logFirst(first, ...rest) {
  console.log('First:', first);
  console.log('Rest:', rest);
}
logFirst('a', 'b', 'c');
// First: a
// Rest: ['b', 'c']

これらを組み合わせることで、可変長引数を受け取り、配列として処理し、それを再び関数の呼び出しや新しいデータ構造へと展開するという、よくあるタスクをこなすことができます。

本当に重要なこと

これらのメソッドの目的は、ワンライナー(一行のコード)でチームメイトを感心させることではありません。意図を明確にすることです。他の開発者が filter() を見れば、要素を削除しようとしていることがわかります。reduce() を見れば、データを集約しようとしていることがわかります。with() を見れば、元の配列がどこかでそのまま保持されていることがわかります。これらのツールを使いこなすということは、コード自体が説明的なものになるということであり、それはどんな巧妙なループを書くよりもはるかに多くの時間を節約することにつながります。