もうだいぶ前からすでに私はクロージャを使っています。使い方を学びましたが、実際にクロージャがどう機能するのか、また、使うと隠れたところで実際に何が起きるのかを明確に理解しているとは言えませんでした。そもそも、クロージャとは一体何なのでしょうか。ウィキペディアはあまり役に立ちません。クロージャはいつ生成され、いつ削除されるのでしょうか。どのように実装されるべきなのでしょうか。
"use strict";
var myClosure = (function outerFunction() {
var hidden = 1;
return {
inc: function innerFunction() {
return hidden++;
}
};
}());
myClosure.inc(); // returns 1
myClosure.inc(); // returns 2
myClosure.inc(); // returns 3
// Ok, いいですね。ですが、これはどう実装され、
// 見えないところでは何が起こっているのでしょう?
やっと理解できた時は、非常に興奮しました。そして、解説しようと思ったのです。もう絶対に忘れることはありませんから。下記の格言のようにね。
言われたことは、忘れる。教わったことは、覚える。取り組んだことは、学ぶ。
©ベンジャミン・フランクリン
クロージャについての説明をいろいろと読みあさり、どのオブジェクトが他の何を参照するのか、どのオブジェクトを継承するのかなど、オブジェクト同士の関係について想像してみました。結局、役に立つイラストを見つけることができませんでしたので、自分で図に描くことにしました。
読者のみなさんはすでにJavaScriptに詳しく、Global Objectが何か、グローバルオブジェクトがJavaScriptの関数で第一級オブジェクトであることなどを知っていると思います。
スコープチェーン
JavaScriptのプログラムを実行している時、ローカル変数を格納する場所が必要になります。それを、スコープオブジェクト(LexicalEnvironmentとする人もいます)と呼びましょう。例えば、ある関数を呼び出し、関数がローカル変数を定義した時、これらの変数をスコープオブジェクトに保存します。オブジェクト自体を直接参照できないという重要な点以外では、普通のJavaScriptオブジェクトと同じだと思って良いでしょう。これらのオブジェクトはプロパティのみ変更でき、スコープオブジェクト自体を参照することはできません。
このスコープオブジェクトの概念は、ローカル変数をスタックに保存するC言語やC++とは異なります。JavaScriptで、ローカル変数はヒープ領域に割り当てられます(あるいは、そうかのように振る舞います)。そのため、関数が返された後も、割り当てられたままかもしれません。これについては、後でさらに触れます。
期待どおり、スコープオブジェクトには親がいます。コードで変数にアクセスしようとすると、現在のスコープオブジェクトのプロパティをインタープリタが検索します。プロパティが存在しない場合、インタープリタは親のスコープオブジェクトのプロパティを検索します。これは、値が見つかるまで、あるいは、親が存在しないと分かるまで続きます。このスコープオブジェクトのシーケンスをスコープチェーンと呼びましょう。
スコープチェーンで変数を解決する振る舞いは、プロトタイプ継承でのそれに似ていますが、一つ重要な違いがあります。プロトタイプチェーンの場合、普通のオブジェクトの存在しないプロパティへアクセスを試みて、そのプロパティがプロトタイプチェーンにも存在しない場合、エラーではなくただundefinedを返すのに対し、スコープチェーンの存在しないプロパティ(例えば、存在しない変数)にアクセスを試みると、ReferenceErrorが発生することです。
スコープチェーンの最後の要素は常にグローバルオブジェクトです。トップレベルのJavaScriptコードの場合、スコープチェーンの要素はグローバルオブジェクトのみとなります。そのため、トップレベルのコードで変数を定義する場合は、グローバルオブジェクトに定義されます。ある関数が呼び出されるとスコープチェーンは複数のオブジェクトを含みます。トップレベルのコードで関数を呼び出すと、ちょうど2つオブジェクトをスコープチェーンに含むことが保証されると思うかもしれませんが、必ずしもそうではありません。関数によって異なりますが、2つ以上のスコープオブジェクトが存在することがあります。これについては、後でさらに触れます。
トップレベルコード
理論はここまでにして、実際にコードを見てみましょう。下記はとても簡単な例です。
"use strict"; var foo = 1; var bar = 2;
変数を2つだけトップレベルコードで生成します。上記でも述べたように、トップレベルコードではスコープオブジェクトはグローバルオブジェクトです。
上記の図では、実行コンテクスト(ただのmy_script.jsのコード)があり、スコープオブジェクトを参照しています。実際には、標準でホストに限定されたものがGlobal Objectに多く含まれていますが、図ではそれを省いています。
ネストされていない関数
では、今度は下記のスクリプトを検討しましょう。
"use strict";
var foo = 1;
var bar = 2;
function myFunc() {
//-- 関数のローカル変数を定義
var a = 1;
var b = 2;
var foo = 3;
console.log("inside myFunc");
}
console.log("outside");
//-- and then, call it:
myFunc();
myFunc関数が定義されると、myFunc識別子が現在のスコープオブジェクトに追加されます(この場合はグローバルオブジェクト)。この識別子が関数オブジェクトを参照します。関数オブジェクトは、関数のコードや他のプロパティを持っています。ここで興味があるのは、internalプロパティの[[scope]]です。これは、現在のスコープオブジェクトを参照します。つまり、関数が定義された時(この場合も、グローバルオブジェクト)に、アクティブ状態のスコープオブジェクトを参照します。
そのため、console.log("outside");が実行される時には、次のような構成になります。
ここでもまた、ちょっと考えてみましょう。myFunc変数が参照する関数オブジェクトは、関数コードを保持するだけでなく、関数が定義された時に有効なスコープオブジェクトを参照します。これはとても重要です。
関数が呼び出されると、myFunc(とその引数の値)のローカル変数を持つ新しいスコープオブジェクトが生成されます。そして、この新しいスコープオブジェクトは、関数を呼び出した時に参照したスコープオブジェクトを継承します。
そのため、実際にはmyFuncが呼び出され、次のような構成になります。
ここにあるのがスコープチェーンです。myFunc内の変数にアクセスしようとすると、まずJavaSciptは、最初のスコープオブジェクトmyFunc() scopeで検索します。検索が失敗すると、チェーンの次のスコープオブジェクト(この場合はGlobal object)を検索します。リクエストされたプロパティがチェーンの全てのスコープオブジェクトにない場合、ReferenceErrorが発生します。
例えば、myFuncからaにアクセスした場合、myFunc() scopeから1の値が返されます。fooにアクセスした場合は、同じmyFunc() scopeから3の値を返し、fooプロパティを効果的にGlobal objectから隠します。barにアクセスした場合は、Global objectから2の値が返されます。プロトタイプ継承のような動作をします。
ここで覚えておかなければならない重要な点は、これらのスコープオブジェクトは参照されている限り存続するということです。しかし、ある特定のスコープオブジェクトへの参照がなくなると、ガベージコレクションが起動し、そのスコープオブジェクトは解放されます。
myFunc()が完了し、myFunc() scopeが参照されていないと、ガベージコレクションが起動し、解放されてしまうため、次のような構成になります。
これ以降は、図が大きくなりすぎてしまうため、明確な関数オブジェクトを省きます。もうすでにご存じだと思いますが、JavaScriptで関数を参照するということは、関数オブジェクトを参照することで、同じくスコープオブジェクトを参照します。
このことを忘れないでください。
ネストされた関数
前にも述べたように、関数が完了すると、スコープオブジェクトの参照が外れるため、ガベージコレクションにより処理されます。しかし、関数をネストして定義して返した(あるいは、外部に保存した)場合はどうでしょうか。ご存じのとおり、関数オブジェクトは常にその関数オブジェクトを生成したスコープオブジェクトを参照します。そのため、ネストされた関数を定義する場合、外側の関数の現在のスコープオブジェクトを参照します。また、ネストされた関数を外部のどこかに格納すると、ネストの外側の関数が返されても参照は維持されるため、スコープオブジェクトがガベージコレクションにより処理されることはありません。次を考えてみてください。
"use strict";
function createCounter(initial) {
//-- 関数のローカル変数を定義
var counter = initial;
//-- ネスト関数を定義。それぞれが現在のスコープオブジェクトへの
// 参照を持つ
/**
* 内部のcounterに引数を足す。
* 引数が有限数でなかったり1より小さい場合は1を足す
*/
function increment(value) {
if (!isFinite(value) || value < 1){
value = 1;
}
counter += value;
}
/**
* Returns current counter value.
*/
function get() {
return counter;
}
//-- ネストされた関数への参照を含む
// オブジェクトを返す
return {
increment: increment,
get: get
};
}
//-- create counter object
var myCounter = createCounter(100);
console.log(myCounter.get()); //-- prints "100"
myCounter.increment(5);
console.log(myCounter.get()); //-- prints "105"
createCounter(100);を呼び出すと、次のような構成になります。
お気づきだとは思いますが、ネストされた関数incrementとgetが、createCounter(100) scopeを参照しています。しかし、createCounter()が、これらの関数を参照するオブジェクトを返すため、次のようになります。
ちょっと考えてみましょう。createCounter(100)がすでに返されていますが、返されたオブジェクトのスコープは存在し、内部の関数によってのみアクセスすることができます。createCounter(100)スコープオブジェクトに直接アクセスするのは全く不可能であり、myCounter.increment()あるいはmyCounter.get()の呼び出しだけが可能です。これら関数は、createCounterスコープに特別に直接アクセスできるようになっています。
では、例としてmyCounter.get()を呼び出してみましょう。どの関数を呼び出しても新しいスコープオブジェクトが生成され、参照するスコープチェーンはこの新しいスコープオブジェクトによって増やされていくことを思い出してください。そのため、myCounter.get()を呼び出すと、次のようになります。
チェーン内での、関数get()の最初のスコープオブジェクトは空のオブジェクトget() scopeなので、get()が変数counterにアクセスする時、JavaScriptはスコープチェーン内の最初のオブジェクト上では見つけられず、次のスコープオブジェクトに移動します。そして変数counterをcreateCounter(100) Scopeで使い、関数get()はただそれを返します。
お気づきかもしれませんがmyCounterオブジェクトは、this(図の中で赤い矢印で示されている箇所)として追加でmyCounter.get()に渡されています。なぜかというと、thisは決してスコープチェーンの一部にはならず、そのことを認識しておく必要があるからです。このことについては後ほどもう少しご説明します。
increment(5)の呼び出しはもう少し面白いものです。引数を持つ関数だからです。
ご覧のとおり、valueという引数は、increment(5)を一度呼び出すためだけに生成されたスコープオブジェクトに保存されます。この関数が変数valueにアクセスすると、Javascriptはすぐさま、スコープチェーンの中の最初のオブジェクト内を探します。しかしその関数がcounterにアクセスする時、JavaScriptはスコープチェーンの最初のオブジェクトでは見つけられず、次のスコープオブジェクトに移動し、そこでまた探します。increment()はcreateCounter(100) scopeの中の変数counterを変更します。そして、仮想的には他にこの変数を変更できるものはありません。だからこそクロージャはこんなにもパワフルなのです。オブジェクトmyCounterに不正アクセスすることは不可能です。クロージャは機密性の高い情報を保存するのに非常に適切な場所なのです。
createCounter()の引数initialは、使用されていないもののスコープオブジェクト内にも保存されているということにお気づきでしょうか。つまり、明確にvar counter = initial;を取り除けば少しメモリを節約できます。initialをcounterに名前を変更し、直接使うのです。しかし、分かりやすいよう、ここでは明示的な引数initialとvar counterを使っています。
スコープの境界が”動的”であるということを強調することが重要です。関数が呼び出された時、現在のスコープチェーンはこの関数のためにはコピーされていません。スコープチェーンは新しいスコープオブジェクトによって増えていきます。そして、チェーン内のスコープオブジェクトが何らかの関数によって変更された場合、この変更は、スコープチェーンにこのスコープオブジェクトを持つすべての関数から、すぐに可観測な状態になります。increment()がcounterの値を変更したら、次に呼び出されたget()は更新された値を返します。
このよく知られた例は機能しないのはそういった理由からです。
"use strict";
var elems = document.getElementsByClassName("myClass"), i;
for (i = 0; i < elems.length; i++) {
elems[i].addEventListener("click", function () {
this.innerHTML = i;
});
}
このループの中で複数の関数が作られ、それらの関数はすべてスコープチェーンの中の同じスコープオブジェクトを参照します。だから、それらの関数は自分用のコピーではなく、まったく同じ変数iを使うのです。この例に関してさらに詳しく知りたければ、ループ内に関数を作らないを参照してみてください。
似ている関数オブジェクト、違うスコープオブジェクト
さて、このcounterの例題を少し拡張して、もう少し楽しいことをしましょう。複数のcounterオブジェクトを作成したらどうなるでしょうか。簡単ですよ。
"use strict";
function createCounter(initial) {
/* ... see the code from previous example ... */
}
//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);
myCounter1とmyCounter2が作成されると、下記のようになります。
それぞれの関数オブジェクトはスコープオブジェクトを参照しているということをちゃんと覚えておいてください。上の例ではmyCounter1.incrementとmyCoutner2.incrementが参照する関数オブジェクトは、まったく同じコードと同じプロパティの値(name、lengthとその他)を備えていますが、それらの[[scope]]は違うスコープオブジェクトを参照しているのです。
分かりやすくするために他の関数オブジェクトは図に含めていませんが、ちゃんと存在しています。
例を挙げます。
var a, b; a = myCounter1.get(); // a equals 100 b = myCounter2.get(); // b equals 200 myCounter1.increment(1); myCounter1.increment(2); myCounter2.increment(5); a = myCounter1.get(); // a equals 103 b = myCounter2.get(); // b equals 205
このようになります。クロージャのコンセプトは非常にパワフルですよね。
スコープチェーンと”this”
好き嫌いに関わらず, thisはスコープチェーンの一部としてはまったく保存されません。その代わり、thisの値は関数呼び出しパターンに依存します。つまり、同じ関数を、thisとして渡された違う値により呼び出すこともあるかもしれません。
呼び出しパターン
見出しの名前が内容を表していますから詳しくは述べませんが、簡潔な概要として、4つの呼び出しパターンを紹介しています。以下をご覧ください。
メソッド呼び出しパターン
"use strict";
var myObj = {
myProp: 100,
myFunc: function myFunc() {
return this.myProp;
}
};
myObj.myFunc(); //-- returned 100
呼び出しの表現がrefinement(ドット、または[subscript])を含んでいる場合、その関数はメソッドとして呼び出されます。ですから、上の例ではmyFunc()に渡されたthisはmyObjにより参照されます。
関数呼び出しパターン
"use strict";
function myFunc() {
return this;
}
myFunc(); //-- returns undefined
Refinementがない場合は、コードがstrictモードで実行されているかどうかによって異なります。
- strictモードでは、
thisはundefinedである - 非strictモードでは
thisはグローバルオブジェクトを指し示す
上記のコードは“use strict”;によりstrictモードで実行されるので、myFunc()はundefinedを返します。
コンストラクタ呼び出しパターン
"use strict";
function MyObj() {
this.a = 'a';
this.b = 'b';
}
var myObj = new MyObj();
関数がnewというプレフィックスで呼ばれる場合、JavaScriptは関数のプロパティprototypeから継承された新しいオブジェクトを割り当てます。そして、新たに割り当てられたオブジェクトはthisとして、関数に渡されます。
Apply呼び出しパターン
"use strict";
function myFunc(myArg) {
return this.myProp + " " + myArg;
}
var result = myFunc.apply(
{ myProp: "prop" },
[ "arg" ]
);
//-- result is "prop arg"
このように、thisとして任意の値を渡すことができます。上記の例ではそのためにFunction.prototype.apply()を使いました。さらに詳しい説明は、以下を参照してください。
次に挙げる例では、主にメソッド呼び出しパターンを使います。
ネストされた関数での”this”の使用
下記について考えてみてください。
"use strict";
var myObj = {
myProp: "outer-value",
createInnerObj: function createInnerObj() {
var hidden = "value-in-closure";
return {
myProp: "inner-value",
innerFunc: function innerFunc() {
return "hidden: '" + hidden + "', myProp: '" + this.myProp + "'";
}
};
}
};
var myInnerObj = myObj.createInnerObj();
console.log( myInnerObj.innerFunc() );
出力:hidden: 'value-in-closure', myProp: 'inner-value'
myObj.createInnerObj()が呼ばれる時までは、下記のようになっています。
そしてmyInnerObj.innerFunc()を呼ぶと、下記のようになります。
上の図を見ると、myObj.createInnerObj()に渡されたthisはmyObjを指していることが明らかですが、myInnerObj.innerFunc()に渡されたthisはmyInnerObjを指しています。上記で説明したように、どちらの関数もメソッド呼び出しパターンで呼ばれています。それでinnerFunc()内のthis.myPropは"outer-value"ではなく"inner-value"を評価するのです。
ですから、以下のように、innerFunc()をだましてmyPropを使わせることが簡単にできます。
/* ... see the definition of myObj above ... */
var myInnerObj = myObj.createInnerObj();
var fakeObject = {
myProp: "fake-inner-value",
innerFunc: myInnerObj.innerFunc
};
console.log( fakeObject.innerFunc() );
出力: hidden: 'value-in-closure', myProp: 'fake-inner-value'
または、apply()やcall()を使うと以下のようになります。
/* ... see the definition of myObj above ... */
var myInnerObj = myObj.createInnerObj();
console.log(
myInnerObj.innerFunc.call(
{
myProp: "fake-inner-value-2",
}
)
);
出力: hidden: 'value-in-closure', myProp: 'fake-inner-value-2'
しかし、しばしばinner関数は、呼び出された方法に関わらず、outer関数に渡されたthisへのアクセスを必要とします。これには共通の表現方法があります。私たちは必要な値を明確にクロージャ内に保存する必要があります(つまり、現在のスコープオブジェクト内に)。例えばvar self = this;のように。そしてthisの代わりにinner関数の中のselfを使います。
次を考えてみてください。
"use strict";
var myObj = {
myProp: "outer-value",
createInnerObj: function createInnerObj() {
var self = this;
var hidden = "value-in-closure";
return {
myProp: "inner-value",
innerFunc: function innerFunc() {
return "hidden: '" + hidden + "', myProp: '" + self.myProp + "'";
}
};
}
};
var myInnerObj = myObj.createInnerObj();
console.log( myInnerObj.innerFunc() );
出力: hidden: 'value-in-closure', myProp: 'outer-value'
このようすると、次の図のようになります。
ご覧のとおり、selfはクロージャの中に保存されていますが、今回innerFunc()は、thisとしてouter関数に渡された値にアクセスがあります。
結論
記事の冒頭での疑問に答えてみましょう。
- クロージャとは何か? →関数オブジェクトとスコープオブジェクト両方を参照するオブジェクトです。実際、すべてのJavaScriptの関数はクロージャです。スコープオブジェクトがないのに関数オブジェクトを参照するのは不可能です。
- いつ生成されるのか? →すべてのJavaScriptの関数がクロージャなので、この答えは明白です。関数を定義した時、実際にはクロージャも定義しているのです。ですから、生成されるのは関数が定義された時です。しかし、クロージャの生成と、新しいスコープオブジェクトの生成とは区別する必要があります。クロージャ(関数+現在のスコープチェーンへの参照)は関数が定義された時に生成されますが、新しいスコープオブジェクトは関数が呼び出された時に生成され(そしてクロージャのスコープチェーンを増やすのに使われ)ます。
- いつ削除されるのか? →JavaScriptの一般的なオブジェクトと同じように、参照元がなくなった時にガベージコレクションされます。
以下は参考文献です。
* 『JavaScript: The Good Parts』 Douglas Crockford著。(訳注: 日本語版があります。『JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス』)クロージャの働きについて理解するのはいいことですが、正しい使い方を学ぶ方がもっと重要です。この本は非常に簡潔で、多くの優れていて便利なパターンが掲載されています。
* 『JavaScript: The Definitive Guide』 David Flanagan著。(訳注: 日本語版がありますJavaScript 第6版)タイトルから推測されるように、この本では詳細にわたってJavaScript言語について解説しています。