詳細 ECMA-262-3 シリーズ・第1章 実行コンテキスト/第2章 変数オブジェクト
初めましてこんにちは。たんぽぽグループの大形尚弘と申します。好きな言語は Dart です。どうぞよろしくお願いします。
さてもう昨年のことになりますが、私個人のブログにて、 Dmitry A. Soshnikov さんの JavaScript. The Core. という記事を翻訳させていただきましたところ、予想以上の反響をいただきました。 JavaScript の実装部分、例えば今なら HTML5 の色とりどりな API といったキラキラした部分だけでなく、 ECMAScript の仕様そのものに興味のある方が、こんなにいたなんて!
と、いうわけで、日本では、先日上梓されました『パーフェクト JavaScript 』でのみ触れられているような、 ECMAScript の言語仕様そのものについて、同じく Dmitry さんが書かれた ECMAScript3 および 5 に関する詳細記事シリーズを翻訳し、この場を借りてみなさまと共有させていただく許可をご本人からいただきましたので、是非ご一緒に、 ECMAScript への理解を深めていけたらと思います。興味のある方が今もいらっしゃったら嬉しいのですが。
まずは、第1章、第2章を翻訳させていただきました。以後、1週間に1度くらいのペースで追加していきますので、どうぞよろしくお願いします。詳細 ECMA-262-3 シリーズ
詳細 ECMA-262-3 第1章 実行コンテキスト
目次
はじめに
この章では、 ECMAScript の実行コンテキストと、それに関連する実行可能コードの種類について説明します。定義
コントロールが ECMAScript の実行可能コードに移るとき、コントロールは必ず実行コンテキストに入ります。実行コンテキスト(以下 EC と略します)は、 ECMA-263 の仕様上で、実行可能コードを特徴毎に分類し、区別するために用いられる抽象的なコンセプトです。仕様では、技術的な実装の観点から EC の種類や構造を正確に定義することはしていません。詳細はこの仕様を実装する ECMAScript エンジンに預けられています。 論理的には、アクティブな実行コンテキストの集合はスタックを形成します。スタックの最下は常にグローバルコンテキストであり、最上が現状の(アクティブな)実行コンテキストとなります。このスタックは、さまざまな種類の EC に出入りする際に変更( push/pop )されてゆきます。
実行可能コードの種類
抽象的なコンセプトである実行コンテキストは、それぞれ実行可能コードの種類が対応します。つまりコードの種類(グローバルコードなのか、関数コードなのか、 eval コードなのか)について話すとき、それは対応する実行コンテキストを意味しているとも言えます。 例えば、実行コンテキストのスタックを配列として定義してみましょう。ECStack = [];
グローバルコード
この種類のコードは、 "Program" というレベルとして処理されます。すなわち、読み込まれた外部 .js ファイルや、ローカルのインラインコード( <script></script> タグの)です。(訳注: "Program" を初めとした ECMAScript の構文上の構成要素に関しては、拙記事もご参照ください)。グローバルコードには、グローバルコード上の関数定義の本文に当たるコードは一切含まれません。 初期化時(プログラム開始時)には、 EC スタックは次のようになっているわけです。ECStack = [ グローバルコンテキスト ];
関数コード
関数コードに入るとき(全ての種類の関数において)、 EC スタックには新しい要素が push されます。その際、対象となる関数のコードとしては、内部関数のコードは決して含まれないことに注意してください。例えば、自己を再帰的に一度だけ呼び出す関数を例に取ってみましょう。(function foo(bar) { if (bar) { return; } foo(true); })();この時、 EC スタックは以下の通り変更されてゆきます。
// 最初に foo という関数コンテキストをアクティベートします ECStack = [ <foo> 関数コンテキスト グローバルコンテキスト ]; // 再帰的にもう一度 foo 関数コンテキストをアクティベートします ECStack = [ <foo> 関数コンテキスト - 再帰的な <foo> 関数コンテキスト グローバルコンテキスト ];
eval コード
eval コードに関してはより興味深い手続きがとられます。呼び出し元コンテキスト、つまり eval 関数が実行された、実行される元となったコンテキストが関わってくるのです。 eval によって実行されたアクション(例えば変数や関数定義など)は、呼び出し元のコンテキストに影響を与えることになります。eval('var x = 10'); (function foo() { eval('var y = 20'); })(); alert(x); // 10 alert(y); // "y" は定義されていませんEC スタックの変更の様子です。
ECStack = [ グローバルコンテキスト ]; // eval('var x = 10'); ECStack.push( eval コンテキスト, 呼び出し元コンテキスト : グローバルコンテキスト ); // eval がコンテキストを抜けます ECStack.pop(); // foo 関数呼び出し ECStack.push(<foo> 関数コンテキスト); // eval('var y = 20'); ECStack.push( eval コンテキスト, 呼び出し元コンテキスト: <foo> 関数コンテキスト ); // eval から戻ります ECStack.pop(); // foo から戻ります ECStack.pop();
どうですか。とても一般的で、理にかなったコールスタックですよね。
バージョン 1.7 までの SpiderMonkey による実装( Firefox や Thunderbird に導入)では、呼び出し元コンテキストを eval 関数の第2引数として渡すことが可能です。従って、そのコンテキストが存在する限り、 "プライベート" (とでも呼べるようなもの)変数に作用させることもできます。function foo() { var x = 1; return function () { alert(x); }; }; var bar = foo(); bar(); // 1 eval('x = 2', bar); // コンテキストを渡し、内部変数 "x" に作用します bar(); // 2
結論
この短い理論は、今後それぞれの章で取り扱われる、実行コンテキストにまつわる様々な詳細(例えば変数オブジェクトや、スコープチェーン等)を理解していくために必要となります。参考文献
ECMA-262-3 仕様の対応する章 ― 10. Execution Contexts (TAKIさんによる邦訳:10 実行コンテキスト (Execution Contexts))
英語版翻訳: Dmitry A. Soshnikov [英語版].
英語版公開日時: 2010-03-11
オリジナルロシア語版著者: Dmitry A. Soshnikov [ロシア語版]
オリジナルロシア語版公開日時: 2009-06-26
詳細 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 と略します)は、実行コンテキストに関連する特別なオブジェクトであり、そのコンテキスト中で宣言された以下の内容を保管します。定義上は、例として、変数オブジェクトを通常の ECMAScript オブジェクトとして表現することが可能です。
- 変数(var 、 VariableDeclaration /変数定義)
- 関数の定義( FunctionDeclaration 、以下 FD)
- 関数の仮引数
VO = {};
activeExecutionContext = { VO: { // コンテキストのデータ(var 、 FD 、 関数の引数) } };
私たちが変数または関数を定義するということは、与えた名前と、変数の値をもった新しいプロパティを、 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 === this === global) ? ???> 関数コンテキストの変数オブジェクト (VO === AO, <arguments> オブジェクト、 <仮引数> が追加される)これを詳しく見てみましょう。
グローバルコンテキストにおける変数オブジェクト
ここでまず先に、グローバルオブジェクトに定義を与えましょう。グローバルオブジェクトは、どんな実行コンテキストに進入するよりも前に生成されるオブジェクトです。このオブジェクトはただ一つだけ存在し、そのプロパティはプログラムのどの箇所からも参照でき、グローバルオブジェクトのライフサイクルはプログラムとともに終了します。グローバルオブジェクトは、例えば、 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> };
- 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段階に分けられます。- 実行コンテキストへの進入
- コード実行
実行コンテキストへの進入
実行コンテキストに進入するとき(しかし、コード実行の前においては)、 VO には次のプロパティが(次の順で)積み込まれます(冒頭で説明した通りです)。- 関数の各仮引数について(関数コンテキストであれば) ―仮引数の名前と実引数に与えられた値をもったプロパティを、変数オブジェクトに生成します。値が渡されなかった仮引数については、仮引数の名前と、 undefined を値にもったプロパティを VO に生成します。
- 各関数の定義( FunctionDeclaration 、 FD )について ―関数の名前と、関数オブジェクトを値に持ったプロパティを、変数オブジェクトに生成します。もし変数オブジェクトが同じ名前のプロパティを持っていた場合、その値と属性を置き換えます。
- 各変数の定義( var 、 VariableDeclaration)について ―変数の名前と、 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/VO にはすでにプロパティ(ただし、この段階ではその全てが我々によって渡される実際の値を持っているわけではありません。ほとんどは未だ初期値である undefined を値として持っています)が積み込まれています。 上記の例で考えると、コード実行時の AO/VO への変更は次のようになります。AO['c'] = 10; AO['e'] = <関数式 "_e" への参照>;
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['x'] = 10; VO['x'] = 20;
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 };
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); // 依然として 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 })();
結論
この章では、実行コンテキストにまつわるオブジェクトについてさらに理解を深めました。この内容が、みなさんにとって曖昧だった部分を明確にし、役立ってくれることを期待します。以降の章では、スコープチェーン、識別子解決、そしてその結果としてクロージャについて触れてゆきます。何が疑問があれば、コメントにて喜んでお答えします(訳注:このブログではコメントをいただけないのですが、日本語で拙ブログにコメントいただければ、わかる限りで訳者も喜んでお答えします)。
参考文献
- 10.1.3 - Variable Instantiation (TAKIさんによる邦訳:10.1.3 変数の実体化 (Variable Instantiation))
- 10.1.5 - Global Object (TAKIさんによる邦訳:10.1.5 Global オブジェクト (Global Object))
- 10.1.6 - Activation Object (TAKIさんによる邦訳:10.1.6 Activation オブジェクト (Activation Object))
- 10.1.8 - Arguments Object (TAKIさんによる邦訳:10.1.8 arguments オブジェクト (Arguments Object))
オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]
オリジナルロシア語版公開日時: 2009-06-27