しばしば JavaScript におけるグローバル変数(または大域変数)およびローカル変数(または局所変数)として紹介される、スコープチェーンと実行コンテキストのメカニズムについて。
識別子は、常に一つのオブジェクトの一つのプロパティを特定する。識別子評価の結果は、これを表現するため、常に基準オブジェクトとプロパティ名の組となり、便宜的に Reference と呼ばれる。他の言語における参照とかリファレンスといった用語とは定義が異なる可能性が強いので、ここはしっかりおさえて置こう。
スコープチェーン というのは、識別子評価において検索されるオブジェクトのリストのことだ。スコープチェーン内のオブジェクトから識別子と同じ名前を持つプロパティを検索し、プロパティが存在すれば、そのオブジェクトと識別子名による Reference を返す。存在しなければ、次のオブジェクトを検索する。スコープチェーン内のオブジェクトを全て検索しても識別子と同名のプロパティを見つけられなければ、 null と識別子名による Reference を返す。
スコープチェーンは、以下のように生成・変更される。
with ( expression ) statement
は、 expression から算出したオブジェクトをスコープチェーンの先頭に追加して statement を評価する。評価が終わると、先頭に追加したオブジェクトをスコープチェーンから削除する。catch ( identifier ) block
は、新規に生成したオブジェクトをスコープチェーンの先頭に追加して block を評価する。このオブジェクトは名前 identifier のプロパティを持ち、この catch クローズに投げられたオブジェクトを値とする。評価が終わると、先頭に追加したオブジェクトをスコープチェーンから削除する。とりあえずここまでをやってみよう。次は、超基本的にも思えるコードだ。
window.v0 = 'Global v0'; document.writeln( 'v0 = ' + v0 ); v1 = 'Global v1'; document.writeln( 'v1 = ' + v1 );
ここでのスコープチェーンはグローバルオブジェクト自身を指すオブジェクト window のみで構成されている。 v0 という識別子はオブジェクト window と 'v0' による Reference として評価される。だから、識別子 v0 はオブジェクト window のプロパティ v0 を指す。
一方、 v1 という識別子はスコープチェーン上のオブジェクトプロパティに存在しないので、識別子 v1 は null と 'v1' による Reference として評価される。このような識別子に対して値の取得を試みると、例外が発生する。値の設定を試みると、グローバルオブジェクトのプロパティに追加される。
では、 with 文を使ってスコープチェーンを変更してみよう。[ ] の中のリストはスコープチェーンを示す。
// スコープチェーン[Global] v0 = 'Global v0'; o0 = new Object(); o1 = new Object(); o2 = new Object(); o1.v0 = 'o1.v0'; with(o0){ // スコープチェーン[o0, Global] document.writeln( 'with(o0):v0 = ' + v0 ); with(o1){ // スコープチェーン[o1, o0, Global] document.writeln( 'with(o1):v0 = ' + v0 ); with(o2){ // スコープチェーン[o2, o1, o0, Global] document.writeln( 'with(o2):v0 = ' + v0 ); v0 = 'v0 by with(o2)'; v1 = 'v1 by with(o2)'; document.writeln( 'with(o2):v0 = ' + v0 ); } document.writeln( 'with(o1):v0 = ' + v0 ); } document.writeln( 'with(o0):v0 = ' + v0 ); } document.writeln( 'v0 = ' + v0 ); document.writeln( 'v1 = ' + v1 ); document.writeln( 'o1.v0 = ' + o1.v0 );
with(o0) 内では、識別子 v0 は window.v0 を指す。with(o1) 内では、識別子 v0 は o1.v0 を指す。with(o2) 内では、識別子 v0 は o1.v0 を指す。v0 を変更すると o1.v0 の値が変更される。識別子 v1 は、スコープチェーン上のどのオブジェクトもこの名前のプロパティを持たないので、取得時は例外が発生し、設定時は window オブジェクトのプロパティに追加される。
次は、 try 文を使ったスコープチェーンの変更例だ。 [ ] の中のリストはスコープチェーンを示す。
v0 = 'Global v0'; try{ throw 'thrown value'; } catch (v0) { // スコープチェーン[オブジェクト {v0, (投げられた値)} , Global] document.writeln( 'v0 = ' + v0 ); } // スコープチェーン[Global] document.writeln( 'v0 = ' + v0 );
catch(v0) クローズが生成してスコープチェーンの先頭に追加するオブジェクトは、単に投げられた結果を値とするプロパティ v0 を持つ。
関数コードの中で関数宣言 function Identifier () {}
や変数宣言 var Identifier
をすると、その変数や関数はその関数の中でのみ有効なローカル関数(局所関数)やローカル変数(局所変数)になる、と言われる。これは、関数コードが実行時に設置する実行コンテキストで説明される。
ECMAScript の実行可能コードは、以下に挙げる 3 つ。
これらの呼び出しはそれぞれ新しい 実行コンテキスト を開始する。新しい実行コンテキストが開始されるとき、次のことが行われる:
新しい実行コンテキストが開始されると、コードの型によって、次のようにスコープチェーンが初期化される。
関数コードのスコープチェーン初期化時に生成されるオブジェクトは Activation オブジェクト と呼ばれ、初期値としてプロパティ arguments
を持つ。arguments
は呼出された関数自身を値とするプロパティ callee
と関数の引数をプロパティにもつオブジェクトである。
Function オブジェクトの内部プロパティ [[Scope]] には、そのオブジェクトの生成時のスコープチェーンが与えられている。これはその関数が宣言として与えられている場合でも式として与えられている場合でも同様である。
例えば、グローバルコード直下の Function オブジェクトの内部プロパティ [[Scope]] はみなグローバルオブジェクトのみだ。だからそういう Function オブジェクトの実行コンテキストで初期化されるスコープチェーンは [ Activation オブジェクト, グローバルオブジェクト ] で構成されるリストになる。
ここでは、関数 F が呼出されるときに生成される Activation オブジェクトを便宜的に @F と表記することにする。以降、 @F.arguments という表記は、関数 F の Activation オブジェクトのプロパティ arguments を意味する。
新しい実行コンテキストが開始されると、コードの型によって、次のように this 値が決定される。
変数の具体化 とは、要するに、各コードを評価していくにあたってあらかじめ使えるオブジェクトを、コード型によって決められた特定のオブジェクトにプロパティとして追加定義する作業である。この特定のオブジェクトを variable オブジェクト(変数オブジェクト?) と呼ぶ。
variable オブジェクトはコード型によって次のオブジェクトが使用される:
そして、variable オブジェクトに、次の順にプロパティが追加される。
function Identifier () {}
ごとに、 Identifier を名前とするプロパティ を作成する。生成される Function オブジェクトを値とする。既にこの名前のプロパティをもっている場合は、その値を置き換える。var Identifier
ごとに、 Identifier を名前とするプロパティ を作成して、undefined を値とする。既にこの名前のプロパティをもっている場合は、特に何もしない。用語噴出で混乱が十分すぎるほど予想されるので、ちょっとおさらい。ここではグローバルオブジェクトを Global と表記する。
例えば、次のようなソーステキストがあるとしよう:
function F ( p1, p2, p3 ) { var v0, v1=p1+p2+p3; function f0 (p1) { v1 = p1; return v0(); } function v0 () { return v1; } v2 = f0(v1); } var v0, v1; F( 'a', 'b', 'c' ); document.write(v2);
グローバルコードにおいて行われる処理は次のようになる。
オブジェクト / 識別子 | p1 | p2 | p3 | F | f0 | v0 | v1 |
---|---|---|---|---|---|---|---|
Global | Function | undefined | undefined |
関数呼び出し F ('a','b','c');
で行われる処理は次のようになる。
オブジェクト / 識別子 | p1 | p2 | p3 | F | f0 | v0 | v1 |
---|---|---|---|---|---|---|---|
@F | 'a' | 'b' | 'c' | Function | Function | undefined | |
Global | Function | undefined | undefined |
F における関数呼び出し f0 (v1);
で行われる処理は次のようになる。
オブジェクト / 識別子 | p1 | p2 | p3 | F | f0 | v0 | v1 |
---|---|---|---|---|---|---|---|
@f0 | 'abc' | ||||||
@F | 'a' | 'b' | 'c' | Function | Function | undefined | |
Global | Function | undefined | undefined |
f0 における関数呼び出し v0();
で行われる処理は次のようになる。
オブジェクト / 識別子 | p1 | p2 | p3 | F | f0 | v0 | v1 |
---|---|---|---|---|---|---|---|
@v0 | |||||||
@F | 'a' | 'b' | 'c' | Function | Function | 'abc' | |
Global | Function | undefined | undefined |
では、このコードを実行してみよう。
関数コード内で関数宣言や変数宣言を使用すると、それは arguments オブジェクトや仮引数と共に Activation オブジェクトのプロパティとして定義され、識別子をスコープチェーン上のオブジェクトから検索していくときに最初に引っかかるようになる。そして仕様にも明言されていることだが、プログラムコードはスコープチェーンを通してそのプロパティを取得できるのみで、 Activation オブジェクトを直接取得することは出来ない。だからプログラムコードはこのスコープチェーンが使われている実行コンテキスト上、すなわち関数コード内でしかこれらのプロパティにアクセスできない。そしてこの実行コンテキストを終了すると元の実行コンテキストに復帰するため、元のスコープチェーンが用いられる。これが ECMAScript におけるグローバル変数(大域変数)、ローカル変数(局所変数)の正体である。実装とかメカニズムって言う方がいいかな。
ちなみに ECMAScript では、ブロック { } は実行コンテキストを生成しない。だから for 文や if 文におけるブロック内で変数宣言等を行っても、その for 文、 if 文を実行するコンテキストの variable オブジェクトのプロパティとして追加され、そのブロック内のみでの局所変数にはならない。
かなり変な例だが、関数を何重にも入れ子にしたときのスコープチェーンの例である。この例でも解るとおり、実行コンテキストは関数をコンストラクタとして呼出した場合でも同じように生成される。
function createCF(p) { // スコープチェーン [ @createCF, window ] var v = p; function CF(){ // スコープチェーン [ @CF, @createCF, window ] this.set_v = function (p) { v = p; }; // スコープチェーン [ @set_v, @CF, @createCF, window ] this.get_v = function () { return v; }; // スコープチェーン [ @get_v, @CF, @createCF, window ] }; // 新しいオブジェクトを生成して返す return new CF(); } // オブジェクト生成 var cf1 = createCF('cf1'); //var cf2 = createCF('cf2'); // @createCF.v を参照 document.writeln( 'cf1.get_v() = ' + cf1.get_v() ); document.writeln( 'cf2.get_v() = ' + cf2.get_v() ); // @createCF.v を変更 cf1.set_v('CF1'); cf2.set_v('CF2'); // @createCF.v を参照 document.writeln( 'cf1.get_v() = ' + cf1.get_v() ); document.writeln( 'cf2.get_v() = ' + cf2.get_v() );
createCF 内の関数 CF を各呼出しの度に同一のオブジェクトとして作成することを、実装は許されているけれども、その挙動が必須ではないことに注意。 cf1.constructor==cf2.constructor
の結果は true でも false でもありうる。