関数は「値」である
JavaScriptでは、関数も数値や文字列と同じように、ひとつの値として扱えます。変数に入れたり、配列に詰めたり、他の関数に引数として渡したり、関数から返したりできるわけです。この性質を押さえておくだけで、プログラミングの幅がぐっと広がります。
関数を値として扱うことに慣れてくると、高階関数もそれほど難しく感じなくなります。高階関数とは、関数を引数として受け取る、関数を返す、あるいはその両方を行う関数のことです。定義としてはそれだけ。
関数を引数として渡す
一番よく見るパターンは、コールバック関数を受け取って代わりに実行してくれる関数です。実は意識しないうちに、すでに使っているはずです。
forEach は高階関数の代表例で、渡した関数を要素ごとに 1 回ずつ呼び出してくれます。setTimeout も同じく高階関数で、こちらは指定した時間が経ってから関数を実行します。開発者は 何をするか だけを書き、いつ 何回 実行するかはこれらの関数に任せればいい、というわけです。
自分で高階関数を書くときも考え方はまったく同じです。次のコードは、条件が true のときだけコールバックを実行する小さな関数の例です:
action は、たまたま関数を値に持っているだけのただのパラメータです。action() と呼び出せば、渡された中身がそのまま実行されます。
実際によく使う3つ: map・filter・reduce
配列には高階メソッドが用意されていて、普段書いている for ループのほとんどを置き換えられます。この3つを覚えるだけで、日常のコードがぐっと短く、読みやすくなります。
map — すべての要素を変換する
map は配列の要素ごとに関数を 1 回ずつ呼び出し、その戻り値を集めて新しい配列を作ります。長さは同じで、中身だけが変換されるイメージです。元の配列はそのまま、変更されません。
filter — 条件に合うものだけを残す
filter は、コールバックが真値(truthy)を返した要素だけを残して、それ以外は捨てていきます。戻り値は新しい配列で、元より短くなることもあります。
reduce — 配列を1つの値にまとめる
reduce は万能選手です。コールバックには「これまでの累積値」と「今の要素」が渡ってきて、return した値が次の累積値になります。第2引数(ここでは 0)は初期値ですね。
これらはチェーンしてつなげられます。このスタイルの真価が発揮されるのはまさにここ。
上から下へ素直に読むだけ。支払い済みの注文を絞り込み、価格を取り出し、合計する。ループもなければ、書き換える変数も、オフバイワンの心配もない。
関数を返す関数
高階関数のもう一つの顔がこれ。別の関数を組み立てて返してくれる関数です。
multiplyBy(2) を一度呼び出すと、新しい関数が返ってきます。その返された関数は factor の値をちゃんと覚えていて、これがいわゆる クロージャ です。クロージャについては別のページで詳しく扱うので、ここではひとまず「multiplyBy に違う引数を渡すだけで、同じテンプレートから用途別の関数をいくつでも作れる」とだけ押さえておけばOKです。
このパターンは本当にいろんな場面で登場します。
定義はひとつ、再利用できる関数はふたつ。warn と info を別々に手書きして、あとから同期を取る手間を考えたら、こっちのほうが断然ラクです。
名前付き関数とインラインのコールバック関数
コールバック関数は、その場でアロー関数を書いて渡してもいいし、名前で呼び出してもOKです。どちらも動くので、読みやすいほうを選べば問題ありません。
isEvenを(カッコなしで)渡すと、関数そのものを引き渡すことになります。()を付けてしまうとその場で呼び出されて、戻り値 が渡されてしまう——初心者がやりがちなミスです。
nums.filter(isEven); // 正しい: 関数を渡している
nums.filter(isEven()); // 誤り: 引数なしで isEven を呼び出し、その結果を渡している
コールバックが数行を超えて長くなりそうなら、思い切って外に出して名前を付けましょう。だいたいの場合、呼び出し側のコードも読みやすくなります。
実際に書いてみる
高階関数の真価は、小さな関数を組み合わせて使うときに発揮されます。たとえば、商品のリストから「手頃な価格」かつ「在庫あり」のものだけを抜き出し、名前を大文字にしたい、というケースを考えてみましょう。
ヘルパー関数はそれぞれ一つの役割だけを持ち、配列メソッドもそれぞれ一つの変換だけを担当します。こうしてできたパイプラインは、「どうループを回すか」ではなく「何をしたいか」の仕様書のように読めるわけです。
高階関数を使うべきでない場面
高階関数は便利ですが、どんなループでも置き換えられる万能薬ではありません。
- 途中で処理を打ち切りたい場合は、
forEachから無理やり抜け出すより、forやfor...ofにbreakを組み合わせるほうがスッキリ書けます。 - コールバックの中で非同期処理をしたい場合、
mapやforEachはawaitしてくれません。for...ofとawaitを組み合わせるか、mapとPromise.allをセットで使いましょう。 - コールバックが共有状態を書き換えているなら、そもそもこのスタイルの良さから外れています。普通のループに戻すか、新しい値を返す形にリファクタリングしましょう。
ハマる場面で使えば、map・filter・reduce は日々のコードからループの定型文をごっそり削ってくれます。ただ、何でもかんでも高階関数で書こうとすると、逆に読みにくくなってしまいます。意図が一番はっきり伝わる道具を選びましょう。
次回:オブジェクト
積み上げていく価値がある値は、関数だけではありません。関連するデータとふるまいをまとめるための JavaScript の主役といえばオブジェクトです。さっきまで filter や map で扱っていた配列の中身も、ほとんどがオブジェクトだったはずです。次のページではそのオブジェクトを見ていきます。
よくある質問
JavaScriptの高階関数とは何ですか?
高階関数とは、「関数を引数として受け取る」「関数を戻り値として返す」のうち、少なくとも片方を満たす関数のことです。Array.prototype.map、setTimeout、addEventListenerなどはいずれも高階関数で、渡したコールバック関数を内部で呼び出してくれます。
map・filter・reduceの違いは?
mapは各要素を変換して、元と同じ長さの新しい配列を返します。filterはコールバックが真を返した要素だけを残すので、結果の配列は元より短くなることがあります。reduceは配列を1つの値に畳み込む関数で、要素を順番に組み合わせて最終結果を作ります。いずれもコールバックを受け取る高階関数です。
関数から関数を返すのはどんな場面で役立ちますか?
同じロジックを繰り返さずに、設定違いの小さなヘルパー関数を量産したいときに便利です。たとえばmultiplyBy(n)という関数はn倍する新しい関数を返すので、multiplyBy(2)やmultiplyBy(10)のように呼び出すだけで専用関数が手に入ります。これはクロージャを活用したパターンで、イベントハンドラやミドルウェア、ユーティリティライブラリでもよく使われます。