第3版が公開されました。こちらで読んでください!

第3章
関数

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

Donald Knuth

あなたはすでに、`alert`のような関数値とその呼び出し方を見てきました。関数はJavaScriptプログラミングの基礎です。プログラムの一部を値で包むという概念は、多くの用途があります。それは、より大規模なプログラムを構造化し、繰り返しを減らし、サブプログラムに名前を関連付け、これらのサブプログラムを互いに分離するためのツールです。

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

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

関数の定義

関数の定義は、変数に与えられる値がたまたま関数であるという通常の変数の定義です。たとえば、次のコードは、与えられた数の二乗を生成する関数を指すように変数`square`を定義します。

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

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

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

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

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

makeNoise();
// → Pling!

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

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

`power`や`square`のように値を生成する関数もあれば、`makeNoise`のように副作用のみを生成する関数もあります。`return`文は、関数が返す値を決定します。制御がこのような文に遭遇すると、すぐに現在の関数からジャンプし、返された値を関数を呼び出したコードに渡します。`return`キーワードの後に式がない場合は、関数は`undefined`を返します。

パラメータとスコープ

関数のパラメータは通常の変数のように動作しますが、その初期値は関数自体ではなく、関数を呼び出した側によって与えられます。

関数の重要な特性は、パラメータを含む、関数内で作成された変数が関数に対してローカルであることです。これは、たとえば、`power`の例にある`result`変数は、関数が呼び出されるたびに新しく作成され、これらの個別のインスタンスは互いに干渉しないことを意味します。

この変数の「ローカル性」は、パラメータと関数本体内で`var`キーワードを使用して宣言された変数にのみ適用されます。関数外部で宣言された変数は、プログラム全体で見えるため、グローバルと呼ばれます。同じ名前のローカル変数を宣言していない限り、関数内からそのような変数にアクセスすることは可能です。

次のコードはこれを示しています。変数`x`に値を代入する2つの関数を定義して呼び出します。最初の関数は変数をローカルとして宣言するため、ローカル変数のみを変更します。2番目の関数は`x`をローカルに宣言しないため、関数内の`x`への参照は、例の先頭で定義されたグローバル変数`x`を参照します。

var x = "outside";

var f1 = function() {
  var x = "inside f1";
};
f1();
console.log(x);
// → outside

var f2 = function() {
  x = "inside f2";
};
f2();
console.log(x);
// → inside f2

この動作は、関数間の意図しない干渉を防ぐのに役立ちます。すべての変数がプログラム全体で共有されている場合、2つの異なる目的で名前が使用されないようにするために、多くの労力が必要になります。そして、変数の名前を再利用した場合、関連のないコードが変数の値を操作することによって、奇妙な影響が現れる可能性があります。関数ローカル変数を関数内でのみ存在するものとして扱うことで、言語は、一度にすべてのコードを心配することなく、小さな宇宙として関数を読み書きすることを可能にします。

ネストされたスコープ

JavaScriptは、グローバル変数とローカル変数の区別だけでなく、関数は他の関数の中に作成でき、複数の局所レベルを生成します。

たとえば、このかなり無意味な関数は、内部に2つの関数を持っています。

var landscape = function() {
  var result = "";
  var flat = function(size) {
    for (var count = 0; count < size; count++)
      result += "_";
  };
  var mountain = function(size) {
    result += "/";
    for (var count = 0; count < size; count++)
      result += "'";
    result += "\\";
  };

  flat(3);
  mountain(4);
  flat(6);
  mountain(1);
  flat(1);
  return result;
};

console.log(landscape());
// → ___/''''\______/'\_

`flat`関数と`mountain`関数は、それらを定義する関数の内部にあるため、`result`と呼ばれる変数を参照できます。しかし、互いに外部にあるため、互いの`count`変数を参照することはできません。`landscape`関数の外部の環境は、`landscape`内で定義された変数のいずれも見ることができません。

要するに、各ローカルスコープは、それを含むすべてのローカルスコープを参照できます。関数内で見える変数の集合は、プログラムテキスト内のその関数の場所によって決定されます。関数の定義を囲むブロックのすべての変数は可視です。つまり、関数を囲む関数本体とプログラムの最上位レベルの両方です。この変数の可視性へのアプローチは、レキシカルスコープと呼ばれます。

他のプログラミング言語の経験がある人は、中括弧間のコードブロックが新しいローカル環境を作成すると予想するかもしれません。しかし、JavaScriptでは、関数が新しいスコープを作成する唯一のものです。スタンドアロンのブロックを使用することはできます。

var something = 1;
{
  var something = 2;
  // Do stuff with variable something...
}
// Outside of the block again...

しかし、ブロック内の`something`は、ブロック外部のものと同じ変数を参照します。実際、このようなブロックは許可されていますが、`if`文やループの本体をグループ化する場合にのみ役立ちます。

これが奇妙に思えるなら、あなただけではありません。次のバージョンのJavaScriptは、`var`のように動作しますが、囲むブロックに対してローカルな変数を作成する`let`キーワードを導入します。囲む関数ではありません。

値としての関数

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

しかし、この2つは異なります。関数値は、他の値ができることをすべて実行できます。呼び出すだけでなく、任意の式で使用できます。関数値を新しい場所に保存したり、関数の引数として渡したりすることもできます。同様に、関数を保持する変数は、通常の変数であり、次のように新しい値を代入できます。

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

第5章では、関数値を他の関数に渡すことでできる素晴らしいことについて説明します。

宣言表記

「`var square = function…`」と言うには、少し短い方法があります。`function`キーワードは、次のようにステートメントの先頭でも使用できます。

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

これは関数宣言です。このステートメントは変数`square`を定義し、それを指定された関数にポイントします。今のところ問題ありません。しかし、この形式の関数定義には、1つの微妙な点があります。

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

function future() {
  return "We STILL have no flying cars.";
}

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

このような関数定義を条件付き(`if`)ブロックやループの内部に配置するとどうなるでしょうか?それはしないでください。さまざまなブラウザーのさまざまなJavaScriptプラットフォームは、従来、その状況で異なる動作をしてきました。最新の標準では、実際にはそれが禁止されています。プログラムを一貫して動作させるには、この形式の関数定義ステートメントを、関数またはプログラムの最外側のブロックでのみ使用してください。

function example() {
  function a() {} // Okay
  if (something) {
    function b() {} // Danger!
  }
}

コールスタック

関数を通して制御がどのように流れるかを詳しく見ていくと役立ちます。いくつかの関数呼び出しを行う簡単なプログラムを次に示します。

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

このプログラムの実行は、おおよそ次のようになります。`greet`への呼び出しにより、制御はその関数の先頭(2行目)にジャンプします。`console.log`(組み込みのブラウザー関数)を呼び出し、制御を受け取り、その仕事を行い、制御を2行目に返します。次に、`greet`関数の最後に到達するため、それを呼び出した場所、つまり4行目に戻ります。その後の行は、`console.log`を再び呼び出します。

制御の流れを図的に示すことができます。

top
   greet
        console.log
   greet
top
   console.log
top

関数は返されるときに呼び出しの場所にジャンプする必要があるため、コンピューターは関数が呼び出されたコンテキストを記憶する必要があります。ある場合では、`console.log`は`greet`関数に戻る必要があります。もう一方の場合、プログラムの終わりに戻ります。

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

このスタックを格納するには、コンピューターのメモリにスペースが必要です。スタックが大きくなりすぎると、コンピューターは「スタックスペース不足」または「再帰が多すぎる」などのメッセージで失敗します。次のコードは、コンピューターに非常に難しい質問をして、2つの関数間で無限の前後を繰り返すことによって、これを示しています。むしろ、コンピューターが無限のスタックを持っている場合、無限になります。現状では、スペースを使い果たしたり、「スタックを吹き飛ばす」ことになります。

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

省略可能な引数

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

alert("Hello", "Good Evening", "How do you do?");

alert関数は正式には引数を1つしか受け付けません。しかし、このように呼び出してもエラーは発生しません。「Hello」が表示されるだけで、他の引数は無視されます。

JavaScriptは、関数に渡す引数の数に関して非常に寛容です。引数を多く渡しすぎても、余分な引数は無視されます。引数を少なく渡しすぎても、足りないパラメータにはundefinedが割り当てられるだけです。

この欠点は、関数に誤った数の引数を渡してしまう可能性(実際にはよくあることですが)があり、それが検知されないことです。

利点は、この動作を利用して関数が「オプション」引数を受け付けるようにできることです。たとえば、以下のpower関数のバージョンは、2つの引数でも1つの引数でも呼び出すことができます。1つの引数の場合、指数は2とみなされ、関数はsquareのように動作します。

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

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

次の章では、関数本体が渡された引数の正確なリストを取得する方法を説明します。これは、関数が任意の数の引数を受け付けるようにできるため便利です。たとえば、console.logはこの機能を利用しており、渡されたすべての値を出力します。

console.log("R", 2, "D", 2);
// → R 2 D 2

クロージャ

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

次のコードは、この例を示しています。ローカル変数を作成するwrapValue関数を定義し、そのローカル変数にアクセスして返す関数を返します。

function wrapValue(n) {
  var localVariable = n;
  return function() { return localVariable; };
}

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

これは許可されており、期待通りに動作します。変数には依然としてアクセスできます。実際、変数の複数のインスタンスが同時に存在する可能性があり、ローカル変数は実際にすべての呼び出しごとに再作成されるという概念のもう1つの良い例です。異なる呼び出しが互いのローカル変数を踏みつけ合うことはありません。

この機能(囲んでいる関数のローカル変数の特定のインスタンスを参照する機能)は、クロージャと呼ばれます。いくつかのローカル変数を「閉じ込める」関数は、クロージャと呼ばれます。この動作により、変数の寿命を心配する必要がなくなるだけでなく、関数値の創造的な使用が可能になります。

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

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

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

wrapValueの例にある明示的なlocalVariableは、パラメータ自体がローカル変数であるため必要ありません。

このようなプログラムについて考えるには、練習が必要です。良いメンタルモデルは、functionキーワードを、その本体のコードを「凍結」してパッケージ(関数値)にラップするものとして考えることです。そのため、return function(...) {...}を読むときは、後で使用するように凍結された計算の一部へのハンドルを返すものと考えてください。

この例では、multiplierは凍結されたコードのチャンクを返し、それがtwice変数に格納されます。最後の行では、この変数の値を呼び出すことで、凍結されたコード(return number * factor;)がアクティブになります。それはまだmultiplier呼び出しで作成されたfactor変数にアクセスでき、さらに、それを「凍結解除」するときに渡された引数5にも、そのnumberパラメータを介してアクセスできます。

再帰

スタックのオーバーフローを起こさない限り、関数が自分自身を呼び出すことは完全に許容されます。自分自身を呼び出す関数は再帰的と呼ばれます。再帰により、一部の関数を異なるスタイルで記述できます。たとえば、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実装では、ループバージョンよりも約10倍遅くなります。単純なループを実行する方が、関数を複数回呼び出すよりもはるかに安価です。

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

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

多くのプログラマによって繰り返し述べられており、私自身も心から同意する基本的なルールは、プログラムが遅すぎることを確信するまで、効率について心配しないことです。遅すぎる場合は、どの部分が最も多くの時間を消費しているかを調べ、それらの部分でエレガンスを効率と交換し始めます。

もちろん、このルールはパフォーマンスを完全に無視すべきという意味ではありません。power関数のような多くの場合、「エレガントな」アプローチではシンプルさがそれほど得られません。そして、経験豊富なプログラマは、単純なアプローチでは決して十分な速度にならないことをすぐに認識できる場合があります。

私がこれを強調している理由は、驚くほど多くの初心者が、最小の詳細でさえも効率に狂信的に焦点を当てていることです。その結果、より大きく、より複雑で、多くの場合、より正確ではないプログラムになり、より簡単な同等のプログラムよりも記述に時間がかかり、通常はわずかに高速にしか実行されません。

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

このパズルを考えてみてください。数字の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つのアクションのいずれかを実行します。現在の数がターゲット数の場合、現在の履歴はターゲットに到達する方法であるため、単純に返されます。現在の数がターゲットよりも大きい場合、加算と乗算の両方で数値が大きくなるだけなので、この履歴をさらに探求する意味はありません。そして最後に、ターゲットを下回っている場合は、関数自体を2回呼び出して、現在の数から始まる2つの可能なパスを試みます。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)(1 * 3)で始まるソリューションを探索するために、自分自身を2回呼び出します。最初の呼び出しは、(1 + 5)で始まるソリューションを見つけようと試み、再帰を使用して、ターゲット数以下の数値を生成するすべてのソリューションを探索します。ターゲットにヒットするソリューションが見つからないため、最初の呼び出しにnullを返します。そこで、||演算子によって(1 * 3)を探索する呼び出しが発生します。この検索は、その最初の再帰呼び出しが、さらに別の再帰呼び出しを介して、ターゲット数13にヒットするため、より幸運です。この最も内側の再帰呼び出しは文字列を返し、中間呼び出しの各||演算子は、その文字列を渡し、最終的に私たちのソリューションを返します。

関数の増加

関数プログラムに導入される方法は、2つほどあります。

1つ目は、非常に似たコードを複数回記述していることに気付くことです。コードが多くなるということは、隠れるミスのためのスペースが増え、プログラムを理解しようとする人にとって読むべき資料が増えることを意味するため、そうしたくありません。そのため、繰り返される機能を取り、適切な名前を見つけ、関数に入れます。

2つ目の方法は、まだ記述していない機能が必要で、独自の関数に値すると思われる機能を見つけることです。関数の名前を付けるところから始め、その本体を記述します。関数を実際に定義する前に、その関数を使用するコードの記述を開始することさえあります。

関数の適切な名前を見つけるのが難しいかどうかは、ラップしようとしている概念がどれだけ明確であるかの良い指標です。例を見ていきましょう。

農場の牛とニワトリの数(2つの数値)を、後ろにCowsChickensという単語を付けて、両方の数値の前にゼロを埋め込んで常に3桁になるようにするプログラムを作成したいと考えています。

007 Cows
011 Chickens

明らかに、これは2つの引数を持つ関数を必要としています。コーディングを始めましょう。

function printFarmInventory(cows, chickens) {
  var cowString = String(cows);
  while (cowString.length < 3)
    cowString = "0" + cowString;
  console.log(cowString + " Cows");
  var 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) {
  var 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) {
  var 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キーワードは、式として使用すると関数値を作成し、文として使用すると変数を宣言して関数値を割り当てることができます。

// Create a function value f
var f = function(a) {
  console.log(a + 2);
};

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

関数を理解する上で重要なのは、ローカルスコープを理解することです。関数内で宣言されたパラメータと変数は、関数に対してローカルであり、関数が呼び出されるたびに再作成され、外部からは見えません。別の関数内で宣言された関数は、外部関数のローカルスコープにアクセスできます。

プログラムが実行するタスクを異なる関数に分離することは役立ちます。繰り返しを減らすことができ、関数により、章や節が通常のテキストを整理するのと同様に、コードを概念的なチャンクにグループ化することで、プログラムの可読性を高めることができます。

練習問題

最小値

前の章では、最小の引数を返す標準関数Math.minを紹介しました。これからは自分自身でそれを行うことができます。2つの引数を取り、その最小値を返す関数minを作成してください。

// Your code here.

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

有効な関数定義を得るために、中括弧と括弧を正しい位置に配置することに苦労する場合は、この章の例をコピーして変更することから始めましょう。

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

再帰

%(剰余演算子)を使用して、% 2を使用して2で割り切れるかどうかをチェックすることで、数値が偶数か奇数かをテストできることを既に見てきました。正の整数が偶数か奇数かを定義する別の方法を次に示します。

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

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文が含まれているか、何らかの方法で特定の値が返されるようにする必要があります。

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

豆の数え上げ

"string".lengthで長さを得るのと同じように、"string".charAt(N)を書いて、文字列からN番目の文字または文字を取得できます。返される値は、1文字のみを含む文字列(例:「b」)になります。最初の文字の位置はゼロであり、最後の文字は位置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を加算します。ループが終了したら、カウンターを返すことができます。

varキーワードを使用して、関数で使用されるすべての変数を関数に対してローカルにするように注意してください。