JavaScript のクロージャ
入れ子の関数は外の関数の変数にアクセスできる
JavaScript では関数が入れ子になっている時、内側の関数から、外側の関数で宣言された変数にアクセスできます。
関数がネストしている時に、内側の関数が外側の関数の変数にもアクセスできるというスコープを「レキシカルスコープ」といいます。
例えば、次の例をみてください。
function greet() {
let msg = 'Hi there!';
function getMessage() {
return msg;
}
console.log(getMessage());
}
greet();
この実行結果は次の通りです。
念のため、流れを描いておくとこんな風になります。
ポイントは、greet関数の内部で定義されたgetMessage関数が、 greet関数で宣言されている変数msgにアクセスできているというところです。
外側の変数が内側で使える、ということはいわば普通のことなので、ここまでは特に問題ないと思います。
クロージャとは?
次の例に進みましょう。次から、「クロージャらしい」コードの例になります。
function greet() {
let msg = 'Hi there!';
return {
getMessage: function() {
return msg;
}
};
}
let o = greet();
console.log(o);
console.log(o.getMessage());
実行結果は次の通りです。
赤枠で囲った箇所は console.log(o) の出力で、黄色いハイライトは console.log(o.getMessage()) の出力です。
greet関数を呼び出して、その結果として得られるオブジェクトを変数oに割り当てています。 o はgetMessage()というメソッドを持つオブジェクトです。
getMessage()は greet関数で宣言された変数msgを返す関数です。 o.getMessage()として、getMessage関数を呼び出すと、その結果、確かに 'Hi there!'という文字が取得できています。
前の例でみたように、greet関数の中で getMessage関数を呼び出してmsgにアクセスできるのは自然なことに見えます。
しかし、この例のようにgreet関数が返したオブジェクトからでも、getMessage関数が定義されたときに参照できていたmsgにアクセスできるのです。
これは JavaScriptの関数オブジェクトは、関数が定義された時のコンテキストを保存しているからです。つまりこの場合は、単純に戻り値のオブジェクトが返されるだけではなく、 オブジェクト内の関数がアクセスできるコンテキストも一緒に含まれているのです。
このコンテキストのことをレキシカル環境 (lexical environment) といいます。そして、関数とレキシカル環境の組み合わせのことをクロージャといいます。
JavaScript の関数オブジェクトはクロージャです。 クロージャであるために、ネストされた関数の内側の関数は、その外側のスコープにある変数にいつでもアクセスできることになります。
クロージャではない場合には、ローカル変数はレジスタにセットされたりスタックに積み上げられ、関数の呼び出しが終わりスタックが巻き上げられるのに伴い消失します。このためこの例のgreet関数が返って、呼び出し元に戻り値となるオブジェクトが返された後にも、greet関数のローカル変数にアクセスできる、というのはクロージャならではの動作です。