このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

プログラマのためのJavaScript (8):オブジェクト生成の仕組み

前回、__proto__チェーンとそれを使ったプロパティ検索の話をしました。オブジェクトの__proto__プロパティは、(このプロパティが公開されているなら)プログラマが勝手にいじることもできます。ですが、人為的操作を受けない自然状態の__proto__はどのように設定されているのでしょうか。自然状態とはつまり、生まれたままの状態です。といういことは、オブジェクト誕生の瞬間に立ち会えば事情がわかるはずです。

そういうわけで今回は、オブジェクト生成と、生成時に__proto__が設定される仕掛けを調べます。

今回の内容:

  1. コンストラクタとは何なのか
  2. 関数オブジェクトのprototypeオブジェクト
  3. オブジェクトはこうして生まれる
  4. オブジェクトは生みの親を憶えているか
  5. 今回のまとめ

●コンストラクタとは何なのか

JavaScriptのオブジェクトは、var d = new Date(); のように、new演算子とコンストラクタで生成されます。オブジェクトによってはリテラル表記も使えますが、それは処理系が生成と初期化をやってくれているのです。例えば var point = {x:20, y:30};は次のコードと同等です。


var point = new Object();
point.x = 20;
point.y = 30;

さてところで、そもそもコンストラクタって何でしょう。JavaScriptのコンストラクタは、実際のところ単なる関数です。しかも、「コンストラクタ」という言葉は関数の種別を表しているのではなくて、関数の使用法の種別を表しているのです。次の例を見てください。


js> function sum(x, y) {
return (x + y);
}
js> var x = new sum(5, 3)
js>

関数sumをコンストラクとして使ってますが、特に問題はありません(その意義・効果はワケわからんけど)。

次の例は、コンストラクタと思える関数をnewなしで呼び出す例です。


js> function Point(x, y) {
this.x = x;
this.y = y;
}
js> Point(20, 30)
js> x
20
js> y
30
js>

普通に関数Pointを呼び出すと、thisとして大域オブジェクトがセットされるので、大域変数x, yに値が代入されました(いいんか?それで)。

●関数オブジェクトのprototypeオブジェクト

JavaScriptの関数はオブジェクトです(詳細は第6回)。よって、プロパティを持つことができます。どんな関数でも持っているプロパティのひとつに、prototypeプロパティがあります。確認してみましょう。


js> typeof sum
function
js> typeof sum.prototype
object
js> typeof Point
function
js> typeof Point.prototype
object
js> typeof Date
function
js> typeof Date.prototype
object
js>

関数のprototypeプロパティが指すオブジェクトを、その関数のprototypeオブジェクトと呼びましょう。関数もオブジェクトなので、前回説明した__proto__プロパティ、したがって__proto__オブジェクトを持ちます。しかし、prototypeオブジェクトと__proto__オブジェクトは別物です。絶対に混同しないように!

●オブジェクトはこうして生まれる

newと関数を組み合わせた形式 new SomeFunc(mayHaveArguments); でオブジェクトが生まれるのですが、そのとき、次の手順に従います。

  1. 処理系が、プレーンなオブジェクトをヒープにアロケートする。
  2. コンストラクタ(として使われる)関数の呼び出し環境(Callオブジェクト、スタックフレームに相当)のthisとして、今アロケートしたオブジェクトをセットする。
  3. コンストラクタ関数のコードが実行される。
  4. thisに入っていたオブジェクトが返される。

大筋はこういうことですね。おっと、大事なことが抜けている。そう、__proto__プロパティのセットです。

  • 生成したオブジェクトの__proto__プロパティとして、そのコンストラクタ関数のprototypeオブジェクトをセットする。

このことから、オブジェクト生成直後の__proto__オブジェクトは、コンストラクのprototypeオブジェクトと一致することになります。確認しましょう。


js> var pt = new Point(20, 30)
js> pt.__proto__ === Point.prototype
true
js>

ここで「===」は、ほんとに同一のオブジェクトかどうかを調べる等号です。

なお、DateとかDocumentのような、言語処理系/ホスト環境が提供するオブジェクトでは、特殊な生成手続きが実行されるかもしれません、念のため。

●オブジェクトは生みの親を憶えているか

クラスを持つプログラミング言語では、オブジェクトの所属クラスは、出生証明や履歴書の役割を果たします(これに関するより詳しい話は「JS番外編:進化的オブジェクトについて考える 」)。JavaScriptでも、出生証明に近いものはあります。オブジェクトのconstructorプロパティです。

はい、さっそく確認(ただし、関数オブジェクトのnameプロパティは環境によりないときもあるので注意。)


js> var d = new Date()
js> d.constructor.name
Date
js> var pt = new Point(20, 30)
js> pt.constructor.name
Point
js>

この事実から、オブジェクト生成に次のステップも存在すると思った人もいるでしょう。

  • 生成したオブジェクトのconstructorプロパティとして、コンストラクタ関数自身をセットする。

実は違うのです。それを調べるため次の関数を定義します。


function hasProperProp(a, p) {
var result;
var orig = a.__proto__; // いったん待避
a.__proto__ = null; // __proto__チェーンを断ち切る
result = (typeof a[p] != 'undefined'); // 調べる
a.__proto__ = orig; // もとに戻す
return result;
}

この関数により、そのオブジェクトにホントに含まれるプロパティと__proto__チェーンをたどって検索されるプロパティを区別できます。


js> d.constructor.name
Date
js> hasProperProp(d, "constructor")
false
js> pt.constructor.name
Point
js> hasProperProp(pt, "constructor")
false
js>

だいたい見当が付くと思いますが、constructorプロパティは、オブジェクトの__proto__オブジェクトに記録されています。確認してみます。


js> hasProperProp(d.__proto__, "constructor")
true
js> hasProperProp(pt.__proto__, "constructor")
true
js>

“オブジェクトの__proto__オブジェクト”は、“コンストラクタのprototypeオブジェクト”ですから、コンストラクタとそのprototypeオブジェクトは相互参照していることになりますね。

  • コンストラクタ -(prototypeプロパティ)→ prototypeオブジェクト
  • prototypeオブジェクト -(constructorプロパティ)→ コンストラク

●今回のまとめ

絵を描いてみました(色鉛筆で)-- 今日の日記内にあります。関数オブジェクトは四角、それ以外のオブジェクトは丸です。矢印はプロパティによる参照関係で、波矢印は生成を意味します。この絵が今回のエッセンスです。

  1. JavaScriptのコンストラクタは単なる関数であり、newと共に使われるとき「コンストラクタ」と呼ばれるだけのこと。
  2. 関数オブジェクトには、prototypeという名前のプロパティが存在する。これは、すべてのオブジェクトが備えている__proto__プロパティとは別である。
  3. オブジェクト生成の過程で、コンストラクタ関数のprototype(が指す)オブジェクトが、生成されたオブジェクトの__proto__プロパティの値としてセットされる。
  4. オブジェクトの生みの親であるコンストラクは、constructorプロパティで知ることができる。だがこれは、オブジェクトの__proto__オブジェクトに記録されている情報である。

次回は、__proto__とprototypeのメカニズムにより、通常のクラスや継承をシミュレートする話をするつもりです。