関数

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

ドナルド・クヌース
Illustration of fern leaves with a fractal shape, bees in the background

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

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

典型的な成人の英語話者は、約20,000語の語彙を持っています。20,000のコマンドが組み込まれているプログラミング言語はほとんどありません。そして、利用可能な語彙は、人間の言語よりも正確に定義されているため、柔軟性が低い傾向があります。したがって、過度の冗長性を避けるために、新しい単語を導入する必要があります。

関数の定義

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

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

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

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

関数は複数のパラメータを持つことも、パラメータをまったく持たないこともできます。次の例では、makeNoiseはパラメータ名を列挙していませんが、roundTonstepの最も近い倍数に丸めます)は2つ列挙しています。

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

makeNoise();
// → Pling!

const roundTo = function(n, step) {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

console.log(roundTo(23, 10));
// → 20

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

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

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

各バインディングには、バインディングが表示されるプログラムの一部であるスコープがあります。関数、ブロック、またはモジュール(第10章を参照)の外部で定義されたバインディングの場合、スコープはプログラム全体です。このようなバインディングは、必要な場所で参照できます。これらはグローバルと呼ばれます。

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

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

let x = 10;   // global
if (true) {
  let y = 20; // local to block
  var z = 30; // also global
}

各スコープは周囲のスコープを「見渡す」ことができるため、例ではブロック内で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章では、関数の値を他の関数に渡すことによってできる興味深いことについて説明します。

宣言表記

関数バインディングを作成するための少し短い方法があります。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 roundTo = (n, step) => {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

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

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

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

パラメータの後に=演算子を記述し、その後に式を記述すると、引数が指定されていない場合、その式の値が引数を置き換えます。たとえば、このバージョンの roundTo は、2番目の引数をオプションにします。それを指定しないか、値 undefined を渡すと、デフォルトで1になります。

function roundTo(n, step = 1) {
  let remainder = n % step;
  return n - remainder + (remainder < step / 2 ? 0 : step);
};

console.log(roundTo(4.5));
// → 5
console.log(roundTo(4.5, 2));
// → 4

次の章では、関数本体が渡された引数のリスト全体を取得する方法を紹介します。これは、関数が任意の数の引数を受け入れることができるため、役立ちます。たとえば、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

これは、数学者がべき乗を定義する方法にかなり近く、第2章で使用したループよりも明確に概念を説明していると言えるでしょう。関数は、繰り返し乗算を実現するために、ますます小さい指数で複数回自分自身を呼び出します。

ただし、この実装には1つの問題があります。一般的なJavaScript実装では、for ループを使用するバージョンよりも約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つの可能なパスを両方試します。最初の呼び出しがnullではない何かを返した場合、それが返されます。それ以外の場合は、文字列を生成するかnullを生成するかに関係なく、2番目の呼び出しが返されます。

この関数がどのように目的の効果を生み出すかをよりよく理解するために、数値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つあります。

関数を記述する最初のきっかけは、似たようなコードを何度も書いていることに気付いたときです。コードが多ければ多いほど、ミスが隠れる場所が増え、プログラムを理解しようとする人が読まなければならない量も増えるため、これは避けたいことです。そこで、繰り返される機能を取り出し、適切な名前を付けて、関数にします。

関数を記述する2つ目のきっかけは、まだ書いていない機能が必要で、それ自身の関数にすべきだと感じる場合です。まず関数に名前を付け、それから本体を書きます。関数を実際に定義する前に、その関数を使うコードを書き始めることさえあるかもしれません。

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

農場の牛と鶏の数を、それぞれ`Cows`と`Chickens`という単語を付けて、両方の数字が常に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行をもう一度コピー&ペーストしようとしたところで、立ち止まって考え直します。もっと良い方法があるはずです。最初の試みは次のとおりです。

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つのことを1つの関数にまとめています。

プログラムの繰り返し部分をそのまま抜き出すのではなく、単一の*概念*を選び出してみましょう。

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番目のバージョンが最初のバージョンよりも多くの状況で役立つのは偶然ではありません。値を作成する関数は、副作用を直接実行する関数よりも、新しい方法で組み合わせるのが容易です。

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

それでも、純粋ではない関数を書いても気にする必要はありません。副作用はしばしば役に立ちます。たとえば、`console.log`の純粋なバージョンを書く方法はありませんし、`console.log`は役に立ちます。副作用を使用すると、一部の操作を効率的な方法で表現しやすくなります。

まとめ

この章では、独自の関数の書き方を学びました。`function`キーワードは、式として使用すると、関数値を作成できます。文として使用すると、バインディングを宣言し、その値として関数を指定することができます。アロー関数は、関数を作成するもう1つの方法です。

// 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`を紹介しました。 jetzt können wir selbst eine solche Funktion schreiben. 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));
// → ??
ヒントを表示...

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

負の数が与えられると、関数は何度も再帰し、ますます負の数を自身に渡し、結果を返すことからますます遠ざかります。最終的にはスタックスペースが不足して中止されます。

豆の数え方

文字列の N 番目の文字を取得するには、文字列の後に [N] を記述します(例:string[2])。結果の値は、1文字のみを含む文字列になります(例:"b")。最初の文字の位置は 0 であるため、最後の文字は string.length - 1 の位置にあります。つまり、2文字の文字列の長さは 2 で、文字の位置は 0 と 1 になります。

文字列を引数として受け取り、その文字列に含まれる大文字の B の数を返す countBs という関数を記述してください。

次に、countBs と同様に動作する countChar という関数を記述しますが、こちらは2番目の引数として、カウントする文字を受け取ります(大文字の B だけをカウントするのではなく)。この新しい関数を利用するように countBs を書き直してください。

// Your code here.

console.log(countBs("BOB"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4
ヒントを表示...

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

関数内で使用されるすべての変数を、let または const キーワードを使用して適切に宣言することにより、関数に対して*ローカル*になるように注意してください。