詳細 ECMA-262-3 第3章 this
どうもおつかれさまでございます。たんぽぽグループの大形尚弘でございます。好きな言語は Dylan です。好きな声優は五十嵐裕美さんです。
さて、週刊のはずが月刊になってしまった、 Dmitry 先生の ECMA-262-3 シリーズの第3章をお送りします。文中、未だ訳出の終わっていないスコープチェーンや関数の章への参照がありますが、特にスコープチェーンにおいてこの時点である程度理解しておきたいとお感じになる方もいらっしゃるかと思います。その辺りは、以前私個人のブログで翻訳・公開させいただいたコア・JavaScript ( JavaScript. The Core. )でも簡単に触れられておりますので、適宜ご参照ください。
また、本章とは全然関係ないのですが、先日 JavaScript Advent Calendar 2011 (オレ標準コース)に参加させていただき、 ECMAScript におけるオブジェクト指向について、また次期バージョンの ES.next におけるオブジェクト指向周辺の新しいシンタックスについてご紹介させていただきましたので、ぜひご印刷の上、おトイレの時間つぶしなどにご利用いただければ幸いです。
それでは本章、 JavaScript / ECMAScript で最も面白いところの一つ、「 this 」についてご高覧ください。
詳細 ECMA-262-3 シリーズ
詳細 ECMA-262-3 第3章 this
目次
はじめに
この章では、実行コンテキストに直接に関係するもうひとつのテーマについて考察します。議題は this
キーワードです。
ご承知の通りこのトピックはとても難しく、時に異なる実行コンテキストにおける this
値の決定について議論の的になるポイントです。
多くのプログラマにとって、プログラム言語における this
キーワードとは、オブジェクト指向プログラミングに密接に結びつき、コンストラクタによって新たに生成されるオブジェクトまさにそれ自体を参照するものとして考えられてきました。 ECMAScript でもその考え方は実装されているものの、これから見ていくように、ここでは生成されるオブジェクトのみには限らないのです。
それでは、 ECMAScript の this
値について、詳しく見てゆきましょう。
定義
this
値は、実行コンテキストのプロパティです。
activeExecutionContext = { VO: {...}, this: thisValue };
ここで VO とは、前の章でお話しした変数オブジェクトです。
this
は、実行可能コードの種類に直接に関係するものです。その値はコンテキスト進入時に決定され、コードがコンテキスト中を実行している間は不変です。
各ケースについて、さらに詳しく見てゆきましょう。
グローバルコード上の this 値
ここでは話は単純です。グローバルコード上では、 this
値は常にグローバルオブジェクトそのものです。従って間接的な参照が可能です。
// グローバルオブジェクトへの // 明示的なプロパティ定義 this.a = 10; // global.a = 10 alert(a); // 10 // 制約されていない識別子への代入による、 // 暗黙のプロパティ定義 b = 20; alert(this.b); // 20 // 変数定義によるこれも暗黙的な例。 // グローバルコンテキストの変数オブジェクトは、 // グローバルオブジェクトそのものである。 var c = 30; alert(this.c); // 30
関数コード上の this 値
関数コード内で this
が用いられるとき、話はもっと興味深くなります。このケースは最も複雑で、さまざまな議論の的になってきました。
この種類のコードにおける、最初の(そしておそらくは、一番重要な) this
値の機能は、関数には静的に束縛されないということです。
前述したように、 this
値はコンテキスト進入時に決定されます。つまり、関数コードの場合においては、その値は都度完全に異なる値を取りうるのです。
しかしながら、コードの実行時には、 this
値は不変です。すなわち、新しい値を代入することはできません。なぜなら、 this は変数ではないからです。(対照的に、プログラミング言語 Python では明確に定義された self
オブジェクトというものがあり、これはランタイムに変更可能です。)
var foo = {x: 10}; var bar = { x: 20, test: function () { alert(this === bar); // true alert(this.x); // 20 this = foo; // エラー。 this 値を変更することはできない。 alert(this.x); // 上のエラーが無かったとすれば、20ではなく、10になっただろう。 } }; // コンテキスト進入時に // this 値は "bar" オブジェクトに決定される。 // 理由は後ほど詳しく説明する。 bar.test(); // true, 20 foo.test = bar.test; // しかし、同じ関数を呼び出しているのだが、 // ここでは this 値は "foo" を参照する。 foo.test(); // false, 10
関数コードにおいて、 this
値が参照する先には一体何が影響しているのでしょうか?。これにはいくつかの要因があります。
まず、通常の関数呼び出しでは、 this
はそのコンテキストのコードをアクティベートする呼び出し元によって与えられます。つまりそれは、関数を呼び出す親コンテキストです。 this
値は呼び出し式の形によって決定されます(言い換えれば、構文的に見て関数がどのように呼び出されたかという形式です)。
どんなコンテキストでも問題なく this
値を決定できるようになるためには、これは心に留めておくべきとても重要なポイントです。まさに、呼び出し式の形、すなわち関数を呼び出す方法こそが呼び出されたコンテキストにおける this
値を決定します。それ以外にはあり得ません。
( JavaScript に関するいくつかの記事や、時に書籍でさえ、「 this 値は関数がどのように定義されているかで決まる。もしグローバル関数であれば this 値はグローバルオブジェクトに、もし関数があるオブジェクトのメソッドであれば常にそのオブジェクトにセットされる」と主張していますが、これは完全に誤りです)。一歩踏み込んでみれば、一般的なグローバル関数でさえ、異なる形式の呼び出し式によってアクティベートされ得、異なる this
値を取り得るのです。
function foo() { alert(this); } foo(); // グローバル alert(foo === foo.prototype.constructor); // true // しかし、同じ関数であっても // 異なる呼び出し式の形式によっては、 // this 値もまた異なる。 foo.prototype.constructor(); // foo.prototype
同様に、あるオブジェクトのメソッドとして定義された関数を、 this
値がそのオブジェクトにセットされない形で呼び出すことも可能です。
var foo = { bar: function () { alert(this); alert(this === foo); } }; foo.bar(); // foo, true var exampleFunc = foo.bar; alert(exampleFunc === foo.bar); // true // もう一度、同じ関数であっても、 // 異なる呼び出し式の形によって、 // 異なる this 値を取ります。 exampleFunc(); // global, false
それでは、この呼び出し式の形式が、どのように this
値に影響するのでしょうか?。 this
値の決定について十分に理解するには、ある内部型、 Reference
型について詳しく考察する必要があります。
Reference 型
擬似コードを用いれば、 Reference
型の値は二つのプロパティを持ったオブジェクトとして表せます。 base(プロパティが属するオブジェクト)と、その base における propertyName です。
var valueOfReferenceType = { base:, propertyName: <プロパティ名> };
Reference
型の値が現れるのは、次の二つの場合です。
- 識別子を取り扱うとき
- あるいは、プロパティアクセサを取り扱うとき
識別子解決の手順については、第4章 スコープチェーンにて詳しく検討します。ここでは、このアルゴリズムからの戻り値は、必ず Reference
型の値( this
値の決定にとって重要な)になると理解しておいてください。
識別子とは、変数名、関数名、関数の仮引数名、グローバルオブジェクトの非制約プロパティの名前です。例えば、下記の識別子の値に対し...
var foo = 10; function bar() {}
演算の中間結果では、対応する Reference
型の値は次の通りです。
var fooReference = { base: global, propertyName: 'foo' }; var barReference = { base: global, propertyName: 'bar' };
Reference
型の値からオブジェクトの実際の値を取得するには GetValue
というメソッドがあり、擬似コードでは次のように表せます。
function GetValue(value) { if (Type(value) != Reference) { return value; } var base = GetBase(value); if (base === null) { throw new ReferenceError; } return base.Get(GetPropertyName(value)); }
内部 [[Get]]
メソッドは、プロトタイプチェーンから継承されるプロパティの解析も含め、オブジェクトのプロパティの実の値を取り出します。
GetValue(fooReference); // 10 GetValue(barReference); // function object "bar"
みなさんご存じの通り、プロパティへのアクセス式(プロパティアクセサ)には二つのバリエーションがあります。 ドット記法(プロパティ名が正しい識別子であり、予め分かっている場合)と 括弧記法です。
foo.bar(); foo['bar']();
演算の途上の戻り値は、ここでも Reference
型の値を持ちます。
var fooBarReference = { base: foo, propertyName: 'bar' }; GetValue(fooBarReference); // function object "bar"
さてそれでは、最も重要な意味において、関数コンテキストの this
値と Reference
型の値は、どのように関係するのでしょうか?。この点が、この章の主題です。関数コンテキストにおける this
値決定の原則は、次のようになります。
関数コンテキストにおけるthis
値は、呼び出し元によって与えられ、呼び出し式の形式(関数呼び出しが構文的にどのように書かれているかということ)によって決定される。
呼び出し括弧 ( ... ) の左辺がReference
型の値を取る場合は、this
値はReference
型の値の base オブジェクトにセットされる。
その他すべての場合においては(すなわち Reference 型と区別されるその他すべての値を取る場合)、this
値は常に null となる。しかし、this
値が null となることは意味を成さないため、暗黙にグローバルオブジェクトに変換される。
例を見てみましょう。
function foo() { return this; } foo(); // グローバルオブジェクト
呼び出し括弧の左辺には Reference
型の値が取られていることが分かります(なぜなら foo は識別子だからです)。
var fooReference = { base: global, propertyName: 'foo' };
このようにして、 this
値は、この Reference
型の値の base オブジェクトにセットされます。この場合、グローバルオブジェクトです。
同様にプロパティアクセサの場合も...
var foo = { bar: function () { return this; } }; foo.bar(); // foo
ここでも、 base が foo
オブジェクトである Reference
型の値を取りますので、 bar
関数のアクティベーション時の this
値には foo オブジェクトがセットされます。
var fooBarReference = { base: foo, propertyName: 'bar' };
しかし、全く同じ関数を違う形式の呼び出し式でアクティベートすると、異なる this
値を取ることになります。
var test = foo.bar; test(); // グローバルオブジェクト
なぜならば、識別子である test が、異なる Reference
型の値を取るためです。ここではグローバルオブジェクトたる base が、 this
値として用いられます。
var testReference = { base: global, propertyName: 'test' };
補足: ES5 の strict mode(未訳) では、this
値はグローバルオブジェクトに強制変更されません。その代わり、undefined
がセットされます。
このようにして、なぜ異なる形式の呼び出し式でアクティベートされた同じ関数が異なる this
値を取るのか、という理由を正確に説明することができます。中間計算値である Reference
型の値が、異なる値を取るため、ということがその答えです。
function foo() { alert(this); } foo(); // グローバルオブジェクト。なぜなら、 var fooReference = { base: global, propertyName: 'foo' }; alert(foo === foo.prototype.constructor); // true // 異なる形式の呼び出し式 foo.prototype.constructor(); // foo.prototype 。なぜなら、 var fooPrototypeConstructorReference = { base: foo.prototype, propertyName: 'constructor' };
もう一つ、呼び出し式の形による this
値の動的な決定の、別の古典的な例を挙げておきます。
function foo() { alert(this.bar); } var x = {bar: 10}; var y = {bar: 20}; x.test = foo; y.test = foo; x.test(); // 10 y.test(); // 20
関数呼び出しと非 Reference 型
ところで、既に述べたように、呼び出し括弧の左辺が Reference
型ではなくその他すべての 型の値を取る場合には、 this
値は自動的に null にセットされ、結果として、グローバルオブジェクトになります。
そうした式の例を見てみましょう。
(function () { alert(this); // null => グローバルオブジェクト })();
この場合、呼び出し括弧の左辺は関数オブジェクトであり、 Reference
型のオブジェクトではありません(識別子でも無ければ、プロパティアクセサでもありません)。従って、結果として this
値はグローバルオブジェクトにセットされます。
もう少し複雑な例を見てみましょう。
var foo = { bar: function () { alert(this); } }; foo.bar(); // Reference, OK => foo (foo.bar)(); // Reference, OK => foo (foo.bar = foo.bar)(); // グローバル? (false || foo.bar)(); // グローバル? (foo.bar, foo.bar)(); // グローバル?
さて、中間計算値が Reference
型の値になるべきプロパティアクセサでありながら、なぜ、ある呼び出しにおいては base オブジェクト(すなわち foo )ではなくグローバルオブジェクトを this
値に取るのでしょうか?
問題となるのは最後の三つです。ある演算を行った後、呼び出し括弧の左辺の値が、 Reference
型の値ではなくなっているのです。
一番最初の例では、すべては明白です。そこには間違いなく Reference
型が存在し、その結果として、 this
値はその base オブジェクト、つまり foo になります。
二番目の例は、グループ演算子です。しかしこのグループ演算子は、既に見たような Reference
型の値からオブジェクトの実値を取得するメソッド、すなわち GetValue を呼び出しません(仕様 11.1.6の注釈(邦訳)を参照してください)。従って、グループ演算子の評価の戻り値は、依然として Reference
型の値であるため、 this
値は再び base オブジェクトである、 foo にセットされます。
三番目のケースは、代入演算子です。代入演算子はグループ演算子と異なり、 GetValue メソッドを呼び出します(仕様 11.13.1 のステップ3(邦訳)を参照してください)。結果として、この代入演算子の戻り値は Reference
型の値ではなく関数オブジェクトになるため、 this
値は null にセットされ、結果として、グローバルオブジェクトになるのです。
四番目、五番目のケースも同様です。カンマ演算子、論理和式は GetValue メソッドを呼び出します。従って、 Reference
型の値を失い、関数 型の値を得ます。そして再び、 this
値はグローバルオブジェクトになります。
Reference 型と null の this 値
ある呼び出し式が、呼び出し括弧の左辺を Reference
型の値と決定しながら、 this
値が null 、結果としてグローバルオブジェクトになる場合があります。これは、 Reference
型の値の base オブジェクトが、アクティベーションオブジェクトである場合に関係しています。
こうした場面は、内部関数が親から呼び出された場合の例に見ることができます。第2章で学んだように、ローカル変数、内部関数、及び仮引数は与えられた関数のアクティベーションオブジェクトに保管されます。
function foo() { function bar() { alert(this); // グローバルオブジェクト } bar(); // AO.bar() に等しい }
アクティベーションオブジェクトは、 this
値として常に null を返します(擬似コード AO.bar()
は、 null.bar()
に等しくなります)。ここでまた再び前述した仕組みに基づいて、 this
値はグローバルオブジェクトにセットされます。
with
オブジェクトが関数名のプロパティを持つとき、 with
文のブロック内部で関数が呼び出された場合に、例外があり得ます。 with
文は、独自の with
オブジェクトをスコープチェーンの先頭に付け加えます。つまり、アクティベーションオブジェクトの前にです。よって、(識別子またはプロパティアクセサに) Reference
型の値を持ちますので、 base オブジェクトはアクティベーションオブジェクトではなく、 with
文のオブジェクトになるのです。ちなみに、この件は内部関数だけでなく、グローバル関数にも影響する問題です。なぜなら with
オブジェクトは、スコープチェーン中のより高位なオブジェクト(グローバルオブジェクトやアクティベーションオブジェクト)を隠してしまうからです。
var x = 10; with ({ foo: function () { alert(this.x); }, x: 20 }) { foo(); // 20 } // なぜなら var fooReference = { base: __withObject, propertyName: 'foo' };
同様の場面は、 catch
節の実引数である関数を呼び出す際にも起こりえます。この場合は、 catch
オブジェクトがスコープチェーンの先頭に追加されます。つまり with
の場合と同様に、アクティベーションオブジェクトやグローバルオブジェクトの手前に、です。しかし、この挙動は ECMA-262-3 のバグであると認識されており、標準の新しいバージョン ECMA-262-5 では、このような関数のアクティベーションでは、 this
値は catch
オブジェクトではなく、グローバルオブジェクトにセットされます。
try { throw function () { alert(this); }; } catch (e) { e(); // __catchObject / ES3 、 (修正されて正しくは)グローバルオブジェクト / ES5 } // 考え方としては var eReference = { base: __catchObject, propertyName: 'e' }; // しかしこれはバグであるため、 // this 値は global に強制される。 // null => global var eReference = { base: global, propertyName: 'e' };
さらに、名前付き関数式(関数についての詳細は第5章 関数を参照ください)を再帰的に呼び出す際も同様です。関数の最初の呼び出し時には、 base オブジェクトは親アクティベーションオブジェクト(またはグローバルオブジェクト)になるわけですが、再帰呼び出し時には、 base オブジェクトが関数式の任意な名前を保管する、特別なオブジェクトとなるのです。ただし、このケースでは、 this
値は常にグローバルオブジェクトにセットされるようになっています。
(function foo(bar) { alert(this); !bar && foo(1); // (再帰呼び出し)特別なオブジェクトである"べき"だが、常にグローバルオブジェクトとなる })(); // (1回目:通常の呼び出し)グローバルオブジェクト
コンストラクタとして呼び出された関数での this 値
関数コンテキストの this
値にまつわるケースがもう一つあります。関数をコンストラクタとして呼び出した場合です。
function A() { alert(this); // 下で新しく作られるオブジェクト - "a" オブジェクト this.x = 10; } var a = new A(); alert(a.x); // 10
この場合、 new 演算子(邦訳)が関数 A
の内部 [[Construct]] (邦訳)メソッドを呼び出し、続いて、オブジェクト生成後に、まったく同じ関数 A
の内部 [[Call]] (邦訳)メソッドを呼び出し、 this
値として新しく生成されたオブジェクトを渡します。
関数呼び出し時に手動で this を指定する
Function.prototype
(すなわちすべての関数からアクセス可能)には二つのメソッドがあり、これらを用いれば、関数呼び出し時の this
値を手動で指定することができます。 .apply
と .call
メソッドです。
これらは共に、呼び出されるコンテキストで使われる this
値を第一引数として受け取ります。差異はあまり重要ではありません。 .apply
は必ず配列(または配列様オブジェクト、例えば arguments
等)を第二引数として受け取り、 .call
はどんな引数でも受け取ります。この二つのメソッドに共通して必須なのは、第一引数、 this
値です。
例です。
var b = 10; function a(c) { alert(this.b); alert(c); } a(20); // this === グローバルオブジェクト, this.b == 10, c == 20 a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30 a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40
結論
この章では、 ECMAScript における this
キーワードの機能について考察しました(そしてそれらは、 C++ や Java のものとは対照的なものでした)。この章が、 ECMAScript における this
キーワードの働きをより正確に理解する手助けになることを期待します。そしていつものように、みなさんの質問にはコメントにて喜んでお答えします(訳注:このブログではコメントをいただけないのですが、日本語で拙ブログにコメントいただければ、わかる限りで訳者も喜んでお答えします)。
参考文献
- 10.1.7 - This(邦訳:this)
- 11.1.1 - The this keyword(邦訳:this キーワード)
- 11.2.2 - The new operator(邦訳:new 演算子)
- 11.2.3 - Function calls(邦訳:関数呼出し)
英語版翻訳: Dmitry A. Soshnikov 、 Stoyan Stefanov の助力を借りて [英語版].
英語版公開日時: 2010-03-07
オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]
補足と訂正: Zeroglif
オリジナルロシア語版公開日時: 2009-06-28; 更新:2010-03-07
本シリーズはすべて英語版からの訳出です。