詳細 ECMA-262-3 第2章 変数オブジェクト

はじめに

プログラムというものにおいて私たちは、関数及び変数を宣言することによりシステムを構築してゆきます。しかし、インタプリタは、どのように、そしてどこで、私たちのデータ(関数・変数)を見つけるのでしょうか?。私たちが必要なオブジェクトを参照するとき、何が起こっているのでしょうか?

ECMAScript プログラマの多くは、変数が実行コンテキストと密接に関わっていることを知っています。

var a = 10; // グローバルコンテキストの変数
 
(function () {
  var b = 20; // 関数コンテキストのローカル変数
})();
 
alert(a); // 10
alert(b); // "b" is not defined(参照エラー)

同様に、現行バージョンの仕様においては、隔離されたスコープは "関数" 型コードからなる実行コンテキストによってのみ作られるということも、多くのプログラマの知るところです。すなわち C/C++ と比較したとき、例えば ECMAScript では for ループのブロックはローカルコンテキストを生成しません。

for (var k in {a: 1, b: 2}) {
  alert(k);
}
 
alert(k); // 変数 "k" は、ループが終了しても依然としてスコープ中に存在します。

私たちがデータを宣言するとき何が起こっているのか、より詳しく見てゆきましょう。


データ宣言

変数が実行コンテキストと関係があるならば、データがどこに保存され、データをどう取得するのか、実行コンテキストはわかっているはずです。このメカニズムを、変数オブジェクトと呼びます。

変数オブジェクト( Variable Object 以下 VO と略します)は、実行コンテキストに関連する特別なオブジェクトであり、そのコンテキスト中で宣言された以下の内容を保管します。

  • 変数(var 、 VariableDeclaration /変数定義)
  • 関数の定義( FunctionDeclaration 、以下 FD)
  • 関数の仮引数

定義上は、例として、変数オブジェクトを通常の ECMAScript オブジェクトとして表現することが可能です。

VO = {};

そしてこれまでご説明した内容から、 VO は実行コンテキストのプロパティですから、

activeExecutionContext = {
  VO: {
    // コンテキストのデータ(var 、 FD 、 関数の引数)
  }
};

間接的な変数の参照( VO のプロパティ名を通じた参照)は、グローバルコンテキスト(グローバルオブジェクトもまたそれ自体が変数オブジェクトなのですが、これは後述します)の変数オブジェクトにのみ許されています。その他のコンテキストにおいては、直接 VO を参照することはできません。これは純粋に実装上の仕組みです。

私たちが変数または関数を定義するということは、与えた名前と、変数の値をもった新しいプロパティを、 VO に生成することに他なりません。

例:

var a = 10;
 
function test(x) {
  var b = 20;
};
 
test(30);

対応する変数オブジェクトは次のようになります。

// グローバルコンテキストの変数オブジェクト
VO(globalContext) = {
  a: 10,
  test: <関数への参照>
};
 
// "test" 関数コンテキストの変数オブジェクト
VO(test functionContext) = {
  x: 30,
  b: 20
};

ただし、実装(および仕様)のレベルにおいては、変数オブジェクトは抽象的な要素です。具体的な、実際の実行コンテキストにおいては、 VO は異なる名称を与えられ、異なる初期構造を持つことでしょう。


様々な実行コンテキストにおける変数オブジェクト

いくつかの演算(例えば変数の具体化)や変数オブジェクトのふるまいは、全ての種類の実行コンテキストで共通しています。この観点から言えば、変数オブジェクトはベースとなる抽象的な存在であると捉えるとよいでしょう。そして、特に関数コンテキストにおいては、変数オブジェクトに追加の詳細が定義され得ます。

抽象的な変数オブジェクト(変数具体化プロセスにおける一般的なふるまい)
 
  ║
  ╠══> グローバルコンテキストの変数オブジェクト
  ║        (VO === this === global)
  ║
  ╚══> 関数コンテキストの変数オブジェクト
           (VO === AO,  オブジェクト、 <仮引数> が追加される)

これを詳しく見てみましょう。


グローバルコンテキストにおける変数オブジェクト

ここでまず先に、グローバルオブジェクトに定義を与えましょう。

グローバルオブジェクトは、どんな実行コンテキストに進入するよりも前に生成されるオブジェクトです。このオブジェクトはただ一つだけ存在し、そのプロパティはプログラムのどの箇所からも参照でき、グローバルオブジェクトのライフサイクルはプログラムとともに終了します。

グローバルオブジェクトは、例えば、 Math 、 String 、 Data 、 parseInt といった、グローバル関数やグローバルにアクセス可能な組み込みオブジェクトなどのプロパティとともに生成されます。その中には、グローバルオブジェクトそのものへの参照となるオブジェクトもあります。例えばそれは、 DOM 環境においては、グローバルオブジェクトの window プロパティがグローバルオブジェクトそのものを参照します(もちろん、全ての実装においてそうであるということではありません)。

global = {
  Math: <...>,
  String: <...>
  ...
  ...
  window: global
};

グローバルオブジェクトのプロパティを参照するとき、大抵プリフィックスは省略されます。なぜなら、グローバルオブジェクトにはその名前によって直接にアクセスすることはできないからです。ただし、グローバルコンテキスト中の this 値を通じて、または上述したように、グローバルオブジェクト自体への再帰参照(例えば DOM における window )を通じて参照可能です。従って、単純に以下のように書くことができます。

String(10); // global.String(10); を意味します
 
// プリフィックスを用いれば…
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;

つまり、グローバルコンテキスト上の変数オブジェクトに話を戻せば、変数オブジェクトとはグローバルオブジェクトそのものであるわけです。

VO(globalContext) === global;

このポイントはぜひ正確に理解してください。つまりこれによってこそ、私たちはグローバルコンテキスト中で宣言された変数を、グローバルオブジェクトのプロパティを通じて間接的に参照することができるからです(例えば、変数名が前もってわからないような場合であってもです)。

var a = new String('test');
 
alert(a); // 直接参照。 VO(globalContext) に見つかります。: "test"
 
alert(window['a']); // グローバルオブジェクト === VO(globalContext) 経由の間接参照: "test"
alert(a === this.a); // true
 
var aKey = 'a';
alert(window[aKey]); // 間接参照。動的なプロパティ名経由。: "test"
関数コンテキストにおける変数オブジェクト

関数の実行コンテキストに関しては、グローバルコンテキストとは異なり、 VO は直接アクセスされ得ず、アクティベーションオブジェクト( AO と略します)と特別に呼ばれる役割を果たします。

VO(functionContext) === AO;

アクティベーションオブジェクトは、関数のコンテキストに進入する際に生成され、 arguments プロパティとともに初期化されます。 arguments プロパティの値は、 Arguments オブジェクト です。

AO = {
  arguments: <ArgO>
};

Arguments オブジェクト はアクティベーションオブジェクトのプロパティです。このオブジェクトは次のプロパティを持ちます。

  • callee ― 現在の関数に対する参照
  • length実際に渡された実引数の数
  • プロパティインデックス( [n] )( 整数、または文字列に変換され得る)―値は、関数の実引数(左から右へ順に)の値です。これらのプロパティインデックスの数は arguments.length で表されます。 arguments オブジェクトのプロパティインデックスの値と、その時の、実際に内容が渡された仮引数の値は共有されます。(訳注:アクティベーションオブジェクトの直接のプロパティである関数の仮引数( x 、 y 、 z )と、アクティベーションオブジェクトのプロパティである arguments オブジェクトの数値インデックスによるプロパティ( arguments[0], arguments[1], arguments[2] )が、場合によって値を共有します。少し不思議な仕組みです)

例:

function foo(x, y, z) {
 
  // 定義された仮引数( x 、 y 、 z )の数
  alert(foo.length); // 3
 
  // 実際に渡された実引数( x 、 y のみ)の数
  alert(arguments.length); // 2
 
  // 関数自体への参照
  alert(arguments.callee === foo); // true
 
  // プロパティインデックス( arguments[n] )と、共有される引数値
  alert(x === arguments[0]); // true
  alert(x); // 10
 
  arguments[0] = 20;
  alert(x); // 20
 
  x = 30;
  alert(arguments[0]); // 30
 
  // しかし、仮引数 z には値が渡されていないため、
  // arguments オブジェクトの対応する数値のインデックスプロパティとは、
  // 値が共有されない。
  z = 40;
  alert(arguments[2]); // undefined
 
  arguments[2] = 50;
  alert(z); // 40
 
}
 
foo(10, 20);

最後のケースについては、現行バージョンの Google Chrome にはバグがあります。―(値の渡されていない)引数 z と、 arguments[2] の値が共有されてしまうのです。


コンテキストコード処理のフェーズ

さて、この記事の中心ポイントまでやってきました。実行コンテキストコードの処理は、次の2段階に分けられます。

  1. 実行コンテキストへの進入
  2. コード実行

変数オブジェクトへの変更処理は、これら二つのフェーズに密接に関係しています。

これら2段階の処理のフェーズが、一般的なふるまいであり、コンテキストの種類(つまり、グローバル関数コンテキストです)とは独立していることには注意してください。


実行コンテキストへの進入

実行コンテキストに進入するとき(しかし、コード実行のにおいては)、 VO には次のプロパティが(次の順で)積み込まれます(冒頭で説明した通りです)。

  • 関数の各仮引数について(関数コンテキストであれば)
    ―仮引数の名前と実引数に与えられた値をもったプロパティを、変数オブジェクトに生成します。値が渡されなかった仮引数については、仮引数の名前と、 undefined を値にもったプロパティを VO に生成します。
  • 各関数の定義( FunctionDeclarationFD )について
    ―関数の名前と、関数オブジェクトを値に持ったプロパティを、変数オブジェクトに生成します。もし変数オブジェクトが同じ名前のプロパティを持っていた場合、その値と属性を置き換えます。
  • 各変数の定義( varVariableDeclaration)について
    ―変数の名前と、 undefined を値にもったプロパティを、変数オブジェクトに生成します。もしその変数名がすでに定義された関数の仮引数、または関数定義と同名であった場合、既存のプロパティを変更しません

例を見てみましょう。

function test(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
  (function x() {});
}
 
test(10); // 呼び出し

実引数 10 をもって "test" 関数に進入するとき、 AO は次のようになっています。

AO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d: <関数定義 "d" への参照>
  e: undefined
};

AO が、 "x" プロパティを持っていないことに注意してください。これは、 "x" が関数定義ではなく、関数式( FunctionExpression 、 FE と略します)であり、 VO には影響を与えないことによるものです。しかし、関数 "_e" は同様に関数式であるにもかかわらず、後に見るように、変数 "e" に代入されることによって、 "e" という名前によって参照可能となります。関数定義関数式の違いについては、第5章 関数(未訳)にて詳しく触れています。

以上の後に、コンテキストコード処理の第二段階に進みます。―コード実行フェーズです。


コードの実行

この時までに、 AO/VO にはすでにプロパティ(ただし、この段階ではその全てが我々によって渡される実際の値を持っているわけではありません。ほとんどは未だ初期値である undefined を値として持っています)が積み込まれています。

上記の例で考えると、コード実行時の AO/VO への変更は次のようになります。

AO['c'] = 10;
AO['e'] = <関数式 "_e" への参照>;

もう一度、関数式 "_e" が、宣言された変数 "e" に代入されたことによって、メモリ上に存在できていることを確認してください。そして、関数式 "x" は AO/VO に存在しません。すなわち、定義式の前、あるいは後に関数 "x" を呼び出そうとした場合、 "x is not defined" というエラーになるでしょう。保存されない関数式は、その定義式そのものによって、あるいは再帰的にのみ、呼び出すことができます。

もう一つ(古典的な)例を挙げてみましょう。

alert(x); // function
 
var x = 10;
alert(x); // 10
 
x = 20;
 
function x() {};
 
alert(x); // 20

なぜ、最初の "x" への alert 文が function となり、しかも定義より以前に参照可能なのでしょうか?。なぜ10や20ではないのでしょうか?。これはまさに前述した規則に従った結果です。― 関数定義は、コンテキスト進入時に VO に積み込まれます。全く同じフェーズ、コンテキスト進入時には同様に変数定義 "x" があります。しかし、前述したように、文法的に変数定義のステップは、関数及び仮引数定義の後に訪れ、その段階では、同名で定義された既存の関数及び仮引数定義を変更しません。従って、コンテキスト進入フェーズでは、 VO には次の通りプロパティが生成されてゆきます。

VO = {};
 
VO['x'] = <関数定義 "x" への参照>
 
// var x = 10; という文を発見しました。
// もし関数 "x" が定義されていなければ
// "x" の値は undefined となったはずです。
// しかしここでは、変数定義は同名の関数の値を変更しません。
 
VO['x'] = <値は変更されていません。依然として関数への参照となります>

コード実行フェーズでは、 VO は次の通り変更されます。

VO['x'] = 10;
VO['x'] = 20;

二つ目、そして三つ目の alert の出力として見たとおりですね。

次の例では、再度、コンテキスト進入フェーズにて VO に変数が生成されることを見て取れます( else 節は決して実行されないのですが、 VO には変数 "b" が存在するのです)。

if (true) {
  var a = 1;
} else {
  var b = 2;
}
 
alert(a); // 1
alert(b); // undefined 。しかし、 "b is not defined" エラーとは違います。

変数について

JavaScript に関するいくつかの記事、あるいは書籍でさえも、時折「(グローバルコンテキストで) var キーワードを使うか、(コード上のどんなところでも) var キーワードを使わなければ、グローバル変数を定義できる」と述べているのを目にします。これは誤りです。思い出してください。

変数は var キーワードを用いてのみ、定義されます。

そして、このような代入は…、

a = 10;

変数ではなく)新しいプロパティをグローバルオブジェクトに動的に生成しているのです。"変数ではない"とは、それが変更できないという意味ではありません。 ECMAScript における変数の概念において、"変数ではない"のです(そして、グローバルコンテキストの VO は グローバルオブジェクトそのものですから、グローバルオブジェクトのプロパティとなるわけです。覚えていますよね?)。

違いは次のようになります。例を見てみましょう。

alert(a); // undefined
alert(b); // "b" is not defined
 
b = 10;
var a = 20;

ここでまた、 VO とその変更のフェーズに関する問題となります(コンテキスト進入フェーズと、コード実行フェーズです)。

コンテキスト進入フェーズでは、

VO = {
  a: undefined
};

このように、 "b" が存在しません。なぜなら変数ではないからです。 "b" はコード実行フェーズに初めて出現します(しかし私たちの例ではエラーがあるのでそこまで到達しませんでしたね)。

コードを変えてみましょう。

alert(a); // undefined です。おわかりの通り。
 
b = 10;
alert(b); // 10 です。コード実行フェーズで(訳注:グローバルコンテキストの VO 、すなわちグローバルオブジェクトに対し、プロパティとして動的に)生成されました。
 
var a = 20;
alert(a); // 20 です。コード実行フェーズで変更されました。

変数にはもう一つ、重要なポイントがあります。単なるプロパティとは異なり、変数は、 {DontDelete} 属性を持ちます。つまり、 delete 演算子によって変数を削除することができないということを意味します。

a = 10;
alert(window.a); // 10
 
alert(delete a); // true
 
alert(window.a); // undefined
 
var b = 20;
alert(window.b); // 20
 
alert(delete b); // false
 
alert(window.b); // still 20

ただしこの規則が適用されない実行コンテキストがあります。それが、 eval コンテキストです。ここでは、変数に {DontDelete} 属性はセットされません。

eval('var a = 10;');
alert(window.a); // 10
 
alert(delete a); // true
 
alert(window.a); // undefined

これらのサンプルコードを、 Firebug のようなデバッグツールのコンソールでテストしている場合には注意が必要です。 Firebug は、コンソールから入力されたコードの実行に、まさに eval使っているからです。したがって、 var された変数も {DontDelete} 属性を持たず、削除することが可能になっています。


実装系による機能: __parent__ プロパティ

すでに触れたように、仕様上では、アクティベーションオブジェクトに直接アクセスすることはできません。しかし、いくつかの実装、すなわち SpiderMonkey と Rhino においては、関数が特別なプロパティ __parent__ を持ちます。これは、関数が作られたアクティベーションオブジェクト(またはグローバル変数オブジェクト)への参照です。

例( SpiderMonkey 、 Rhino )

var global = this;
var a = 10;
 
function foo() {}
 
alert(foo.__parent__); // global
 
var VO = foo.__parent__;
 
alert(VO.a); // 10
alert(VO === global); // true

この例では関数 foo がグローバルコンテキスト上に作られ、従ってその __parent__ プロパティはグローバルコンテキストの変数オブジェクト、すなわちグローバルオブジェクトにセットされます。

ただし SpiderMonkey では、この方法ではアクティベーションオブジェクト取得できません。バージョンによりますが、内部関数の __parent__null またはグローバルオブジェクトを返すのです。

Rhino ではグローバルオブジェクトと同様の方法でアクティベーションオブジェクトにアクセスできます。

例( Rhino )

var global = this;
var x = 10;
 
(function foo() {
 
  var y = 20;
 
  // "foo" コンテキストのアクティベーションオブジェクト
  var AO = (function () {}).__parent__;
 
  print(AO.y); // 20
 
  // 現在のアクティベーションオブジェクトの __parent__ は、
  // グローバルオブジェクトです。
  // すなわち、変数オブジェクトの特別なチェーンが形成されているわけです。
  // これをスコープチェーンと呼びます。
  print(AO.__parent__ === global); // true
 
  print(AO.__parent__.x); // 10
 
})();

結論

この章では、実行コンテキストにまつわるオブジェクトについてさらに理解を深めました。この内容が、みなさんにとって曖昧だった部分を明確にし、役立ってくれることを期待します。以降の章では、スコープチェーン識別子解決、そしてその結果としてクロージャについて触れてゆきます。

何が疑問があれば、コメントにて喜んでお答えします(訳注:日本語でこの記事にコメントいただければ、わかる限りで訳者も喜んでお答えします)。


参考文献


英語版翻訳: Dmitry A. Soshnikov [英語版].
英語版公開日時: 2010-03-15

オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]
オリジナルロシア語版公開日時: 2009-06-27


詳細 ECMA-262-3 シリーズ目次