JavaScript のスコープチェーンとクロージャを理解する
前回で JavaScript のスコープの基本がわかったので、今回はスコープチェーンとクロージャを勉強してみました。 Call オブジェクトとクロージャの理解がかなり大変でした・・。
変数オブジェクト
JavaScript で変数の宣言と参照をするということは、変数オブジェクトを読み書きするということです。
- 変数オブジェクトというのは、key と value による変数管理専用のハッシュテーブルのこと
- key が変数名、value が値のセットになっているテーブルで、変数の数だけレコードができるイメージ
- 変数オブジェクトはプログラマが意識することのない、便宜的なオブジェクト
グローバルオブジェクト
JavaScript は、ブラウザが新しいページを読み込んだとき、内部的に新しいグローバルオブジェクトを生成して初期化します。 グローバルオブジェクトとは、グローバル変数やグローバル関数を管理するための、変数オブジェクトです。
つまり、グローバル変数を定義するということは、実際にはグローバルオブジェクトのプロパティを定義するということです。 ブラウザ上では window オブジェクトがグローバルオブジェクトなので、トップレベルで宣言した変数が window オブジェクトのプロパティとして扱えるようになるのも納得できます。
var n = 10; console.log(window.n); // > 10
Call オブジェクト
ローカル変数もグローバルオブジェクトと同じように、関数が実行されたタイミングで変数オブジェクトができます。 それを Call オブジェクトといいます。目に見えないオブジェクトで、プログラマが直接操作することはできません。
関数が実行されたタイミングで、var で宣言された変数と実行時に渡された引数などが、Call オブジェクトに格納されます。
Call オブジェクトは関数実行が終了すると消えます。ローカル変数の記憶領域が関数実行終了とともに破棄されるのは、これが関係しているのかもしれません。
Call オブジェクトに含まれる情報
- その関数内のローカル変数の値
- 関数に渡された引数名とその値
- 引数情報を管理するオブジェクト(arguments オブジェクト)
- this
- 親の Call オブジェクトの場所(アドレス情報)
スコープチェーン
変数 x がグローバル変数とローカル変数で名前がぶつかっている場合、値を調べる手順は次のとおりです(これを名前解決という)。
- チェーン先頭の Call オブジェクトから x というプロパティを探す。
- あればその値を返し、なければチェーンの次の Call オブジェクトを探す。
- あればその値を返し、なければチェーンの最後まで順に Call オブジェクトを探していく。
- グローバルオブジェクトにも見つからなければ、x はこのスコープ内に存在しないので、エラーになる。
チェーンの先頭は一番内側にある Call オブジェクトで、外側に行くにつれて優先度が下がります。この繋がりをスコープチェーンといい、Call オブジェクトに含まれる「親の Call オブジェクトの場所」の情報をたどっていくことによって、スコープチェーンが成り立ちます。
スコープチェーンを理解することによって、次のようなコードで名前解決がどのように行われるかをイメージできるようになりました。
var a = "Global"; var b = "Global"; function outer() { var a = "Outer"; function inner() { var c = "Inner"; console.log(c); // > "Inner" (自身の Call Obj で発見) console.log(b); // > "Global" (inner なし → outer なし → Global Obj で発見) console.log(a); // > "Outer" (inner なし → outer の Call Obj で発見) } inner(); // 呼び出し } outer(); // 呼び出し
クロージャ
クロージャとは、ローカル変数の状態を保持できる関数のことです。 通常ローカル変数は関数の呼び出しが終わると破棄されますが、クロージャはローカル変数を参照し続けられます。
クロージャのつくりかた
ローカル変数を参照している関数内の関数のことをクロージャといい、次のようにつくります。
- 関数の中にさらに関数を作る
- 外側の関数のスコープ内で変数を定義する
- 関数の中で入れ子になった関数を戻り値として返す
- 内側の関数から、外側の変数を参照する
クロージャでカウンタをつくる
function fn(n) { var cnt = n; return function() { // この無名関数がクロージャ return ++cnt; // スコープチェーンにより、外側のローカル変数 cnt を参照 } } var f = fn(0); // fn 関数の実行 console.log(f()); // > 1 console.log(f()); // > 2 console.log(f()); // > 3
上記のコードでは、次のようなことが内部で行われています。
var f = fn(0);
で fn 関数を実行- fn 関数の戻り値は内側の無名関数なので、f には無名関数のアドレスが代入される
- このとき、無名関数が内部的に参照しているローカル変数 cnt も一緒に引っ張ってくる
f();
とは、無名関数を実行すること(ここではreturn ++cnt;
)なので、cnt に渡した 0 がインクリメントされて 1 として返ってくる- 次に
f();
を実行したときも、匿名関数が cnt を参照し続けているため、先ほどの 1 をさらにインクリメントした 2 が返ってくる
内側の関数は、スコープチェーンによって、外側の関数の Call オブジェクトを参照し続けることができます。
独立したスコープチェーン
先ほどのカウンタで、fn 関数の実行を複数行った場合の挙動は次のとおりです。
function fn(n) { var cnt = n; return function() { return ++cnt; } } var f1 = fn(0); // fn 関数の実行 1 var f2 = fn(10); // fn 関数の実行 2 console.log(f1()); // > 1 console.log(f2()); // > 11 console.log(f1()); // > 2 console.log(f2()); // > 12
Call オブジェクトは関数の実行のタイミングで生成されます。よって、f1 と f2 で fn 関数を実行(インスタンス化)することで、別々の Call オブジェクトが生成され、スコープチェーンも別物になります。それぞれのローカル変数 cnt も別々の記憶領域として確保され、独立しています。
fn 関数は、インスタンス化するときに、一度だけ実行されるコンストラクタの役割をしています。 クロージャ自身はメソッド、クロージャから参照されるローカル変数 cnt はプロパティと考えられます。
クロージャによって、データの記憶領域をもったメソッドを作ることができます。
クロージャの使いどころ
クロージャでできることはなんとなく分かったけれど、実際にはどう使えばいいのかと考えていたところ、猿でもわかるクロージャ超入門 6 クロージャの応用例「注文ボタン」 のページで、なるほどという使い方が紹介されていたので、引用させていただきます。
ショッピングカートの「注文する」ボタンで、2重クリックすると2回決済されてしまうというサイトをたまに見かけます。 jQuery + クロージャを使うことで、これを防止してみましょう。
<form name="frm" id="frm"> <input type="submit" value="注文する" /> </form>
$(function(){ var isClicked = false; $('#frm').submit(function(){ if (isClicked) { alert('すでにクリック済みです。'); return false; } isClicked = true; }); });
たしかにクロージャを使って、isClicked ローカル変数の状態を保存しておけば、2 重注文を防げますね。
まとめ
- JavaScript で変数の宣言と参照をするということは、変数オブジェクトを読み書きするということ
- 変数の名前解決は、スコープチェーンによって内側のローカル変数から順に探していく
- スコープチェーンは、生成したインスタンスごとで別物になる
- クロージャによって、ローカル変数の状態を保持できるメソッドをつくれる