第4版が利用可能です。こちらで読んでください

第3章関数

人々はコンピュータサイエンスは天才の芸術だと考えがちですが、実際のところは正反対です。多くの人々が互いに積み重ねることで物事を成し遂げ、それはミニ石の壁のようなものです。

Donald Knuth
Picture of fern leaves with a fractal shape

関数はJavaScriptプログラミングの基礎です。プログラムの一部分を値でラップするという概念は、多くの用途があります。より大きなプログラムを構造化し、繰り返しを減らし、名前をサブルーチンプログラムに関連付け、これらのサブルーチンプログラムを互いに分離するための方法を提供します。

関数の最も明白な用途は、新しい語彙を定義することです。散文で新しい単語を作成することは、通常、悪いスタイルです。しかし、プログラミングでは不可欠です。

典型的な大人の英語話者は、約2万語の語彙を持っています。2万ものコマンドを組み込んで提供されているプログラミング言語はほとんどありません。そして、実際に利用可能な語彙は、人間の言語よりも正確に定義されており、そのため柔軟性が低くなっています。したがって、自分自身をあまり繰り返さないように、通常は新しい概念を導入する必要があります。

関数の定義

関数定義は、バインディングの値が関数である通常のバインディングです。たとえば、このコードは、与えられた数の2乗を生成する関数を参照するようにsquareを定義します。

const square = function(x) {
  return x * x;
};

console.log(square(12));
// → 144

関数は、functionキーワードから始まる式で作成されます。関数はパラメータ(この場合はxのみ)と本体を持ち、本体には関数が呼び出されたときに実行されるステートメントが含まれています。このように作成された関数の関数本体は、単一のステートメントで構成されている場合でも、常に中括弧で囲む必要があります。

関数は、複数のパラメータを持つことも、パラメータを持たないこともできます。次の例では、makeNoiseはパラメータ名をリストせず、powerは2つリストしています。

const makeNoise = function() {
  console.log("Pling!");
};

makeNoise();
// → Pling!

const power = function(base, exponent) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

console.log(power(2, 10));
// → 1024

一部の関数は、powersquareなど、値を生成し、makeNoiseなど、副作用のみが結果となるものもあります。returnステートメントは、関数が返す値を決定します。制御がこのステートメントに遭遇すると、現在の関数からすぐにジャンプし、返された値を関数を呼び出したコードに渡します。式が後に続かないreturnキーワードは、関数がundefinedを返す原因となります。makeNoiseなど、returnステートメントをまったく持たない関数は、同様にundefinedを返します。

関数の引数は、通常のバインディングのように動作しますが、その初期値は関数自体ではなく、関数の呼び出し元によって与えられます。

バインディングとスコープ

各バインディングにはスコープがあり、それはバインディングが見えるプログラムの一部です。関数やブロックの外で定義されたバインディングの場合、スコープはプログラム全体です。このようなバインディングはどこでも参照できます。これらはグローバルと呼ばれます。

しかし、関数パラメータに対して作成されたバインディング、または関数内で宣言されたバインディングは、その関数内でのみ参照できます。そのため、ローカルバインディングとして知られています。関数が呼び出されるたびに、これらのバインディングの新しいインスタンスが作成されます。これにより、関数間に分離が提供されます。各関数呼び出しは独自の小さな世界(ローカル環境)で動作し、グローバル環境で何が起こっているかについて多くのことを知る必要なく、多くの場合理解できます。

letconstで宣言されたバインディングは、実際には宣言されているブロックに対してローカルです。そのため、ループ内でそのようなバインディングを作成した場合、ループの前後のコードはそのバインディングを「見ることができません」。2015年以前のJavaScriptでは、関数のみが新しいスコープを作成していたため、varキーワードで作成された古いスタイルのバインディングは、表示される関数全体、または関数内にない場合はグローバルスコープ全体で表示されます。

let x = 10;
if (true) {
  let y = 20;
  var z = 30;
  console.log(x + y + z);
  // → 60
}
// y is not visible here
console.log(x + z);
// → 40

各スコープは周囲のスコープを「見ることが」できるため、例ではブロック内でxを見ることができます。例外は、複数のバインディングが同じ名前を持つ場合です。その場合、コードは最も内側のものしか見ることができません。たとえば、halve関数の内部のコードがnを参照する場合、それはグローバルnではなく、独自のnを参照しています。

const halve = function(n) {
  return n / 2;
};

let n = 10;
console.log(halve(100));
// → 50
console.log(n);
// → 10

ネストされたスコープ

JavaScriptは、グローバルバインディングとローカルバインディングだけを区別しません。ブロックと関数は、他のブロックと関数の中に作成でき、複数の局所性の度合いを生み出します。

たとえば、ヒュムスのバッチを作るのに必要な材料を出力するこの関数は、内部に別の関数を持っています。

const hummus = function(factor) {
  const ingredient = function(amount, unit, name) {
    let ingredientAmount = amount * factor;
    if (ingredientAmount > 1) {
      unit += "s";
    }
    console.log(`${ingredientAmount} ${unit} ${name}`);
  };
  ingredient(1, "can", "chickpeas");
  ingredient(0.25, "cup", "tahini");
  ingredient(0.25, "cup", "lemon juice");
  ingredient(1, "clove", "garlic");
  ingredient(2, "tablespoon", "olive oil");
  ingredient(0.5, "teaspoon", "cumin");
};

ingredient関数の内部のコードは、外部関数のfactorバインディングを見ることができます。しかし、unitingredientAmountなどのローカルバインディングは、外部関数では見ることができません。

ブロック内で見えるバインディングの集合は、プログラムテキストにおけるそのブロックの位置によって決定されます。各ローカルスコープは、それを含むすべてのローカルスコープ、そしてすべてのスコープはグローバルスコープを見ることができます。このバインディングの可視性へのアプローチは、レキシカルスコープと呼ばれます。

値としての関数

関数バインディングは、通常、プログラムの特定の部分の名前として機能します。このようなバインディングは一度定義され、変更されることはありません。そのため、関数とその名前を混同しやすくなります。

しかし、この2つは異なります。関数値は、他の値ができることをすべて実行できます。つまり、それを呼び出すだけでなく、任意の式で使用できます。関数値を新しいバインディングに格納したり、それを関数に引数として渡したりすることができます。同様に、関数を保持するバインディングは、依然として通常のバインディングであり、定数でない場合は、次のように新しい値を代入できます。

let launchMissiles = function() {
  missileSystem.launch("now");
};
if (safeMode) {
  launchMissiles = function() {/* do nothing */};
}

第5章では、関数値を他の関数に渡すことによって行うことができる興味深いことについて説明します。

宣言表記

関数バインディングを作成する方法は、もう1つ少し短い方法があります。ステートメントの先頭でfunctionキーワードを使用すると、動作が異なります。

function square(x) {
  return x * x;
}

これは関数宣言です。このステートメントは、バインディングsquareを定義し、それを指定された関数にポイントします。書き方が少し簡単で、関数の後にセミコロンは必要ありません。

この形式の関数定義には、1つの微妙な点があります。

console.log("The future says:", future());

function future() {
  return "You'll never have flying cars";
}

関数が使用しているコードのに定義されている場合でも、上記のコードは機能します。関数宣言は、通常のトップダウンの制御フローの一部ではありません。概念的には、スコープの先頭に移動され、そのスコープ内のすべてのコードで使用できます。これは、使用前にすべての関数を定義する必要を心配することなく、意味のあるように見える方法でコードの順序を自由に設定できるため、便利な場合があります。

アロー関数

関数には3番目の表記法があり、他の表記法とは大きく異なります。functionキーワードの代わりに、等号と大于号で構成されたアロー(=>)を使用します(大于号または等号演算子と混同しないでください。大于号または等号演算子は>=と書かれています)。

const power = (base, exponent) => {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
};

アローはパラメータリストのにあり、関数の本体が続きます。これは、「この入力(パラメータ)はこの結果(本体)を生成する」のようなことを表しています。

パラメータ名が1つしかない場合は、パラメータリストを囲む括弧を省略できます。本体が中括弧内のブロックではなく、単一の式である場合、その式は関数から返されます。したがって、これらの2つのsquareの定義は同じことを行います。

const square1 = (x) => { return x * x; };
const square2 = x => x * x;

アロー関数がパラメータをまったく持たない場合、そのパラメータリストは空の括弧の集合です。

const horn = () => {
  console.log("Toot");
};

アロー関数と`function`式を両方とも言語に含める深い理由は特にありません。第6章で説明する些細な点を除けば、両者は同じ動作をします。アロー関数は2015年に追加され、主に簡潔な方法で小さな関数式を書くことを可能にしました。第5章では、アロー関数を多用します。

コールスタック

関数を介した制御の流れは、やや複雑です。詳しく見ていきましょう。いくつかの関数呼び出しを行う単純なプログラムを以下に示します。

function greet(who) {
  console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

このプログラムの実行は、おおよそ次のように進みます。`greet`への呼び出しにより、制御はその関数の先頭(2行目)にジャンプします。関数は`console.log`を呼び出し、制御を受け取り、その処理を行い、制御を2行目に返します。そこで`greet`関数の終わりに到達するため、それを呼び出した場所(4行目)に戻ります。その次の行は`console.log`を再び呼び出します。それが戻った後、プログラムは終了します。

制御の流れを模式的に示すと次のようになります。

not in function
   in greet
        in console.log
   in greet
not in function
   in console.log
not in function

関数は戻るときにそれを呼び出した場所にジャンプする必要があるため、コンピューターは呼び出しが行われたコンテキストを記憶する必要があります。あるケースでは、`console.log`は完了時に`greet`関数に戻る必要があります。別のケースでは、プログラムの終わりに戻ります。

コンピューターがこのコンテキストを格納する場所は、コールスタックです。関数が呼び出されるたびに、現在のコンテキストがこのスタックの一番上に格納されます。関数が戻ると、スタックの一番上のコンテキストが削除され、そのコンテキストを使用して実行が続行されます。

このスタックを格納するには、コンピューターのメモリに領域が必要です。スタックが大きくなりすぎると、コンピューターは「スタック領域不足」や「再帰が多すぎる」といったメッセージで失敗します。次のコードは、コンピューターに非常に難しい質問をして、2つの関数間で無限のやり取りを引き起こすことで、これを示しています。むしろ、コンピューターが無限のスタックを持っていれば、無限になるでしょう。現状では、領域を使い果たして、「スタックオーバーフロー」が発生します。

function chicken() {
  return egg();
}
function egg() {
  return chicken();
}
console.log(chicken() + " came first.");
// → ??

オプション引数

次のコードは許容されており、問題なく実行されます。

function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16

`square`は1つのパラメーターのみで定義しました。しかし、3つで呼び出した場合、言語は文句を言いません。余分な引数は無視され、最初の引数の2乗が計算されます。

JavaScriptは、関数に渡す引数の数について非常に寛大です。多すぎると余分な引数は無視されます。少なすぎると、不足しているパラメーターには`undefined`という値が割り当てられます。

欠点は、関数に誤った数の引数を誤って渡す可能性があることです。そして、誰もそれを教えてくれません。

利点は、この動作を使用して、異なる数の引数で関数を呼び出すことを許可できることです。たとえば、この`minus`関数は、1つまたは2つの引数に対して動作することで、`-`演算子を模倣しようとします。

function minus(a, b) {
  if (b === undefined) return -a;
  else return a - b;
}

console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5

パラメーターの後に`=`演算子と式を記述すると、その式の値が、引数が指定されていない場合にその引数を置き換えます。

たとえば、このバージョンの`power`では、2番目の引数はオプションです。それを指定しないか、`undefined`を渡すと、デフォルトで2になり、関数は`square`のように動作します。

function power(base, exponent = 2) {
  let result = 1;
  for (let count = 0; count < exponent; count++) {
    result *= base;
  }
  return result;
}

console.log(power(4));
// → 16
console.log(power(2, 6));
// → 64

次の章では、関数の本体が渡された引数のリスト全体を取得する方法を確認します。これは、関数が任意の数の引数を受け入れることができるようになるため、役立ちます。たとえば、`console.log`はこのように動作し、渡されたすべての値を出力します。

console.log("C", "O", 2);
// → C O 2

クロージャ

関数を値として扱う機能と、ローカルバインディングが関数が呼び出されるたびに再作成されるという事実を組み合わせると、興味深い疑問が生じます。それらを作成した関数呼び出しがアクティブではなくなった場合、ローカルバインディングはどうなりますか?

次のコードは、この例を示しています。ローカルバインディングを作成する関数`wrapValue`を定義します。次に、このローカルバインディングにアクセスして返す関数を返します。

function wrapValue(n) {
  let local = n;
  return () => local;
}

let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

これは許容されており、期待どおりに機能します。バインディングの両方のインスタンスにアクセスできます。この状況は、ローカルバインディングが呼び出しごとに新しく作成され、異なる呼び出しがお互いのローカルバインディングを踏みつけられないという事実の良い例です。

この機能(包含スコープ内のローカルバインディングの特定のインスタンスを参照する機能)は、クロージャと呼ばれます。周囲のローカルスコープからのバインディングを参照する関数は、クロージャと呼ばれます。この動作により、バインディングの存続期間を心配する必要がなくなるだけでなく、関数値を創造的な方法で使用することも可能になります。

わずかに変更を加えることで、任意の量を掛ける関数を生成する方法に前の例を変えることができます。

function multiplier(factor) {
  return number => number * factor;
}

let twice = multiplier(2);
console.log(twice(5));
// → 10

`wrapValue`例の明示的な`local`バインディングは、パラメーター自体がローカルバインディングであるため、実際には必要ありません。

このようなプログラムについて考えるには、練習が必要です。良いメンタルモデルは、関数値を、その本体のコードと、それらが作成された環境の両方を含むものとして考えることです。呼び出されると、関数の本体は、呼び出された環境ではなく、作成された環境を認識します。

例では、`multiplier`が呼び出され、その`factor`パラメーターが2にバインドされている環境が作成されます。それが返す関数値(`twice`に格納されている)はこの環境を記憶しています。そのため、それが呼び出されると、その引数を2倍します。

再帰

スタックをオーバーフローさせるほど頻繁に実行しない限り、関数が自身を呼び出すことは完全に問題ありません。自身を呼び出す関数は、再帰的と呼ばれます。再帰により、一部の関数を異なるスタイルで記述できます。たとえば、`power`の代替実装を以下に示します。

function power(base, exponent) {
  if (exponent == 0) {
    return 1;
  } else {
    return base * power(base, exponent - 1);
  }
}

console.log(power(2, 3));
// → 8

これは、数学者がべき乗を定義する方法にかなり近く、ループを使った変種よりも概念をより明確に記述していると言えるでしょう。関数は、繰り返し掛け算を実現するために、指数が徐々に小さくなる値で自身を複数回呼び出します。

しかし、この実装には1つの問題があります。一般的なJavaScript実装では、ループを使ったバージョンよりも約3倍遅いです。単純なループを実行する方が、関数を複数回呼び出すよりも一般的に安価です。

速度とエレガンスのジレンマは興味深いものです。人間にとっての使いやすさと機械にとっての使いやすさの間の連続体として見ることができます。ほとんどすべてのプログラムは、より大きく複雑にすることで高速化できます。プログラマーは、適切なバランスを決定する必要があります。

`power`関数の場合は、エレガントではない(ループを使った)バージョンはまだ非常にシンプルで読みやすいです。再帰バージョンに置き換える意味はほとんどありません。しかし、多くの場合、プログラムは非常に複雑な概念を扱うため、プログラムをより簡単にするために効率をある程度犠牲にすることは有益です。

効率を心配することは、気が散ることがあります。プログラム設計を複雑にするもう1つの要因であり、すでに難しいことを行っている場合、心配する必要があるその余分なことは麻痺を引き起こす可能性があります。

したがって、常に正しく理解しやすいものから書き始めましょう。遅すぎるのではないかと心配している場合(ほとんどのコードは実行頻度が低いため、かなりの時間を要することは通常ないため)、後で測定して必要に応じて改善できます。

再帰は、ループに対する非効率的な代替手段だけではありません。実際、ループよりも再帰で解決しやすい問題もあります。ほとんどの場合、これらは、それぞれがさらに多くの分岐に枝分かれする可能性のあるいくつかの「分岐」を探索または処理する必要がある問題です。

次のパズルを考えてみてください。数字の1から始めて、5を加算するか3を乗算するという操作を繰り返し行うことで、無限の数の集合を生成できます。与えられた数に対して、そのような加算と乗算のシーケンスを見つける関数を作成するにはどうすればよいでしょうか。

例えば、数字の13は、最初に3を掛けてから5を2回加えることで到達できますが、数字の15は全く到達できません。

ここに再帰的な解法を示します。

function findSolution(target) {
  function find(current, history) {
    if (current == target) {
      return history;
    } else if (current > target) {
      return null;
    } else {
      return find(current + 5, `(${history} + 5)`) ||
             find(current * 3, `(${history} * 3)`);
    }
  }
  return find(1, "1");
}

console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

このプログラムは必ずしも最短の演算シーケンスを見つけるわけではありません。どのようなシーケンスでも見つかれば満足します。

すぐに仕組みが分からなくても大丈夫です。再帰的思考の良い練習になるので、一緒に見ていきましょう。

内部関数findが実際の再帰処理を行います。この関数は、現在の数と、その数に到達する方法を記録した文字列の2つの引数を取ります。解が見つかると、ターゲットに到達する方法を示す文字列を返します。この数から始めて解が見つからない場合は、nullを返します。

そのため、関数は3つのアクションのいずれかを実行します。現在の数がターゲット数の場合、現在の履歴はそのターゲットに到達する方法であるため、返されます。現在の数がターゲット数より大きい場合、加算と乗算の両方によって数値が大きくなるだけなので、このブランチをさらに探しても意味がないため、nullを返します。最後に、ターゲット数より小さい場合は、関数自身を2回呼び出して、現在の数から始まる2つの可能なパスを試みます。1回は加算用、もう1回は乗算用です。最初の呼び出しがnullではないものを返す場合、それは返されます。そうでない場合、2番目の呼び出しが返されます。文字列を生成するかどうか、nullを生成するかに関係なく。

この関数が目的の効果を生み出す仕組みをよりよく理解するために、数字13の解を検索する際に実行されるfindへのすべての呼び出しを見てみましょう。

find(1, "1")
  find(6, "(1 + 5)")
    find(11, "((1 + 5) + 5)")
      find(16, "(((1 + 5) + 5) + 5)")
        too big
      find(33, "(((1 + 5) + 5) * 3)")
        too big
    find(18, "((1 + 5) * 3)")
      too big
  find(3, "(1 * 3)")
    find(8, "((1 * 3) + 5)")
      find(13, "(((1 * 3) + 5) + 5)")
        found!

インデントは呼び出しスタックの深さを示しています。findが最初に呼び出されると、(1 + 5)で始まる解を探索するために自身を呼び出します。その呼び出しはさらに再帰的に、ターゲット数以下の数値を生成するすべての継続的な解を探索します。ターゲットに一致する解が見つからないため、最初の呼び出しにnullを返します。そこで||演算子によって、(1 * 3)を探索する呼び出しが行われます。この検索はより幸運で、その最初の再帰呼び出しが、さらに別の再帰呼び出しを通じて、ターゲット数に到達します。その最内側の呼び出しは文字列を返し、中間呼び出しの各||演算子はそれを渡して、最終的に解を返します。

関数の増加

プログラムに関数を導入する方法は、大きく分けて2つあります。

1つ目は、似たようなコードを何度も書いていることに気づくことです。それは避けたいものです。コードが多くなると、ミスが隠れる余地が増え、プログラムを理解しようとする人のための読み物も増えます。そこで、繰り返される機能を取り出し、適切な名前を付けて、関数にまとめます。

2つ目は、まだ書いていないが、独自の関数として扱う価値のある機能が必要になることです。関数の名前を付けることから始め、次に本体を書きます。関数を実際に定義する前に、その関数を使用するコードを書き始めることさえあります。

関数の良い名前を見つけるのが難しいかどうかは、カプセル化しようとしている概念がどれだけ明確であるかの良い指標です。例を見てみましょう。

農場の牛とニワトリの数(CowsChickensの単語を後に付け、両方の数値の前にゼロをパディングして常に3桁になるようにする)を出力するプログラムを作成したいと考えています。

007 Cows
011 Chickens

これは、2つの引数(牛の数とニワトリの数)を持つ関数を要求します。コーディングを始めましょう。

function printFarmInventory(cows, chickens) {
  let cowString = String(cows);
  while (cowString.length < 3) {
    cowString = "0" + cowString;
  }
  console.log(`${cowString} Cows`);
  let chickenString = String(chickens);
  while (chickenString.length < 3) {
    chickenString = "0" + chickenString;
  }
  console.log(`${chickenString} Chickens`);
}
printFarmInventory(7, 11);

文字列式に.lengthを付けると、その文字列の長さが得られます。したがって、whileループは、数値文字列が少なくとも3文字になるまで、数値の前にゼロを追加し続けます。

ミッション完了!しかし、農家の人にコード(と高額な請求書)を送ろうとしたまさにその時、彼女は豚も飼い始めたから、豚も出力するようにソフトウェアを拡張してもらえないかと電話をかけてきました。

もちろんです。しかし、その4行をもう1回コピー&ペーストしようとしているまさにその時に、私たちは立ち止まって考え直します。もっと良い方法があるはずです。最初の試みはこちらです。

function printZeroPaddedWithLabel(number, label) {
  let numberString = String(number);
  while (numberString.length < 3) {
    numberString = "0" + numberString;
  }
  console.log(`${numberString} ${label}`);
}

function printFarmInventory(cows, chickens, pigs) {
  printZeroPaddedWithLabel(cows, "Cows");
  printZeroPaddedWithLabel(chickens, "Chickens");
  printZeroPaddedWithLabel(pigs, "Pigs");
}

printFarmInventory(7, 11, 3);

動作します!しかし、その名前printZeroPaddedWithLabelはややぎこちなく、印刷、ゼロパディング、ラベルの追加という3つのことを単一の関数にまとめています。

プログラムの繰り返し部分をそのまま持ち上げるのではなく、単一の概念を取り出してみましょう。

function zeroPad(number, width) {
  let string = String(number);
  while (string.length < width) {
    string = "0" + string;
  }
  return string;
}

function printFarmInventory(cows, chickens, pigs) {
  console.log(`${zeroPad(cows, 3)} Cows`);
  console.log(`${zeroPad(chickens, 3)} Chickens`);
  console.log(`${zeroPad(pigs, 3)} Pigs`);
}

printFarmInventory(7, 16, 3);

zeroPadのような、明確で分かりやすい名前の関数は、コードを読む人がそれが何をするのかを理解しやすくします。そして、そのような関数は、この特定のプログラムだけでなく、より多くの状況で役立ちます。例えば、数値の整然とした表を出力する際に使用できます。

関数はどれほどスマートで汎用的であるべきでしょうか?3文字幅に数値をパディングすることしかできない非常に単純な関数から、分数、負の数、小数点のアライメント、異なる文字によるパディングなどを処理する複雑な汎用数値フォーマットシステムまで、何でも書くことができます。

役に立つ原則は、絶対に必要だと確信が持てない限り、賢さを追加しないことです。遭遇するすべての機能について、一般的な「フレームワーク」を書くのは魅力的かもしれませんが、その衝動に抵抗しましょう。実際には何も仕事ができません。使わないコードを書くだけになります。

関数と副作用

関数は、その副作用のために呼び出されるものと、その戻り値のために呼び出されるものに大まかに分けられます。(副作用があって戻り値を持つことももちろん可能です。)

農場の例における最初のヘルパー関数printZeroPaddedWithLabelは、その副作用、つまり行を出力するために呼び出されます。2番目のバージョンzeroPadは、その戻り値のために呼び出されます。2番目が1番目より多くの状況で役立つのは偶然ではありません。値を作成する関数は、副作用を直接実行する関数よりも、新しい方法で組み合わせやすくなります。

純粋関数は、副作用がなく、他のコードからの副作用にも依存しない(例えば、値が変わる可能性のあるグローバルバインディングを読み取らない)特定の種類の値生成関数です。純粋関数は、同じ引数で呼び出されると常に同じ値を生成し(そして他に何も行わず)、という好ましい特性を持っています。このような関数の呼び出しは、コードの意味を変えずにその戻り値で置き換えることができます。純粋関数が正しく動作しているか確信が持てない場合は、単に呼び出してテストできます。そのコンテキストで動作すれば、どのコンテキストでも動作することが分かります。純粋でない関数は、テストするための足場がより多く必要になる傾向があります。

それでも、純粋でない関数を記述しても悪い気分になる必要はありませんし、コードからそれらを排除するための聖戦を戦う必要もありません。副作用はしばしば有用です。例えば、console.logの純粋なバージョンを書く方法はなく、console.logは便利なものです。また、副作用を使用すると、効率的な方法で表現しやすい操作もあるため、計算速度を理由に純粋さを避けることもあります。

まとめ

この章では、独自の関数の書き方を学びました。functionキーワードは、式として使用すると関数値を作成し、文として使用すると、バインディングを宣言して関数値を割り当てることができます。アロー関数は、関数を生成する別の方法です。

// Define f to hold a function value
const f = function(a) {
  console.log(a + 2);
};

// Declare g to be a function
function g(a, b) {
  return a * b * 3.5;
}

// A less verbose function value
let h = a => a % 3;

関数を理解する上で重要な側面は、スコープを理解することです。各ブロックは新しいスコープを作成します。特定のスコープで宣言されたパラメーターとバインディングはローカルであり、外部からは表示されません。varで宣言されたバインディングは異なる動作をします。最寄りの関数スコープまたはグローバルスコープに属することになります。

プログラムが実行するタスクを異なる関数に分けることは有益です。自己複製を減らすことができ、関数は特定の処理を行うコードをグループ化することで、プログラムの整理に役立ちます。

練習問題

最小値

前の章では、最小の引数を返す標準関数Math.minを紹介しました。それを今すぐ作成できます。2つの引数を取り、それらの最小値を返す関数minを作成してください。

// Your code here.

console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10

有効な関数定義を得るために、中括弧と括弧を適切な場所に配置するのが難しい場合は、この章の例を1つコピーして修正することから始めましょう。

関数は複数のreturn文を含むことができます。

再帰

これまで、剰余演算子%を使用して、% 2で2で割り切れるかどうかを確認することで、数が偶数か奇数かをテストできることを見てきました。正の整数が偶数か奇数かを定義する別の方法を以下に示します。

この説明に対応する再帰関数isEvenを定義します。この関数は単一のパラメータ(正の整数)を受け取り、ブール値を返します。

50と75でテストします。-1での動作を確認してください。なぜでしょうか?修正方法を考えられますか?

// Your code here.

console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??

作成する関数は、この章の再帰的findSolutionにある内部のfind関数と多少似ており、3つのケースのいずれが適用されるかをテストするif/else if/elseチェーンが含まれます。3番目のケースに対応する最後のelseでは、再帰呼び出しが行われます。各ブランチにはreturn文を含めるか、何らかの方法で特定の値が返されるようにする必要があります。

負の数が与えられると、関数は何度も再帰し、自分自身にますます負の数を渡し、結果を返すことからますます遠ざかります。最終的にはスタック空間を使い果たして異常終了します。

豆の計数

文字列からN番目の文字を取得するには、"string"[N]と書きます。返される値は、1文字だけを含む文字列(例:「b」)になります。最初の文字の位置は0であり、最後の文字の位置はstring.length - 1になります。つまり、2文字の文字列の長さは2であり、その文字の位置は0と1になります。

引数として文字列を受け取り、その文字列に含まれる大文字の「B」文字の数を示す数値を返す関数countBsを作成します。

次に、countBsと同様に動作するcountCharという関数を作成します。ただし、この関数は、(大文字の「B」文字だけを数えるのではなく)数える文字を示す2番目の引数を取ります。この新しい関数を使用してcountBsを書き換えます。

// Your code here.

console.log(countBs("BBC"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4

この関数には、文字列のすべての文字を確認するループが必要です。このループは、インデックスを0から長さより1小さい値(< string.length)まで実行できます。現在の位置にある文字が関数で探している文字と同じ場合、カウンター変数に1を加算します。ループが終了したら、カウンターを返すことができます。

letまたはconstキーワードを使用して適切に宣言することで、関数で使用されるすべてのバインディングを関数にローカルにします。