JavaScriptにおいて、関数は最も基本的な機能であるにもかかわらず、きちんと理解しようとするととても難しい。JavaScriptの関数は独特の特徴をもつ。関数の実体はオブジェクトであり、普通のデータと同様に扱うこともできる。また、関数は使用する変数を保持するクロージャーでもある。関数の記述の方法がとても多くあり、関数に似たジェネレーターや非同期関数もある。
関数を中心とした、関数型プログラミングと呼ばれるスタイルがある。JavaScriptでは関数型の記述のサポートは万全とはいえないが、部分的に関数型のスタイルを実現する可能性は考えられる。本書では関数型プログラミングとはどのようなものか簡単に紹介し、関数型プログラミングで頻繁に登場する構造であるファンクタとモナドについて、JavaScriptでどのように記述ができるか、ざっくりと眺めてみる。
JavaScriptの関数を理解することは、JavaScript言語を理解する際の重要なポイントである。本書が少しでもその助けになることができれば幸いである。
1章では関数の基礎について述べる。2章で関数の定義、3章で関数の呼び出しについて詳しく解説する。4章では関数のさまざまな記述を解説する。5,6章では、関数の少し高度な使い方を紹介する。7,8章では関数型プログラミングの基礎を紹介し、ファンクタとモナドの実装を考える。9章では非同期処理のPromiseと、8章で述べるモナドとの比較を行う。
・本書は、だいたい「入門書の次に読む本」を想定して作られている。基礎的な解説から行っているが、JavaScriptの文法は一通り学んでいることが望ましい。不明な点は、手持ちの入門者やMDN web docsを参照してもらいたい。
・5章以降は高度な内容や理論的な話題も含まれるため、理解しやすい部分から読んでいくとよいかもしれない。
・ライブラリー、フレームワークは使用しない。基本的に素のJavaScriptで完結する範囲で構成している。
・実行環境は最近のブラウザーを想定している。
本書に記載されている会社名、製品名などは、一般に各社の登録商標または商標、商品名です。会社名、製品名については、本文中では©、®、™マークなどは表示していません。
本書に記載された内容は、情報の提供のみを目的としています。本書を用いた開発、製作、運用は、必ずご自身の責任において行ってください。これらの情報による開発、製作、運用の結果について、著者はいかなる責任も負いません。
JavaScriptの関数は、functionキーワードにより関数宣言を行うことで定義できる。また、関数名に()をつけることにより、関数の呼び出しができる。
// 関数の宣言(定義)
function f1() {
*** // 関数の処理
}
// 関数の呼び出し
f1();
関数は、まとまった一連の処理を記述しておき、プログラムの適当な場所からその処理を実行するために使われる。
関数の記述において、JavaScriptに限らずプログラムの初心者にとっては、書かれた順番に命令が実行されないという事態に戸惑うかもしれない。以下の記述を考えよう。
console.log("A");
// 関数の定義(関数宣言)
function f1() {
console.log("C");
}
console.log("B");
// 関数の呼び出し
f1();
console.log("D");
結果
A
B
C
D
"C"の出力は、"A"と"B"の出力の間に書かれている。にもかかわらず、実際は"B"と"D"の出力の間に行われる。これは、関数の定義時には関数の中の処理は行われず、関数の呼び出し時に中の処理が行われるということである。
プログラムが複雑になると、関数の定義と呼び出しが様々な形で行われるようになる。プログラムの実行が関数の定義を行っているのか、関数の中の処理を行っているのか見分けるのが、理解するコツである。
JavaScriptでは、基本的に複数の「文」、すなわちプログラムの処理が、前から順番に実行されていく。このようなプログラムの方法を一般に、手続き型プログラミング、または、命令型プログラミングと呼ぶ。
手続き型プログラミングにおいて、一連のプログラムの処理をまとめたものをサブルーチンと呼ぶ。JavaScriptの関数は、一連のプログラムの処理を定義したサブルーチンであると見ることができる。関数を呼び出すと、処理は関数内部に移行し、関数内部の文が順に実行されていく。関数の処理が全て終了すると、呼び出した位置に戻る。そして、関数呼び出しの続きの処理が実行されていく。
関数呼び出しを行い、その後関数から戻って続きの処理を行うためには、処理を実行していた場所を覚えていなければならない。このために、呼び出しスタック(call stack)という仕組みが使われる。
スタックとは、データを積み上げ式で保持する構造である。スタックにデータを追加するには、スタックの一番上に置く。スタックからデータを取り出すには、スタックの一番上から取り出す。複数のデータを入れるとき、最初に追加されたものが最後に取り出されることになるので、これをFILO(First-in, Last-out)と呼ぶことがある。
関数呼び出し時には、スタックに新たな実行状態を積む。関数の終了時には、スタックから関数の実行状態を取り除き、元の状態を復元する。この機構により、関数の中でさらに関数を呼び出すなどしても、適切に実行状態を復元し、関数呼び出しの続きから実行を再開できる。
次のコードに示す一連の処理を考える。はじめにトップレベルから関数f1が呼び出され、呼び出しスタックにf1の実行状態が積まれる。次にf2が呼び出されると、f1の実行状態はそのまま保持し、呼び出しスタックにf2の実行状態が積まれる。f3の呼び出しでも同様である。f3の実行が終了すると、f3の実行状態は取り除かれ、f2の実行状態が復元される。以下同様にして、はじめにf1を呼び出した状態(top)が復元されるのである。
function f1() {
f2();
}
function f2() {
f3();
}
function f3() {
***
}
f1();
関数には、その関数内で使うことのできる値を渡すことができる。
function f1(p) {
console.log(p); // 値pが使える
}
f1(1); // 値を渡す
f1(100);
結果
1
100
関数内では、渡された値を使った処理を行うことができる。異なる値を渡すことによって、同じ関数でも異なる処理を行うことができる。関数に渡す値のことを、引数と呼ぶ。
本書では、f1(1)のように関数を呼び出すことを、「関数f1に1を渡して呼び出す」、または単に「関数f1に1を渡す」と表現する。
このコードでは、まず関数f1に1を渡して呼び出している。するとf1の中が実行されるが、このとき変数pに1が代入されている。次に関数f1に100を渡して呼び出したとき、変数pには100が代入されている。
関数定義では、pのように引数を受け取る変数を指定する。本書では、「関数f1はpを引数に取る」、または単に「関数f1はpを取る」と表現する。
普段まとめて引数と呼ぶことが多いが、その実態を区別して実引数(argument)と仮引数(parameter)を使い分けることがある。実引数とは、実際に関数に渡される値そのもののことである。仮引数とは、関数内で値を受け取る変数のことである。
function f1(p) { // pは仮引数
***
}
f1(10); // 10は実引数
この記述では、関数f1には実引数として10が与えられる。関数f1内では、この値を仮引数pとして受け取る。
英語でも、仮引数はparameter、実引数はargumentと使い分けている。しかし、実際にはどちらもparameterといってしまったり、argumentといってしまったりすることがある。より紛れないようには、仮引数はformal parameter(formal:形式上の)、実引数はactual argument(actual:実際の)のように冗長にいう。ECMAScript仕様書では、仮引数はformal parameter、実引数はargumentとしているようである。
関数の処理が完了したとき、その関数を呼び出した側へ値を渡すことができる。これを関数が「値を返す」と呼び、返す値を返り値(return value)と呼ぶ。返り値は戻り値と呼ばれることも多いが、同じ意味である。
関数内から値を返すには、return文を使う。return文は、関数の実行を終了し、値を返す。関数の呼び出し側からは、関数呼び出しを評価した値として、返り値が取得できる。
function f1() {
return 1; // 値1を返す
}
const ret1 = f1(); // 返り値を取得
console.log(ret1); // 1
return文に何も渡さなければ、undefinedが返される。また、returnに到達せず、関数を最後まで実行したときにも、undefinedが返される。
JavaScriptは関数内で引数の値を変更したときの挙動がわかりにくい部分がある。関数に対してオブジェクトを渡した場合と、オブジェクト以外のプリミティブ値を渡した場合で、値の変更の影響が異なるように見えることがある。
まず、プリミティブ値を渡した場合を考えよう。関数内で、渡した値を変更したとする。このとき、渡した元の変数には影響を与えない。
function f1(x) {
x = 2; // 仮引数xを変更
}
let a = 1;
f1(a);
console.log(a); // 1
変数aと仮引数xは別の実体であるので、関数内でxの値を変更しても、元の変数aには影響しない。
次に、オブジェクトを渡す場合を考える。関数内で、渡されたオブジェクトのプロパティーを変更したとする。このとき、元のオブジェクトのプロパティーも変更される。
function f2(o) {
o.a = 2;
}
let obj1 = { a: 1 };
f2(obj1);
console.log(obj1); // { a: 2 }
obj1と仮引数oは同じオブジェクトを指している。このため、関数内でプロパティーを変更すると、元のオブジェクトも変更される。
オブジェクトが変更されるのは、プロパティーを変更する場合であることに注意しよう。次のように、仮引数oに別のオブジェクトを代入した場合を考える。
function f3(o) {
o = { a: 2 };
}
let obj2 = { a: 1 };
f3(obj2);
console.log(obj2); // { a: 1 }
このとき、元のオブジェクトには影響しない。これは、関数にobj2を渡したとき、仮引数oとobj2が共に同じオブジェクトの実体を指しているということであり、o自体がobj2と同じというわけではないからである。
この挙動は、関数へ引数を渡すことを、通常の代入に置き換えた場合も同じであることがわかる。
let obj2 = { a: 1 };
let o = obj2;
o = { a: 2 };
console.log(obj2); // { a: 1 }
オブジェクト型の変数に別のオブジェクトを代入したとき、変数はそのオブジェクトを指すように変更される。obj2を指していた変数oに{ a: 2 }を代入したとき、変数oが{ a: 2 }を指すようになるだけなので、obj2には影響しない。
オブジェクト型の変数は、オブジェクトの実体そのものではなく、オブジェクトを指し示すものとしてはたらく。引数としてオブジェクトを渡したとき、オブジェクトの実体ではなく、それを指し示すものが渡される。このため、関数内で引数のオブジェクトのプロパティーを変更したとき、呼び出し元と同じオブジェクトの実体が変更されるのである。
JavaScriptでは、関数の実体は関数オブジェクトと呼ばれるオブジェクトである。関数オブジェクトは通常のオブジェクトと同様のものであるが、関数の呼び出しができるという点が異なる。
次のようにfunc1関数を定義すると、func1という名前の関数オブジェクトが生成される。
function func1() {}
関数オブジェクトfunc1に対し、typeof演算子を使うと、文字列"function"が得られる。
console.log(typeof func1); // "function"
これは、通常のオブジェクトに対するtypeofが"object"を返すのと異なる。
const obj1 = {};
console.log(typeof obj1); // "object"
実際、これは通常のオブジェクトと関数オブジェクトを見分けるために使うことができる。
関数定義により生成された関数オブジェクトは、Functionオブジェクトのインスタンスになっている。
console.log(func1 instanceof Function); // true
すなわち、func1のプロトタイプがFunction.prototypeになっているということである。
console.log(Object.getPrototypeOf(func1) === Function.prototype); // true
FunctionオブジェクトはObjectを継承しているため、関数func1はObjectのインスタンスでもある。
console.log(func1 instanceof Object); // true
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype);
// true:FunctionはObjectを継承している
関数定義により、関数オブジェクトが生成される。この関数オブジェクトはいつ作られているのだろうか。
関数オブジェクトは、関数定義を実行したときに作られていると考えることができる。関数定義の実行とは、関数定義の文を評価したとき、と言い換えられるが、実際には若干複雑である。詳細については次章で述べる。
ここでは単に、ある関数の内部で関数が定義されている場合、その関数の呼び出し時に関数オブジェクトが生成される、ということを理解しよう。次のように入れ子の関数を考える。
function func1() {
function func2() {
}
}
この記述ではまず、func1の関数定義が実行される。ここで関数オブジェクトfunc1が生成される。この時点で、func2はfunc1の内部で定義されているので、func2の定義はまだ実行されていない。なので、関数オブジェクトfunc2は生成されていない。
次に、func1を呼び出す。
func1();
するとfunc1が実行され、内部の関数定義が評価される。この時点でfunc2の関数オブジェクトが生成される。
では、次のように続けてfunc1を呼び出したとき、関数オブジェクトfunc2はどうなるだろうか?
func1();
func1();
1回目のfunc1の呼び出しで関数オブジェクトfunc2が生成される。2回目のfunc1の呼び出しでも関数オブジェクトfunc2が生成されるのだが、これは先のfunc2とは異なる実体である。関数オブジェクトは、関数定義が実行されるたびに生成されるのである。
このことは、次のように確かめられる。func1が、func2を返すようにする。
function func1() {
function func2() {
}
return func2;
}
func1の返り値を比較すれば、異なるfunc2の実体であることがわかる。
const func2_1 = func1();
const func2_2 = func1();
console.log(func2_1 === func2_2); // false
それぞれの関数の実行時に、それぞれ関数の実体が作られている。
C言語など、関数が静的に(実行前あるいはコンパイル時に)生成される言語に慣れていると、このふるまいは違和感があるかもしれない。同じ中身の関数なのに、それぞれ別の実体が生成されているのだろうか?この問いに対しては、詳しくはクロージャーについて述べる必要がある(「5.1 クロージャー」)。
functionキーワードを使った関数の定義の記述には、関数宣言(function declaration)と関数式(function expression)がある。関数宣言になるのは、トップレベルや関数内で独立して書いた場合、すなわち、文(statement)として書いた場合である。一方、式(expression)として書いた場合、関数式となる。式とは、値を生成する記述のことである。
一般的な記述は次のようなものである。
// 関数宣言
function f1() {}
// 関数式
const f3 = function f2() {}; // 名前付き関数式
const f4 = function() {}; // 無名関数式
だいたい、何もないところにいきなりfunctionを書いた場合は関数宣言で、何かの中に書いたときは関数式である。もう少しだけ具体的には、function … の記述が値を返すものであれば関数式である。
次のように書いても関数式である。
(function() {});
+function() {};
JavaScriptでは式を書くことができるあらゆる場所に、関数式を書くことができる。
strictモードでは、関数宣言はトップレベルまたはブロック内にのみ書くことができる。文が書ける任意の場所に書けるわけではないことに注意しよう。
if (cond)
function f1() { }
のような書き方はできない(構文エラーとなる)。
関数の定義を関数宣言として書いた場合、その場所(ブロック内またはトップレベル)でどこでも使えるような関数が生成される。このため、関数宣言の前に関数呼び出しを記述したとしても、関数呼び出しが行える。
f1(); // この呼び出しは有効
// 関数宣言を後方に記述
function f1() {
}
関数宣言では、記述が含まれる場所の先頭で関数の実体が生成される。これを「関数宣言の巻き上げ」と呼ぶことがある。後に述べる関数式では巻き上げは行われない。
同名の関数宣言を重複して記述しても、エラーにはならない。この場合、後方で宣言した関数が有効になる。
f2(); // "f2-2"
function f2() {
console.log("f2-1");
}
function f2() {
console.log("f2-2");
}
関数宣言と同名のlet/const変数を定義しようとした場合、構文エラーである。
let f3; // 構文エラー: f3はすでに宣言されている
const f4 = 1; // 構文エラー: f4はすでに宣言されている
function f3() {}
function f4() {}
var変数であれば定義できる。
f5(); // この呼び出しは有効
var f5 = 1;
f5(); // エラー: f5は関数でない(ここではf5は1なので関数呼び出しはできない)
function f5() {
console.log("f5");
}
関数宣言は、巻き上げや重複した宣言のように、若干直感と異なる挙動をすることがあるので、コーディング規約によっては非推奨とされる場合もある。
関数宣言は、「function *** () { *** }」で完結したひとつの文であるため、最後にセミコロン;をつける必要はない。ただし、JavaScriptでは単体の;も完結したひとつの文(何もしない)であるので、つけたからといって文法上間違いになるわけではない。つけたりつけなかったりすることは見た目がよくないので、コーディング規約で統一しておくのが望ましい。
関数宣言をブロック内で行うことは、ECMAScript 2015(ES6)より以前は仕様上できなかった。
if (cond) {
function f1() {} // ES6より前は仕様上できない
}
しかし、実際にはブラウザーはこのような記述も許容しており、その挙動は実装依存であった。
// ES5以前
function f1() { console.log(1); }
if (cond) {
function f1() { console.log(2); }
}
f1(); // この結果は実装依存
ES6のstrictモードでは、関数宣言はブロックレベルのスコープをもつ。つまり、関数宣言はブロック内でのみ有効である。strictモードで上記のような記述をしたとき、関数f1の呼び出しはcondの値にかかわらず、外側のf1になる。しかし、非strictモードでは過去のブラウザーとの互換性のため、condに依存した挙動となる。よって、現在では常にstrictモードを使用し、非strictモードは使用しないことが推奨される。
もし非strictモードで、条件によって関数を定義するかどうか決める必要があるなら、関数宣言でなく関数式を使用する。
if (cond) {
var f1 = function() {}; // これなら許容
}
関数定義を式として書いた場合、関数式である。関数式が実行されると、関数オブジェクトが生成されて返される。
const f1 = function() {};
関数式は、このように名前なしの関数(無名関数)を定義し、その値(関数オブジェクト)を変数に格納する使い方が一般的である。
関数式では、関数宣言のような巻き上げは行われない。なので、定義の記述の前に呼び出すことはできない。
f2(); // エラー:ここではf2はundefined
var f2 = function() { console.log("f2"); };
f3(); // エラー:let/const変数は宣言前に使用できない
const f3 = function() { console.log("f3"); };
関数式は、名前をつけることもできる。この場合、名前付き関数式と呼ばれる。
const f4 = function f4_named() {};
名前付き関数式の名前は、関数内部からのみ参照できるもので、関数外部からは使うことができない。
const f4 = function f4_named() {
f4_named(); // これはできる
};
f4_named(); // これはできない 参照エラー:f4_namedは見つからない
このため、次のように同名の名前付き関数式を並べて記述することもできる。
const f5 = function func() {};
const f6 = function func() {};
変数と同名の名前付き関数式にすることもできる。
const f7 = function f7() {};
名前付き関数式については「2.2.2 名前付き関数式」で詳しく述べる。
関数オブジェクトは、それが定義されたときの名前をnameプロパティーとして持つ。
function f1 () {} // 関数宣言
const f2 = function() {}; // 無名関数式
const f3 = function f3n() {}; // 名前付き関数式
const f4name = (function() {}).name; // 無名関数式(代入しない)
console.log(f1.name); // "f1"
console.log(f2.name); // "f2"
console.log(f3.name); // "f3n"
console.log(f4name); // ""(空文字列)
関数宣言や名前付き関数式であれば、関数名として書いたものがそのままnameプロパティーになる。気を付けるべき点は、無名関数式の場合でも、関数オブジェクトを変数に代入する記述をした場合、nameプロパティーはその変数名になるということである。変数に代入する記述でない場合、nameプロパティーは空文字列になる。
関数式は、多くの場合無名関数式として記述する。しかし、関数式に名前をつけることもできる(「2.1.3 関数式」)。この名前は、関数の外からは参照できない。
次の記述で、関数f1の定義は全体に()がついているので、名前付き関数式である。
(function f1() {
console.log("f1");
});
f1(); // f1はここから参照できない
このとき、f1()として外部から呼び出すことはできない。
名前付き関数式の内部からは、その名前を使って呼び出すことができる。次の例を考えよう。関数f2内で、f2(true)として関数f2を呼び出している。ここでは無限ループにならないように、引数にtrueを与えた場合、再びf2を呼び出すことはしないとする。
const f2ref = function f2(p) {
console.log("f2");
if (p) { return; }
f2(true); // f2はここから参照できる
};
f2ref();
結果
f2
f2
関数f2の内部からは、名前f2を使って呼び出すことができる。
このようになる理由は、名前付き関数式を定義したとき、その名前は独立した場所に保持されるためである。関数実行時には、変数を探索するスコープチェーンは、 [関数内部] → [関数の名前] → [関数外部] のようになる。このため、関数の外部からは関数名が参照できない。
関数の名前が独自のスコープになっていることは、次のように確認できる。次のように、名前付き関数式内部で、同名の変数を定義してもエラーにならない。
const f3ref = function f3(p) {
const f3 = function() { console.log("f3int"); };
console.log("f3");
if (p) { return; }
f3(true); // f3は関数内で定義されたものになる
};
f3ref();
結果
f3
f3int
この名前付き関数f3は、その内部で同名の変数f3を定義している。これはエラーにはならない。外側のf3は関数内部のf3で隠蔽され、内部からf3を参照すると、内部で定義された変数となる。
名前付き関数式を使う主な理由は、関数の再帰呼び出しのためである。名前付き関数式を使えば、外部に名前を公開することなく、再帰が行える。
かつて、デバッガの出力が貧弱であった時代、無名関数が避けられたことがあった。デバッガで無名関数を参照すると、どの関数かわからないという問題がしばしば生じた。このような場合、名前を付けておくのが便利という考えもあった。
しかし、名前をつけることは、多少なりとも名前の衝突の問題を増やすことになる。現在では、この目的で名前を付けることは推奨されない。全ての無名関数に適切な名前を付けるのは難しく、適切な名前がついていないことで混乱する場合があるからである。
たとえば、関数オブジェクトを格納した変数と異なる名前であると、混乱の元である。
// 注意!どの関数名が適当なのか不明瞭
const fa = function fb() {};
const fb = function fc() {};
なので、コーディング規約によっては、名前付き関数式を一律に禁止する場合もあるだろう。無名関数は、その名前がついていないことこそがメリットなのである。