JavaScript
es6

【JavaScript】関数を使いこなす(高階関数, 再帰関数)

More than 1 year has passed since last update.

今回は、関数を使いこなせることを目標に、
スコープ(名前空間)を扱うだけでなく、
多様な処理を抽象化し、まとまりを持たせることが可能な、
高階関数と再帰関数という概念について扱ってみる。
前回の内容はこちらから↓↓↓
【JavaScript】即時関数, 無名関数, クロージャについて

高階関数

関数をファーストクラスのオブジェクト(第一級オブジェクト)と捉える

JavaScriptでは、上記のように関数が第一級オブジェクトであるため、
以下のようなことが可能

  • リテラルを介して作成ができる
  • 変数や配列の要素, その他のオブジェクトのプロパティとして扱える
  • 引数として関数を渡せる
  • 関数を戻り値にできる などなど...

高階関数とは?

関数を引数, 戻り値として扱う関数
高階関数では、前述したJavaScriptの特徴である
引数として関数を渡せる, 関数を戻り値にできる
ことに注目している。
言葉だけで説明するのは難しいので、具体的なコードを用いて解釈してみる

kokai.js
const sample = (ary, func) => {
    let newAry = [];
    for (let value of ary) {
        const newValue = func(value);
        newAry.push(newValue);
    };
    return newAry;
};

const ary = [1, 2, 3, 4, 5];

const ary1 = sample(ary, x => x + x);
const ary2 = sample(ary, x => x * x);

console.log(ary1); // => [2, 4, 6, 8, 10]
console.log(ary2); // => [1, 2, 4, 9, 16, 25]

この例でいうと、
アロー関数で定義した関数sampleが高階関数
sampleには、
x => x + xx => x * x
という関数 (関数に渡された関数をコールバック関数とも呼ぶ) が引数として渡されている。
その結果、ary1とary2では、異なる配列となった。
今回の場合、まず大枠となる関数sampleを定め、引数で実際に変数を操作する関数を渡すことで、上手くまとまりがある書き方ができた。
このように、高階関数では、
【大枠の機能を定義しておくことで、詳細な機能は自由に変更できる】

高階関数を用いた便利なメソッド達

配列に備わっているメソッドのうち、高階関数を利用した便利なメソッドが存在する。

  • forEach:配列の要素を順番に取り出す
  • map:配列の要素を順番に取り出し、それぞれを加工した値を1つの配列として返す
  • filter:配列の要素を順番に取り出し、条件に合うものだけを配列にして返す
  • sort:配列の要素を順番に取り出し、条件によって並び替え、配列にして返す
  • reduce:隣り合う 2 つの配列要素に対して(左から右へ)同時に関数を適用し、単一の値を返す

具体例で比べる方がわかりやすい(はず)

forEach.js
let ary = [1, 2, 3, 4, 5];
ary.forEach((x, index, array) => {
    array[index] = x * x;
});
console.log(ary); // => [1, 4, 9, 16, 25]
map.js
const ary = [1, 2, 3, 4, 5];
const newAry = ary.map(x => x * x);
console.log(newAry); // => [1, 4, 9, 16, 25]
filter.js
const ary = [1, 2, 3, 4, 5];
const newAry = ary.filter(x => x > 3);
console.log(newAry); // => [4, 5]
sort.js
const ary = [1, 2, 3, 4, 5];
const newAry = ary.sort((x, y) => y - x);
console.log(newAry); // => [5, 4, 3, 2, 1]
reduce.js
const ary = [1, 2, 3, 4, 5];
const result = ary.reduce((x, y) => x + y);
console.log(result); // => 15

再帰関数

だいぶ話は、変わってしまうが
もう一つ、まとまりをもたらすことができる代表的なものに再帰関数がある。
簡単に言うと、関数が自分自身を呼ぶということ

よくデメリットがあげられる再帰関数ですが、再帰関数にすることでわかりやすくなることもある。
再帰の例でよく使われる階乗を算出する関数を作り、検証してみる。

kaijo.js
function factorial(num) {
    return num == 1 ? num : num * factorial(num - 1);
}
console.log(factorial(5)); // => 120

関数factorialを定義して、この関数内でさらに関数factorialを呼んでいる。
同じことをfor文でやってみる。

kaijo2.js
let factorial = 1;
for(let num = 1; num < 6; num += 1) {
    factorial = num * factorial
}
console.log(factorial); // => 120

一回だけの計算なら、たしかにfor文でも良いかもしれないが、
この計算が頻出であるとするとかなり辛い...
といった感じで再帰関数が役立つケースもある。
が、デメリットも多いので、よく考えて使うことをオススメ。

再帰によるスタックオーバーフロー

JavaScriptは、基本的にシングルスレッドで実行されている。(webworkerとかは一旦置いておく。)
関数を実行すると、その関数はメモリ上に乗っかることになるが、
この際に、関数内で別の関数を実行すると、さらに別領域にのメモリをしようすることになる。
加えて、スタック構造(LIFO)で処理されていくので、関数内で更に関数を実行すればその分メモリを使用することになる。
故に、メモリ消費が激しく、再帰しすぎるとスタックオーバーフローを起こしやすくなる。
あと、for文の方がシンプルに早い。
ので、再帰関数はあまり使われないことが多い。
再帰で書くもとで、問題がシンプルになる場合に絞って書くことをオススメ。。。

まとめ

関数をうまく使うことで、問題がシンプルになるケースが多々ある。
高階関数や再帰関数を使って、シンプルに少ないコードでプログラムを組めると楽しい。
次回は、部分最適, カリー化について書くことにする。
書いた → 【JavaScript】部分適用とカリー化

参考書籍, 記事

書籍
JavaScriptパターン
開眼!JavaScript
Effective JavaScript
徹底マスター!JavaScriptの教科書

記事
JavaScript 高階関数を説明するよ
【Javascript】「高階関数」を使ってみる
再帰関数を学んでワンランク上のJavaScriptエンジニアになろう!
再帰関数をマスターする
末尾再帰による最適化