配列には便利なメソッドが最初から揃っている
JavaScriptの配列には、豊富な組み込みメソッドが用意されています。値を変換したり、条件に合うものだけを取り出したり、合計を出したり──forループで書きがちな処理のほとんどは、専用のメソッドを使えば一行で済み、読みやすく、他のメソッドともきれいにつながります。
まず押さえておきたいのがmap、filter、reduceの3つ。この3つと、あといくつか仲間のメソッドを覚えるだけで、ループだらけだったコードがひと目で意味のわかる形に一気に縮まります。
各メソッドはコールバックを受け取って何かを返します。どれも nums を書き換えていない点に注目してください。これは早い段階で体に染み込ませておきたいポイントです。
map の使い方:すべての要素を変換する
map は関数を受け取り、配列の各要素に対してその関数を呼び出し、戻り値を集めて同じ長さの新しい配列を作ります。「入力1つにつき出力1つ」が欲しいときに使うメソッドです。
コールバックの第2引数ではインデックスも受け取れます(arr.map((item, i) => ...))。使わないなら無視してOKです。
よくあるミスは、戻り値の配列が不要な場面で map を使ってしまうこと。各要素をただ出力したいだけ、あるいはDBに保存したいだけなら、forEach か普通のループで十分です。
filter の使い方:条件に合う要素だけを残す
filter は各要素に対して述語関数(true か false を返す関数)を実行し、真を返したものだけを残します。返ってくる新しい配列の長さは、元と同じか短くなります。
map と filter はチェーンさせると自然につながります。チェーンは前から順に読んでいけば、パイプラインとしてそのまま理解できます。
まず filter で絞り込んでから map にかける。こうすれば map は生き残った要素だけを処理すればよくなります。
reduce の使い方:配列を1つの値に畳み込む
3つの中で最も汎用性が高いのが reduce です。(accumulator, item) => newAccumulator というリデューサ関数と初期値を渡すと、配列を順にたどりながら、各要素とそれまでの累積値をリデューサに渡していき、最終的な累積値を返してくれます。
結果が数値である必要はありません。オブジェクト、別の配列、文字列など、積み上げて作れるものなら何でも OK です。
初期値(第二引数)は必ず渡しましょう。省略すると reduce は最初の要素をアキュムレータの初期値として使ってしまい、空配列でエラーになったり、そもそも意図した動作にならないことがよくあります。
reduce は強力ですが、ロジックが複雑になると途端に読みにくくなります。リデューサーが数行を超えるようなら、素直に for...of ループで書いた方がわかりやすいことが多いです。
forEach:副作用専用、戻り値なし
forEach は、配列を返さない map のようなものです。各要素に対して何か 処理を実行したい ときに使います。たとえばログ出力、API 呼び出し、DOM の更新などですね。新しい配列が必要ないケース向けのメソッドです。
覚えておきたいポイントは2つです。
forEachはundefinedを返します。後ろに.map()をチェーンすることはできません。forEachの途中でbreakはできません。早期に抜けたいときはfor...ofかsome/everyを使いましょう。
もし arr.forEach(x => results.push(transform(x))) のようなコードを書きそうになったら、それは map の出番です。
find と findIndex:ひとつだけ欲しいとき
find は条件に最初にマッチした要素を返し、見つからなければ undefined を返します。findIndex はそのインデックス(見つからない場合は -1)を返します。
find は最初に一致したところで処理を止めます。filter(...)[0] のような書き方は、残りまで全部走査してから捨てることになるので避けましょう。
some と every:配列を真偽値で判定する
some は、少なくとも1つの要素が条件を満たせば true を返します。一方 every は、すべての要素が条件を満たしたときだけ true になります。
どちらも短絡評価です。some は最初に true になった時点で止まり、every は最初に false が出た時点で止まります。「どれか一つでも該当するか?」「全部該当するか?」を判定したいときにぴったりのメソッドです。
slice と splice の違い:コピーか、切り出しか
名前が似ていて紛らわしいのですが、この2つは動きが全然違います。
slice(start, end) は配列の一部を浅くコピーして返します。元の配列には一切手を加えません。end は含まれない点に注意してください。省略すると末尾までが対象になります。
splice(start, deleteCount, ...items) は元の配列を直接書き換える破壊的メソッドです。start の位置から deleteCount 個の要素を取り除き、必要なら新しい要素を挿入します。戻り値は取り除かれた要素の配列です。
覚え方はこう。slice は安全(コピーを返す)、splice は配列を直接手術する(その場で書き換え)。
破壊的メソッドと非破壊的メソッドの違い
この区別はかなり重要です。共有している配列をうっかり書き換えてしまうバグは、原因を突き止めるのが本当に面倒なタイプのバグの一つです。
破壊的メソッド(元の配列を変更し、戻り値は別のものを返すことが多い):
push,pop,shift,unshiftsplice,sort,reversefill,copyWithin
非破壊的メソッド(新しい配列や値を返し、元の配列はそのまま):
map,filter,slice,concatflat,flatMapfind,findIndex,some,every,includes,indexOfreduce,reduceRight
特に気をつけたいのが sort と reverse の2つ。見た目は無害そうなのに、こっそり元の配列を書き換えてきます。ソート済みのコピーが欲しいときは、先に slice してから並べ替えましょう。
モダンな JavaScript には、これらの非破壊的メソッドのペアとして toSorted・toReversed・toSpliced・with が用意されています。新しい配列を返すので、元の配列には手を付けません。今どきのランタイムならどれでも動くので、使える環境なら積極的に使いましょう。
flat と flatMap
flat はネストされた配列を 1 階層だけ平坦化します(深さを引数で指定すれば、それ以上もいけます)。flatMap は map の後に 1 階層分の flat をかけたもので、1 つの要素から 0 個以上の結果を生成したいときに便利です。
flatMap は「入力1つに対して出力が複数」というような要素を展開したいときに、わざわざ flat() を挟まずに済むスッキリとした書き方です。
実例で組み合わせてみる
ちょっと現実的な例を見てみましょう。注文リストから、完了済みで $50 を超えるものだけを対象に、合計売上を求めてみます:
3 つのメソッドを組み合わせるだけで、ループまわりの面倒な管理なしにパイプラインが完成します。各ステップが「何をしているか」をそのまま物語ってくれるのが魅力です。2 つの filter は 1 つにまとめてもいいのですが、分けておいた方が読みやすく、デバッグのときにも助かることがあります。
次のテーマ:Map と Set
配列は順番のあるデータを扱うのは得意ですが、キーで高速に値を引きたいときや、重複のない集合がほしいときには少し扱いにくいものです。JavaScript には、まさにその用途のために用意された組み込みのデータ構造 Map と Set があります。次のページではこの 2 つを見ていきましょう。
よくある質問
map・filter・reduceの違いは?
mapは各要素を変換して、同じ長さの新しい配列を返します。filterは条件に合う要素だけを残して、(大抵は短くなった)新しい配列を返します。reduceは配列を頭からなめていって、最終的に1つの値にまとめる処理です。合計値、オブジェクト、別の配列など、何にでも畳み込めます。
forEachとmapはどう使い分ける?
forEachは各要素に対して関数を実行するだけで、戻り値はundefinedです。つまり副作用を起こすためのメソッド。一方mapは各要素に関数を適用して、その結果を新しい配列として返します。変換後の配列が欲しいならmap、単に要素ごとに何か処理したいだけならforEach(またはfor...of)を使うのが自然です。
元の配列を変更してしまうメソッドはどれ?
破壊的メソッド(元の配列を書き換えるもの)はpush、pop、shift、unshift、splice、sort、reverse、fill、copyWithinです。それ以外のmap、filter、slice、concat、flat、flatMap、find、some、every、reduceなどは、元の配列には手を付けず新しい値を返します。
sliceとspliceはどっちを使えばいい?
slice(start, end)は配列の一部を浅くコピーして返すだけで、元の配列には触りません。splice(start, deleteCount, ...items)は破壊的で、要素をその場で削除・挿入し、削除した要素を返します。覚え方は「sliceは安全、spliceは切った貼った」です。