JavaScriptの一番パワフルな特徴の一つとしてクロージャが使える事が挙げられます。これはスコープがいつも外部に定義されたスコープにアクセスできるという事です。JavaScriptの唯一のスコープは関数スコープですが、全ての関数は標準でクロージャとして振る舞います。
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
ここでCounterは2つのクロージャを返します。関数incrementと同じく関数getです。これら両方の関数はCounterのスコープを参照し続けます。その為、そのスコープ内に定義されているcount変数に対していつもアクセスできるようになっています。
JavaScriptでは、スコープ自体を参照・代入する事が出来無い為に、外部から変数countにアクセスする手段がありません。唯一の手段は、2つのクロージャを介してアクセスする方法だけです。
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
上記のコードはCounterのスコープ中にある変数countの値を変更する事はありません。foo.hackはそのスコープで定義されていないからです。これはグローバル変数countの作成 -またはオーバーライド- の代わりになるでしょう。
一つ良くある間違いとして、ループのインデックス変数をコピーしようとしてか、ループの中でクロージャを使用してしまうというものがあります。
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
上記の例では0から9の数値が出力される事はありません。もっと簡単に10という数字が10回出力されるだけです。
匿名関数はiへの参照を維持しており、同時にforループは既にiの値に10をセットし終ったconsole.logが呼ばれてしまいます。
期待した動作をする為には、iの値のコピーを作る必要があります。
ループのインデックス変数をコピーする為には、匿名ラッパーを使うのがベストです。
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
外部の匿名関数はiを即座に第一引数として呼び出し、引数eをiの値のコピーとして受け取ります。
eを参照しているsetTimeoutを受け取った匿名関数はループによって値が変わる事がありません。
他にこのような事を実現する方法があります。それは匿名ラッパーから関数を返してあげる事です。これは上記のコードと同じような効果があります。
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}