new演算子の働き
前回の記事では、プロトタイプチェーンを理解するために、プロパティの"参照"のタイミングで何が起こっているのかを解説した。それでは、プロパティを"作成・変更"する際には何が起こるのだろうか。
連載第3回で述べた通り、JavaScriptでは(リテラル値を除けば)すべてのデータはオブジェクトである。つまり、プロパティの値も(リテラル値を除けば)オブジェクトとして定義される。プロパティを定義(作成)するときの動きを理解するために、まずは、new演算子の働きをきちんと確認しておきたい。
new演算子の働きは、これまでたびたび紹介してきたCore JavaScript 1.5 Referenceを見ても理解し難いかもしれない。
Core JavaScript 1.5 Referenceで解説されているのは、new演算子の働きというよりは使いかたの部分だからだ。もう少し理解を深めるために、JavaScriptのベースとなっているECMAScript(エクマスクリプト)の言語仕様を読み込んでみよう。JavaScript 1.7に対応するECMAScriptのバージョンは1999年12月に公開されたStandard ECMA-262だ。Ecma internationalのWebページから仕様書(PDF)がダウンロードできる。
ECMA-262のドキュメントを取得してきたら、目次をざっと眺め、ビューアの検索機能などを用いて"new Operator"という文字列を検索してみよう。11.2.2節に、次の解説がなされていることがわかる。
11.2.2 The new Operator
The production NewExpression : new NewExpression is evaluated as follows:
1. Evaluate NewExpression.
2. Call GetValue(Result(1)).
3. If Type(Result(2)) is not Object, throw a TypeError exception.
4. If Result(2) does not implement the internal [[Construct]] method, throw a TypeError exception.
5. Call the [[Construct]] method on Result(2), providing no arguments (that is, an empty list of arguments).
6. Return Result(5).
ここで解説されている通り、インタプリタはnew演算子を発見すると、対象がオブジェクトであるか(数値や文字列などのリテラル値で無いか)を判別し、続いて内部 [[Construct]] メソッドをコールしようとすることがわかる。
それでは、続けて、[[Construct]] メソッドについても見ていこう。13.2.2節に、次の解説がなされている。
13.2.2 [[Construct]]
When the [[Construct]] property for a Function object F is called, the following steps are taken:
1. Create a new native ECMAScript object.
2. Set the [[Class]] property of Result(1) to "Object".
3. Get the value of the prototype property of the F.
4. If Result(3) is an object, set the [[Prototype]] property of Result(1) to Result(3).
5. If Result(3) is not an object, set the [[Prototype]] property of Result(1) to the original Object prototype object as described in 15.2.3.1.
6. Invoke the [[Call]] property of F, providing Result(1) as the this value and providing the argument list passed into [[Construct]] as the argument values.
7. If Type(Result(6)) is Object then return Result(6).
8. Return Result(1).
ここでは、(var F = function() { ... } で定義されるような)関数オブジェクト F を例に、new F() した際の動きが詳細に解説されている。ここの解説でも、他のページを参照すべきキーワードが多々登場しているが、端的に訳すと、ここでの処理は次の通りだ(次の項番は、必ずしも上記のそれと一致しないので注意)。([[Class]], [[Prototype]], [[Call]] についても同様に仕様を確認しておこう。)
- 新しくネイティブのECMAScriptオブジェクトを生成する
- Fオブジェクトのプロトタイププロパティ(F.prototype)を取得する
- これがオブジェクトであれば(数値や文字列などのリテラル値でなければ)、これを 1 で生成したオブジェクトの[[prototype]]プロパティにセットする(JavaScriptの実装では __proto__ プロパティとなる)
- オブジェクトでなければ、(最上位に位置する)Objectオブジェクトのプロトタイププロパティを [[prototype]]プロパティにセットする
- 引数として、newされたときの引数ををそのまま設定し、F() (コンストラクタ)をコールする。このコンストラクタ内部では、1で生成したオブジェクトをthisとして扱う
- この返り値がオブジェクトであれば、これを"new"の結果オブジェクトとして返す
- オブジェクトでなければ 1 で生成したオブジェクトを"new"の結果オブジェクトとして返す
以上から、JavaScriptでオブジェクトをnewした際は、まず、プロトタイプチェーンを辿りprototypeプロパティをセットし、その上でコンストラクタがコールされることがわかる。
しかし、少しトリッキーなのは、
- コンストラクタ内でプロトタイプを適用したオブジェクトが生成されていること(クラスベースで言うスーパークラスのコンストラクタが(引数の有無にかかわらず)暗黙で呼び出されている)(上記2)
- コンストラクタの返り値がオブジェクトでなければ、プロトタイプを適用したオブジェクトがそのまま返される(上記3.2)
という点だ。「コンストラクタの返り値」とは、馴染みにくいかもしれないが、連載第4回で述べた通り、コンストラクタは単なる関数オブジェクトである。関数オブジェクトにnew演算子を施すと、(関数オブジェクトがコンストラクタとして機能し、)上記のような処理がなされる、と考えれば、より理解しやすいだろう。
プロパティの作成・変更
さて、以上でnew演算子の働きが確認できた。今回の主旨である、プロパティの "作成・変更" に解説を戻そう。ここまでくれば、プロトタイプチェーンの仕組みもよく理解できるはずだ。
プロパティの "作成・変更" では、明示的に prototype を上書きしようとしない限り、そのオブジェクト自身のプロパティが作成・変更される。new演算子の働きや、プロパティの定義方法をよく確認しながら、前回実行したコマンド(後半部分)を振り返ってみよう。
// ダックスフントオブジェクト"Dachshund"のプロトタイプを
// 犬オブジェクトとする
>>> var Dachshund = function() { this.name='サラ'; }
// Dog(これ自体はprototypeプロパティにMammalオブジェクトを持つ)をnewする
>>> Dachshund.prototype = new Dog()
Object name=noname
// Dachshund(これ自体はprototypeプロパティにDogオブジェクトを持つ)をnewする
>>> var dh = new Dachshund()
// コンストラクタにより、prototypeを適用して生成されたオブジェクトのnameが書き換えられている
>>> dh.name
"サラ"
// ダックスフントオブジェクトのプロトタイプを上書き
>>> Dachshund.prototype.name = 'ポチ'
"ポチ"
>>> dh.name
"サラ"
// ↑ dhは8行目で既にnewされているため、プロトタイプの変更は影響しない
>>> dh.bark()
// ↑ プロトタイププロパティ(__proto__)が参照される = 'ばうわう' と表示
// dhオブジェクト自身のbarkプロパティを作成
>>> dh.bark = function() { alert('くぅーん'); }
function()
>>> dh.bark()
// ↑'くぅーん' と表示
上記では、前回の解説よりも、コメントを詳細に記述してある。それぞれのオブジェクトがnewされるタイミングで何が起こっており、Dachshund.prototype.nameの上書きやdh.barkの定義が、どのように動作しているか理解できただろうか。
以上、プロトタイプチェーンによる継承メカニズムを紹介した。プロトタイプベースオブジェクト指向では、クラスベースでいうオーバーライドと似たような仕組みで継承が利用できる。しかし、プロパティを逐次作成・変更・削除できるという点からも、クラスベースと比較し、よりダイナミックなオブジェクト指向プログラミングが可能になっている事がわかるだろう。
一方、プロトタイプチェーンは便利な仕組みだが、場当たり的なプロパティ操作を行っていると、たちまちオブジェクトが肥大化・複雑化してしまう。どのようにコーディングしていくのが、より実践的なのだろうか。次回以降は、世に公開されているさまざまなJavaScriptライブラリのソースコードを読みながら、基礎的な部分も押さえつつ、さらに実践的なトピックについて解説していこう。