変数のスコープについて

しばしば JavaScript におけるグローバル変数(または大域変数)およびローカル変数(または局所変数)として紹介される、スコープチェーンと実行コンテキストのメカニズムについて。

識別子評価と Reference

識別子は、常に一つのオブジェクトの一つのプロパティを特定する。識別子評価の結果は、これを表現するため、常に基準オブジェクトとプロパティ名の組となり、便宜的に Reference と呼ばれる。他の言語における参照とかリファレンスといった用語とは定義が異なる可能性が強いので、ここはしっかりおさえて置こう。

スコープチェーン

スコープチェーンの基本

スコープチェーン というのは、識別子評価において検索されるオブジェクトのリストのことだ。スコープチェーン内のオブジェクトから識別子と同じ名前を持つプロパティを検索し、プロパティが存在すれば、そのオブジェクトと識別子名による Reference を返す。存在しなければ、次のオブジェクトを検索する。スコープチェーン内のオブジェクトを全て検索しても識別子と同名のプロパティを見つけられなければ、 null と識別子名による Reference を返す。

スコープチェーンは、以下のように生成・変更される。

グローバルコード

とりあえずここまでをやってみよう。次は、超基本的にも思えるコードだ。

ソーステキスト
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 文

では、 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 文

次は、 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 つ。

グローバルコード
プログラムとして解析されるコード。個々の関数宣言、関数式の内部は含まない。
eval コード
eval 関数の引数として解析されるコード。
関数コード
関数宣言、関数式の内部のコード。コード内でネストされた関数宣言また関数式の内部は含まない。

これらの呼び出しはそれぞれ新しい 実行コンテキスト を開始する。新しい実行コンテキストが開始されるとき、次のことが行われる:

スコープチェーンの初期化

新しい実行コンテキストが開始されると、コードの型によって、次のようにスコープチェーンが初期化される。

グローバルコード
スコープチェーンはグローバルオブジェクトのみで構成される。
eval コード
eval 関数を呼び出した実行コンテキストのスコープチェーンがそのまま適用される。
関数コード
新規にオブジェクトを作成し、そのオブジェクトとそれに続く関数(Function オブジェクト)の内部プロパティ [[Scope]] に蓄積されたオブジェクトでスコープチェーンを構成する。

関数コードのスコープチェーン初期化時に生成されるオブジェクトは Activation オブジェクト と呼ばれ、初期値としてプロパティ arguments を持つ。arguments は呼出された関数自身を値とするプロパティ callee と関数の引数をプロパティにもつオブジェクトである。

Function オブジェクトの内部プロパティ [[Scope]] には、そのオブジェクトの生成時のスコープチェーンが与えられている。これはその関数が宣言として与えられている場合でも式として与えられている場合でも同様である。

例えば、グローバルコード直下の Function オブジェクトの内部プロパティ [[Scope]] はみなグローバルオブジェクトのみだ。だからそういう Function オブジェクトの実行コンテキストで初期化されるスコープチェーンは [ Activation オブジェクト, グローバルオブジェクト ] で構成されるリストになる。

ここでは、関数 F が呼出されるときに生成される Activation オブジェクトを便宜的に @F と表記することにする。以降、 @F.arguments という表記は、関数 F の Activation オブジェクトのプロパティ arguments を意味する。

this 値の決定

新しい実行コンテキストが開始されると、コードの型によって、次のように this 値が決定される。

グローバルコード
this 値はグローバルオブジェクトを参照する。
eval コード
eval 関数を呼び出した実行コンテキストの this 値がそのまま使用される。
関数コード
関数の呼出側が this 値を提供する。提供された値がオブジェクトでない場合( null 等) はグローバルオブジェクトを参照する。

変数の具体化

変数の具体化 とは、要するに、各コードを評価していくにあたってあらかじめ使えるオブジェクトを、コード型によって決められた特定のオブジェクトにプロパティとして追加定義する作業である。この特定のオブジェクトを variable オブジェクト(変数オブジェクト?) と呼ぶ。

variable オブジェクトはコード型によって次のオブジェクトが使用される:

グローバルコード
variable オブジェクトには グローバルオブジェクト が用いられる。ここで追加されるのはすべてグローバルオブジェクトのプロパティで、そのプロパティ属性は { DontDelete } である。
eval コード
variable オブジェクトには 呼出コンテキストの variable オブジェクト が用いられる。ここで追加されるのはすべて呼出コンテキストの variable オブジェクトのプロパティで、そのプロパティ属性は { } である。
関数コード
variable オブジェクトには Activation オブジェクト が用いられる。ここで追加されるのはすべて Activation オブジェクトのプロパティで、そのプロパティ属性は { DontDelete } である。

そして、variable オブジェクトに、次の順にプロパティが追加される。

  1. 関数コードの場合、仮引数ごとに、その識別子を名前とするプロパティが作成される。値はそれぞれ受け取った値である。
  2. コード中の 関数宣言 function Identifier () {} ごとに、 Identifier を名前とするプロパティ を作成する。生成される Function オブジェクトを値とする。既にこの名前のプロパティをもっている場合は、その値を置き換える。
  3. コード中の 変数宣言 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);

グローバルコードにおいて行われる処理は次のようになる。

  1. スコープチェーンが [ Global ] で初期化される。
  2. this は Global である。
  3. 関数宣言 F について Global.F が定義され、値は Function オブジェクトとする。このオブジェクトの [[Scope]] は [ Global ] である。
  4. 変数宣言 v0 について Global.v0 が定義され、値を undefined とする。
  5. 変数宣言 v1 について Global.v1 が定義され、値を undefined とする。
  6. グローバルコード本文を実行する。識別子 F は Global.F, v0 は Global.v0, v1 は Global.v1 をそれぞれ参照する。
    グローバルコードのスコープチェーン上のオブジェクトとプロパティ
    オブジェクト / 識別子 p1 p2 p3 F f0 v0 v1
    Global Function undefined undefined

関数呼び出し F ('a','b','c'); で行われる処理は次のようになる。

  1. Activation オブジェクト @F を生成する。
  2. @F の arguments プロパティを定義する。
  3. スコープチェーンが [ @F, Global ] で初期化される。
  4. this は Global である。
  5. 仮引数 p1 について @F.p1 が定義され、値は 'a' とする。
  6. 仮引数 p2 について @F.p2 が定義され、値は 'b' とする。
  7. 仮引数 p3 について @F.p3 が定義され、値は 'c' とする。
  8. 関数宣言 f0 について @F.f0 が定義され、値は Function オブジェクトとする。このオブジェクトの [[Scope]] は [ @F, Global ] である。
  9. 関数宣言 v0 について @F.v0 が定義され、値は Function オブジェクトとする。このオブジェクトの [[Scope]] は [ @F, Global ] である。
  10. 変数宣言 v0 について @F.v0 は既に存在しているので特に何もしない。
  11. 変数宣言 v1 について @F.v1 が定義され、値を undefined とする。
  12. F の関数コード本文を実行する。識別子 p1 は @F.p1, p2 は @F.p2, p3 は @F.p3, f0 は @F.f0, v0 は @F.v0, v1 は @F.v1 をそれぞれ参照する。 v2 は参照時は例外を発生し、設定時は Global にプロパティを追加する。
    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); で行われる処理は次のようになる。

  1. Activation オブジェクト @f0 を生成する。
  2. @f0 の arguments プロパティを定義する。
  3. スコープチェーンが [ @f0, @F, Global ] で初期化される。
  4. this は Global である。
  5. 仮引数 p1 について @f.p1 が定義され、値は 'abc' とする。
  6. f0 の関数コード本文を実行する。識別子 p1 は @f0.p1, v0 は @F.v0, v1 は @F.v1 をそれぞれ参照する。
    f0() 呼出し時のスコープチェーン上のオブジェクトとプロパティ
    オブジェクト / 識別子 p1 p2 p3 F f0 v0 v1
    @f0 'abc'
    @F 'a' 'b' 'c' Function Function undefined
    Global Function undefined undefined

f0 における関数呼び出し v0(); で行われる処理は次のようになる。

  1. Activation オブジェクト @v0 を生成する。
  2. @v0 の arguments プロパティを定義する。
  3. スコープチェーンが [ @v0, @F, Global ] で初期化される。
  4. this は Global である。
  5. v0 の関数コード本文を実行する。識別子 v1 は @F.v1 を参照する。
    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 でもありうる。

Issued: / Revised: / All rights reserved. © 2002-2017 TAKI