Functions

Eloquent JavaScript Chapter 3 の読書ノート。

他のプログラミングと同様に JavaScript でも関数概念は基本的だ。大規模なプログラムを構造化し、反復を減らし、部分プログラムに分割して全体を管理することができる。

Defining a function

  • 関数定義とは、変数定義であって、変数の値が関数であるものだ。

  • 関数はキーワード function で始まる式で作成される。

const square = function(x) {
    return x * x;
};
  • 他のプログラミング言語同様に、引数リスト、本体、返り値などの概念も通用する。

  • キーワード return の後に式がない場合、その関数は値 undefined を返す。また、return 文を持たない関数も同様に値 undefined を返す。

Bindings and scopes

スコープの基本的な考え方は他のプログラミングと同様のようだ。

  • 他のプログラミング同様に、関数も含む変数にはスコープがある。

  • スコープの概念によって、関数間の分離が実現され、各関数の呼び出しはそれぞれの局所的な世界で動作する。

  • letconst で宣言された変数は、実際にはそれが宣言されたブロック内での局所的な実体だ。ループ内でこれらの変数を作成した場合、ループの前のコードも後のコードもそれを「見る」ことはできない。

  • 2015 年以前の JavaScript では新しいスコープを作るのは関数しかなかった。それゆえキーワード var で作成された旧式の変数は、それが登場する関数全体に加えて、大域スコープから見ることができる。

  • 同じ名前の変数が複数ある場合は、コードは一番内側の変数しか見ることができない。

Nested scope

  • ブロックや関数は、他のブロックや関数の内部に作成することができる。局所性を複数持つ。

  • ブロック内で見える変数の集合は、プログラムテキスト内のそのブロックの位置によって決定する。各局所スコープからは、それを含むすべての局所スコープも見ることができ、すべてのスコープは大域スコープを見ることができる。このように変数を可視化する方法をレキシカルスコープと呼ぶ。

レキシカルスコープの概念をしっかりと理解すること。

Functions as values

特になし。

Declaration notation

次の形式の関数定義も認められている。そのようなものは先ほどの形式のそれと動作が異なる。

function square(x) {
    return x * x;
}
  • 関数の後にセミコロンを入れる必要がないので、若干書きやすい。

  • 関数がそれを呼び出すコードの下に定義してもプログラムは動作する。この形式の関数の定義は通常の上から下への制御の流れには含まれない、このような関数定義は概念的にはそのスコープの最上部にあるかのように扱われ、そのスコープ内のすべてのコードが使用することができる。

Arrow functions

他プログラム言語でいうラムダ式に相当する関数定義の様式もある。キーワード function の代わりに、記号 => を使う。

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

const square1 = (x) => { return x * x; };
const square2 = x => x * x;
  • 引数がただ一つの場合は、引数リストを囲む丸括弧を省略してもよい。

  • 関数本体が単一の式の場合、中括弧で囲まれたブロックではなく、その式が関数から返される。

  • 矢印関数は 2015 年に追加されたものだ。小さな関数をより簡潔に書くことを目的としている。

The call stack

これも JavaScript というよりは、プログラミング言語全般の基本的な概念だ。

関数は終了するときに呼び出し元にジャンプして戻る必要がある。つまり、コンピューターは呼び出しが行われたときのコンテキストを記憶していなければならない。このコンテキストを保存する場所をコールスタックという。関数が呼び出されるたびに、その時点のコンテキストがこのスタックのいちばん上に格納される。

スタックが大きくなりすぎると、コンピューターは out of stack space や too much recursion などのメッセージを出して失敗する。

Optional Arguments

JavaScript は関数に渡す引数の個数についてはひじょうにに寛大だ。

  • 多すぎる数の引数を渡しても、余分なものは無視される少なすぎると、足りない引数には undefined という値が割り当てられる。

  • 他のプログラミング言語におけるデフォルト引数やキーワード引数の概念と同様のものがある。仮引数の後ろに演算子 = を書き、式を記述すると実引数が与えられていない場合にはその式の値が実引数となる。

次の章で、引数のリスト全体を関数本体が取得する方法を見ていく。

Closure

局所変数の特定のオブジェクトを、それを囲むスコープの中で参照することができる機能を クロージャー と呼ぶ。

function wrapValue(n) {
    let local = n;
    return () => local;
}
let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.assert(wrap1() == 1);
console.assert(wrap2() == 2);

関数 wrapValue の変数 local のような明示的な定義は実は必要ない。引数それ自体が局所変数だ。

Recursion

他のプログラミング言語と同様に、関数を再帰的に呼び出すことが許される。

  • 自分自身を呼び出す関数を再帰関数と呼ぶ。

  • 再帰関数はループの形で書くことができるのがふつうだ。

  • 一般的な JavaScript の実装では、ループに比べて約 3 倍遅くなる。単純なループを実行する方が関数を何度も呼び出すよりも一般的に安上がりだ。

  • 再帰は必ずしもループの代わりになる非効率なものばかりではない。問題によってはループよりも再帰の方が解決しやすいものもある。

Growing functions

  • 同じようなコードを何度も書いてしまうことがあれば、それは関数を導入する兆候だ。

  • まだ書いていない必要な機能があり、それが関数であることがふさわしいように思える場合もそうだ。

  • 関数の良い命名を見つける難しさは、定義しようとしている概念のわかりやすさと関係する。

  • 絶対に必要だと確信できる場合を除き、小賢しいことをしないのが原則だ。

Functions and side effects

  • 値を生成する関数は、副作用がある関数よりも、新しい方法で組み合わせることが容易だ。

  • 純粋関数 とは、値を返す関数であって、副作用がないだけでなく、他のコードからの副作用にも依存しないものをいう。

  • 純粋関数は、同じ引数で呼び出された場合、常に同じ値を返すといううれしい性質がある。

Summary

  • キーワード function を式として使うと、関数を定義する。

  • キーワード function を文として使うと、変数を宣言して、その値として関数を定義する。

  • 矢関数は、関数を定義するもう一つの方法だ。

  • 関数を理解する上で重要なのはスコープを理解することだ。

  • キーワード var により定義された変数のスコープはそれ以外のキーワードにより定義されたものとかなり異なる。

Exercises

Minimum

問題:標準関数 Math.min のようなものを作れ:引数を二つ取り、それらの最小値を返す関数 min を書け。

解答:C++ 標準の std::max をパクる。

function max(a, b){
    console.assert(!isNaN(a));
    console.assert(!isNaN(b));
    return (a < b) ? b : a;
}

console.assert(max(100, 5) == 100);
console.assert(max(5, 100) == 100);
console.assert(max(5, 5) == 5);
  • この仕様の関数は Math.min のそれとは全然違うことに注意。

Recursion

問題:ここでは、正の整数が偶数か奇数かを次で定義する:

  • \(0\) は偶数であるとする。

  • \(1\) は奇数であるとする。

  • その他の数 \(n\) については、その偶数性は \(n - 2\) と同じとする

この記述に対応する再帰関数 isEven を定義しろ。この関数は正の整数である引数を一つ取り、真偽値を返すものとする。

解答:教科書の演習問題の解答としてはこの程度の品質でいいと思われる:

function isEven(n){
    console.assert(Number.isSafeInteger(n) && n >= 0);
    return n == 0 ? true : (n == 1 ? false : isEven(n - 2));
}

console.assert(isEven(50));
console.assert(!isEven(75));
console.assert(!isEven(1));

Bean counting

問題:文字列を唯一の引数として受け取り、その文字列に含まれる大文字の B の個数を返す関数 countBs を書け。その後、関数 countBs を次のように書き換えて関数 countChar を定義しろ。この関数は、数える文字を第二引数として取ることを除いては、countBs と同様に動作するものとする。

解答:題意を無視して後半からやる:

function countChar(s, char = "B"){
    return Array.from(s).filter(c => c == char).length;
}

以上