詳細 ECMA-262-3 第4章 スコープチェーン
どうもおつかれさまでございます。大形尚弘でございます。先日、 tumblr developer meetup 2011 に参加させていただき、「リブログのこの快感をどのようにサービスに取り入れるか」についてご参加の皆様と熱い議論を交わしたのですが、20時を過ぎた辺りで全員「そろそろ早く帰ってリブログしたいな...」という気持ちになりましたためスッと解散になったのが印象的でした。
ちなみに、「たんぽぽグループ」は実在いたします。みなさんの心の中に...。
というわけで、 Dmitry 先生の ECMA-262-3 シリーズもいよいよ中盤、スコープチェーンについてご一緒に勉強させてください。
詳細 ECMA-262-3 シリーズ
詳細 ECMA-262-3 第4章 スコープチェーン
目次
はじめに
変数オブジェクトについての第2章で既に触れたように、実行コンテキストのデータ(変数、関数定義、関数の仮引数)は、変数オブジェクトのプロパティとして保管されます。
また、変数オブジェクトはコンテキスト進入時に生成及び初期値の代入が行われ、コード実行フェーズにて変更が発生します。
この章は、実行コンテキストに直接に関係するさらにもう一つのトピックに充てられます。今回は、スコープチェーンについて検討します。
定義
簡潔に、要点だけ説明するならば、スコープチェーンとは主に内部関数に関係するものです。
ご存じの通り、 ECMAScript は内部関数の生成を許可しており、それらの関数を親の関数から戻すことさえできます。
var x = 10; function foo() { var y = 20; function bar() { alert(x + y); } return bar; } foo()(); // 30
そしてまた、すべてのコンテキストは各々の変数オブジェクトを持ちます。グローバルコンテキストにはグローバルオブジェクト自身が、関数コンテキストにはアクティベーションオブジェクトが対応して存在します。
つまりスコープチェーンとは、まさに内部関数にとってのすべての(親の)変数オブジェクトのリストに他なりません。このチェーンが、変数の探索に用いられるものです。すなわち上の例では、 "bar" コンテキストのスコープチェーンは、 AO(bar) 、 AO(foo) 、 VO(global) を含んだリストなのです。
しかしこれをもう少し詳しく見てみましょう。
始めにまず定義を与え、その後に例を元に検討します。
スコープチェーンとは、実行コンテキストに関係して、識別子解決時の変数探索に用いられる変数オブジェクトのチェーンです。
関数コンテキストのスコープチェーンは関数呼び出し時に生成され、アクティベーションオブジェクトと関数の内部 Scope プロパティで構成されます。関数の Scope プロパティについてはこの後に詳しく検討します。
実行コンテキストから図式的に見れば、
activeExecutionContext = { VO: {...}, // あるいは AO this: thisValue, Scope: [ // スコープチェーン // 識別子探索のための、 // すべての変数オブジェクトのリスト ] };
この時、 Scope の定義は次のようになります。
Scope = AO + Scope
ここで例として、 Scope 及び Scope を通常の ECMAScript 配列として表すことができます。
var Scope = [VO1, VO2, ..., VOn]; // スコープチェーン
この構造の別な見方としては、チェーン中のすべてのオブジェクトにおいて親スコープへの(親変数オブジェクトへの)参照を持った、階層的なオブジェクトのチェーンが考えられます。
この見方は、変数オブジェクトに関数第2章で検討した一部の実装における __parent__ の考え方に対応します。
var VO1 = {__parent__: null, ... other data}; --> var VO2 = {__parent__: VO1, ... other data}; --> // などなど
しかし、配列を用いてスコープチェーンを表現する方が簡便ですので、ここではこのアプローチを採ることにします。実装のレベルにおいては、 __parent__ 機能を伴った階層型チェーンのアプローチが採られ得るわけですが、一方仕様では、抽象的には「スコープチェーンはオブジェクトのリストである」と示しています。配列を用いた抽象的な表現は、リストというコンセプトを説明するために都合の良い方法です。
これから検討していく AO と Scope の組み合わせ、及び識別子解決のプロセスは、関数のライフサイクルに結びついています。
関数のライフサイクル
関数のライフサイクルは、生成、そしてアクティベーション(呼び出し)の2ステージに分かれます。詳しく見ていきましょう。
関数の生成
ご存じの通り、関数定義はコンテキスト進入時に変数オブジェクトまたはアクティベーションオブジェクト( VO/AO )に積み込まれます。グローバルコンテキスト上の変数と関数定義の例を挙げてみましょう(グローバルコンテキストではグローバルオブジェクトが変数オブジェクトでした。覚えていますよね?)。
var x = 10; function foo() { var y = 20; alert(x + y); } foo(); // 30
関数がアクティベーションされる段階に至れば、正しい(そして期待された)結果、30が返ってきます。しかしここには一つある重要な特徴があります。
ここまで、私たちは現在のコンテキストの変数オブジェクトについてのみ話してきました。つまり変数 "y" は、関数 "foo" の中で定義されていることは見て取れます( "foo" コンテキストの AO 中に、ということです)。しかし変数 "x" は、コンテキスト "foo" では定義されておらず、従って "foo" の AO にも積み込まれていません。一見すると、関数 "foo" にとって、変数 "x" は全く存在しないのです。後に見るように、これは一見しただけの結果です。しかしともかく、 "foo" のアクティベーションオブジェクトには、ただ一つのプロパティ、 "y" のみしか含まれていないのです。
fooContext.AO = { y: undefined // コンテキスト進入時:undefined 、アクティベーション時:20 };
それでは、関数 "foo" はどのようにして変数 "x" にアクセスするのでしょうか?。関数は、より上のコンテキストの変数オブジェクトにアクセスすることができる、そう考えるのが理に適っています。これは事実上、その通りです。実際には、この仕組みは関数の内部 Scope プロパティとして実装されています。
Scope は、現在の関数コンテキストより上の、すべての親変数オブジェクトの階層的なチェーンであり、関数の生成時に、関数に対して保存されます。
ここは重要なポイントです。 Scope は関数生成時に、静的に、常に、そして関数が破壊されるまでの間ずっと、保存されるのです。つまり、その関数が決して呼び出されなくとも、 Scope プロパティはその関数オブジェクトに必ず書き込まれ、保存されているのです。
もう一つ気に留めておくべきポイントは、 Scope (スコープチェーン)がコンテキストのプロパティであるのと異なり、 Scope は関数のプロパティであるということです。上に挙げた例では、関数 "foo" の Scope は次のようになります。
foo.Scope = [ globalContext.VO // === グローバルオブジェクト ];
こうした後に、ご存じ関数呼び出しによって関数コンテキストへの進入が始まり、アクティベーションオブジェクトが作られ、 this 値と Scope (スコープチェーン)が決定されます。それでは次に、この段階について検討しましょう。
関数のアクティベーション
定義の項で触れたとおり、コンテキストに進入し AO/VO が生成された後、コンテキストの Scope プロパティ(変数探索にとってのスコープチェーンに当たるもの)が下記の通り定義されます。
Scope = AO|VO + Scope
注目は、アクティベーションオブジェクトが Scope 配列の先頭であるということです。すなわち、スコープチェーンの先頭に追加されるのです。
Scope = [AO].concat(Scope);
この特性は識別子解決のプロセスを考える上でとても重要です。
識別子解決とは、その変数(あるいは関数定義)が、スコープチェーン中のどの変数オブジェクトに属するのかを、決定するプロセスです。
このアルゴリズムからの戻り値には、常に Reference 型の値が取られます。この値の base 要素は対応する変数オブジェクト(あるいは変数が見つからなければ null )、プロパティ名要素は探索された(解決された)識別子です。 Reference 型の詳細については、第3章 this で詳しく考察しています。
識別子解決のプロセスには、変数の名前に対応するプロパティの探索が含まれます。つまり、スコープチェーン中の変数オブジェクトに対し、最も深いコンテキストから、スコープチェーンの最上位まで、連続的に検査が行われると言うことです。
従って、探索プロセスにおいては、あるコンテキストのローカル変数が、親コンテキストの変数よりも高い優先度となります。もし二つの変数が同じ名前を持ち、しかし異なるコンテキストにあった場合、より深いコンテキストの変数が最初に発見されるのです。
前述の例をもう少し複雑にして、内部段を追加してみましょう。
var x = 10; function foo() { var y = 20; function bar() { var z = 30; alert(x + y + z); } bar(); } foo(); // 60
ここでは、各々の変数/アクティベーションオブジェクト、関数の Scope プロパティ、コンテキストのスコープチェーンは、次の通りになります。
グローバルコンテキストの変数オブジェクトは、
globalContext.VO === Global = { x: 10 foo: <関数への参照> };
関数 "foo" の生成時、 "foo" の Scope プロパティは、
foo.Scope = [ globalContext.VO ];
関数 "foo" のアクティベーション(コンテキスト進入)時、コンテキスト "foo" のアクティベーションオブジェクトは、
fooContext.AO = { y: 20, bar: <関数への参照> };
そしてコンテキスト "foo" のスコープチェーンは、
fooContext.Scope = fooContext.AO + foo.Scope // つまり fooContext.Scope = [ fooContext.AO, globalContext.VO ];
内部関数 "bar" の生成時、その Scope プロパティは、
bar.Scope = [ fooContext.AO, globalContext.VO ];
"bar" のアクティベーション時、コンテキスト "bar" のアクティベーションオブジェクトは、
barContext.AO = { z: 30 };
そしてコンテキスト "bar" のスコープチェーンは、
barContext.Scope = barContext.AO + bar.Scope // つまり barContext.Scope = [ barContext.AO, fooContext.AO, globalContext.VO ];
名前 "x" 、 "y" 、 "z" の識別子解決プロセスはそれぞれ...、
- "x" -- barContext.AO // 無し -- fooContext.AO // 無し -- globalContext.VO // 発見 - 10 - "y" -- barContext.AO // 無し -- fooContext.AO // 発見 - 20 - "z" -- barContext.AO // 発見 - 30
スコープの特徴
それでは次に、スコープチェーン及び関数の Scope プロパティに関係した、いくつかの重要な特徴について検討していきましょう。
クロージャ
ECMAScript におけるクロージャは、関数の Scope プロパティに直接関係します。ご説明したとおり、 Scope は関数生成時に保存され、関数オブジェクトが破壊されるまで存在し続けます。実際のところ、 クロージャとは、全くもって関数コードと Scope プロパティの組み合わせのことなのです。だからこそ、 Scope は関数が生成されたレキシカル環境(親の変数オブジェクト)を含んでいます。その後の関数アクティベーション時において、より上位のコンテキストにおける変数はこのレキシカルな(生成時に静的に保存された)変数オブジェクトのチェーン中から探索されます。
例です。
var x = 10; function foo() { alert(x); } (function () { var x = 20; foo(); // 10 、 20 ではなく })();
変数 "x" が、関数 "foo" の Scope に見つかることが分かります。つまりは、変数探索において、関数呼び出し時に作成される動的なチェーンではなく(その場合変数 "x" は 20 と解決されたでしょうが)、関数生成時に定義されたレキシカルな(閉じ込められた)チェーンが用いられているのです。
クロージャの別の古典的な例を見てみましょう。
function foo() { var x = 10; var y = 20; return function () { alert([x, y]); }; } var x = 30; var bar = foo(); // 無名関数が戻されてきます bar(); // [10, 20]
ここでも、識別子解決には関数生成時に定義されたレキシカルなスコープチェーンが用いられていることが分かります。変数 "x" は、 30 ではなく 10 と解決されます。さらにこの例は、関数(この場合関数 "foo" から戻された無名関数)の Scope が、関数が定義されたコンテキストが終了してしまった後も依然として存在し続けるということを、はっきりと示しています。
クロージャの理論と、その ECMAScript における実装については、第6章 クロージャを参照してください。
関数コンストラクタから生成された関数の [[Scope]] プロパティ
これまでの例を通じて、関数はその生成時に Scope プロパティを取り、このプロパティを経由して全ての親コンテキストの変数にアクセスする様子を見てきました。しかしながら、このルールには一つ重要な例外が存在します。関数が Function コンストラクタで生成された場合です。
var x = 10; function foo() { var y = 20; function barFD() { // 関数定義 alert(x); alert(y); } var barFE = function () { // 関数式 alert(x); alert(y); }; var barFn = Function('alert(x); alert(y);'); barFD(); // 10, 20 barFE(); // 10, 20 barFn(); // 10, "y" は定義されていない } foo();
ご覧の通り、 Function コンストラクタによって生成された関数 "barFn" からは、変数 "y" はアクセスできません。しかし、だからといって関数 "barFn" が内部 Scope プロパティを持たないという訳ではありません(だとすれば変数 "x" にもアクセスできないはずです)。ここで重要なのは、 Function コンストラクタを通じて生成された関数の Scope プロパティには、常にグローバルオブジェクトのみが含まれているということです。例えばこう考えてみてください、なぜならこのような関数によって、グローバルを除いた上位コンテキストのクロージャを生成することは、不可能だからです(訳注:関数の生成時とアクティベーション時が、 Function コンストラクタを使用した関数定義の場合どのようになるか、検討してみてください)。
二次元のスコープチェーン探索
もう一つ、スコープチェーン中の探索で重要なポイントは、変数オブジェクトのプロトタイプ(もし存在すれば)もまた、考慮に入れられるということです。 ECMAScript のプロトタイプ的な性質から、もしプロパティがそのオブジェクト中に見つからなければ、探索はプロトタイプチェーンに進んでゆくからです。これはある種、チェーンの二次元探索であると言えます。 (1) まずスコープチェーン探索が一コマ進められ、 (2) 次にそのスコープチェーン中の一コマ毎に、プロトタイプチェーンのコマの中へと探索が進みます。 Object.prototype にプロパティを定義してみると、この動作を実際に観察できます。
function foo() { alert(x); } Object.prototype.x = 10; foo(); // 10
ただし以下の例の通り、アクティベーションオブジェクトはプロトタイプを持ちません。
function foo() { var x = 20; function bar() { alert(x); } bar(); } Object.prototype.x = 10; foo(); // 20
もし関数コンテキスト "bar" のアクティベーションオブジェクトがプロトタイプを持っていたとしたら、プロパティ "x" は Object.prototype にて解決されていたはずです。なぜなら、 コンテキスト "bar" の AO においては直接に解決されていないからです。しかし前者の例では、識別子解決のスコープチェーン探索が、「 Object.prototype を継承するグローバルオブジェクト」に行き着くことで(すべての実装がそうではありませんが)、 "x" は 10 と解決されるのです。
同様の例は、名前付き関数式( Named Function Expressions : NFE と略します)に関して、 SpiderMonkey のいくつかのバージョンで見ることができます。関数式の任意名を保存する特別なオブジェクトがあり、それが Object.prototype を継承しているのです。また、 BlackBerry 実装のいくつかのバージョンにおいては、アクティベーションオブジェクトが Object.prototype を継承してしまっています。しかしこれらの特徴について、より詳しくは第5章 関数にて検討することにしましょう。
グローバル及び eval コンテキストのスコープチェーン
これは驚くべきことではありませんが、お伝えしておかなければならないでしょう。グローバルコンテキストのスコープチェーンは、グローバルオブジェクトのみを含みます。 "eval" タイプのコードのコンテキストでは、呼び出し元コンテキストと同じスコープチェーンになります。
globalContext.Scope = [ Global ]; evalContext.Scope === callingContext.Scope;
コード実行中にスコープチェーンを変更する
ECMAScript には、コード実行フェーズでランタイムにスコープチェーンを変更できる二つの文が存在します。 with 文と、 catch 節です。これらは両方とも、スコープチェーンの先頭に、これらの文の中に現れる識別子を探索するために必要なオブジェクトを追加します。つまり、これが起こる場合にはスコープチェーンは図式的には次の通りとなります。
Scope = withObject|catchObject + AO|VO + Scope
この例の with 文は、その引数となるオブジェクトをスコープチェーンの先頭に加えています(そうすることで、このオブジェクトのプロパティがプリフィックス無しでアクセスできているのです)。
var foo = {x: 10, y: 20}; with (foo) { alert(x); // 10 alert(y); // 20 }
スコープチェーンの変更を表してみると、
Scope = foo + AO|VO + Scope
もう一度、 with 文によってスコープチェーンの先頭に追加されたオブジェクトの中で識別子が解決される例をご紹介します。
var x = 10, y = 10; with ({x: 20}) { var x = 30, y = 30; alert(x); // 30 alert(y); // 30 } alert(x); // 10 alert(y); // 30
ここで何が起こったのでしょうか?。まず、コンテキスト進入フェーズにおいて、 "x" および "y" 識別子が変数オブジェクトに積み込まれました。さらにランタイムコード実行フェーズで、次のような変更が行われていきます。
- x = 10, y = 10;
- オブジェクト {x: 20} がスコープチェーンの先頭に追加される。
- with の内部の var 文が現れるが、ここで新たに生成されるものはない。なぜなら全ての変数はコンテキスト進入ステージで既にパースされ、追加されているからである。
- "x" 値の変更のみが行われる。 "x" は今まさに第2ステップにてスコープチェーンの先頭に追加されたオブジェクト中に解決される。 "x" の値は 20 だったが、ここで 30 となる。
- さらに "y" 値の変更が行われる。これは上の変数オブジェクトに解決されるので、 10 だったものが、 30 となる。
- さらに with 文終了後、特別なオブジェクトがスコープチェーンから削除される(変更された値 "x" 、 30 も、このオブジェクトと同時に削除される)。すなわち、スコープチェーンの構造は with 文による拡張が行われる以前の状態に回復される。
- 最後の二つの alert に見るように、現在の変数オブジェクトにおける "x" の値は同じまま残り、 "y" は with 文中で変更された 30 となる。
さらに、 catch 節もまた、引数である例外にアクセスするために、中間スコープオブジェクトを生成します。このオブジェクトの唯一のプロパティは例外の引数としての名前で、スコープチェーンの先頭に配置されます。図式的には、
try { ... } catch (ex) { alert(ex); }
スコープチェーンの変更は、
var catchObject = { ex: <例外オブジェクト> }; Scope = catchObject + AO|VO + Scope
catch 節の働きが終了した後、スコープチェーンは以前の状態に回復されます。
結論
この章まで、実行コンテキストにまつわる全ての一般的な概念を検討してきました。続いて、予定では関数オブジェクトの詳細な分析と、関数の種類(関数定義、関数式)、それにクロージャへと進みます。ともあれ、クロージャはこの章で検討した Scope プロパティに直接関係しています。しかし、詳しくは専用の章に任せるとしましょう。コメントでみなさんの質問にお答えできることを楽しみにしています(訳注:例によって後日訳者の個人ブログにも訳文をアップいたしますので、そちらで喜んで質問にお答えいたします)。
参考文献
- 8.6.2 - [[Scope]](邦訳)
- 10.1.4 - Scope Chain and Identifier Resolution(邦訳:スコープ連鎖と識別子の解決 (Scope Chain and Identifier Resolution))
英語版翻訳: Dmitry A. Soshnikov[英語版].
英語版公開日時: 2010-03-21
オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]
オリジナルロシア語版公開日時: 2009-07-01
本シリーズはすべて英語版からの訳出です。