2015年10月12日
JavaScriptのクロージャは内部でどう機能するのか
本記事は、原著者の許諾のもとに翻訳・掲載しております。
もうだいぶ前からすでに私はクロージャを使っています。使い方を学びましたが、実際にクロージャがどう機能するのか、また、使うと隠れたところで実際に何が起きるのかを明確に理解しているとは言えませんでした。そもそも、クロージャとは一体何なのでしょうか。 ウィキペディア はあまり役に立ちません。クロージャはいつ生成され、いつ削除されるのでしょうか。どのように実装されるべきなのでしょうか。
"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言語について解説しています。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa