空想犬猫記

※当日記では、犬も猫も空想も扱っておりません。(旧・エト記)

JavaScript Object Model

このブログでも数回触れたことがある JavaScript について,もう少し調べてみる。入門編として「まずは動かしてみる」という段階を踏まえ,最近まで open source の JavaScript を「読んでみる」というという段階を経てきた。prototype.js を使うとなんだかそれっぽくきれいに書けて楽しいのだけど,this の振る舞いがいまいち分からんし,やっぱり JavaScript を理解せずに使っている感が否めず,気持ち悪い。そんなわけで今日は JavaScript のオブジェクトモデルに付いて調べてみた。このあたりを探っていてたどり着いた中では Mozilla のドキュメントが良く出来ている。対応する Java のコードも併記されていて分かり易い。

これ読んで,本当に良かった。最後の方は未消化なんだけど,ともかく JavaScript の inheritance が __proto__ オブジェクトを介したプロトタイプ・チェインで実現されているということが分かって,今までの色んな謎が氷解。

  • prototype は function object_name() {...} で定義した JavaScript オブジェクトにおまけで付いてくる
  • new object_name() すると,new メソッドは,新しいインスタンス(左辺値)の __proto__ に prototype をセットする。
  • その後,インスタンスそのものにも,prototype のメンバをコピーする。
  • object_name() そのものが実行される。

って感じ。コンストラクタ内でメンバを上書きしてしまっても,__proto__ メンバを介して,基底クラスのメンバにアクセスすることができる。JavaScript では,コンストラクタとクラスの定義が同一なので,継承を定義した瞬間に,基底クラスは初期化されてしまっている。C++Java のように,導出クラスから基底クラスを初期化するためには,call や apply メソッドを使って値を上書きする。って理解で OK かな?

なんか理解が進んだ感があるぞ。

#追記
  • その後,インスタンスそのものにも,prototype のメンバをコピーする。

というのは間違い。for (var i in x){ ... } 文で,あたかもコピーされているかのようにアクセスできるけど,上書きしない限りは prototype の変数そのものを探しに行って参照しているようだ。したがって

function A(){}
A.prototype.prot_a = 1;

function B(){ A.call(this); }
B.prototype = new A;

var instB1 = new B;
var instB2 = new B;

instB1.__proto__.__proto__.prot_a = 10;

print(instB2.prot_a);           // => 10
print(instB2.__proto__.prot_a); // => 10

とか,さらに

function A(){}
A.prototype.prot_a = 1;

function B(){ A.call(this); }
B.prototype = new A;

var instB1 = new B;
var instB2 = new B;

instB1.__proto__.prot_a = 5;
instB1.__proto__.__proto__.prot_a = 10;

print(instB2.prot_a);                     // => 5
print(instB2.__proto__.prot_a);           // => 5
print(instB2.__proto__.__proto__.prot_a); // => 10
print(A.prototype.prot_a);                // => 10

となる。むぅ…。上書きするまでは単なる参照*1だけど,上書きした時点で,そのオブジェクトに直接ぶら下がるオブジェクトとしてそのメンバが作成され,以降はそのオブジェクトが返されるというわけか・・・。アクセスが非対称なんだねぇ・・・。

色々考えていたら,prototype を使った継承もどき(委譲)のメリットがよく分からなくなってきた。たとえば

function A(){ this.a = 1; }
function B(){ A.call(this); this.b = 2; }
var instB = new B;

こー書くのと

function A(){ this.a = 1; }
function B(){ A.call(this); this.b = 2; }
B.prototype = new A;
var instB = new B;

こー書くのとでは,instB.__proto__ オブジェクトが作られるか否かだけの差しかない,実用的には

  1. 基底クラスオブジェクトで,共通の変数を保持する
  2. 初期化し忘れの際,デフォルトの値を提供する

くらいのメリットしかなさそうなんですが。1. のような使い方をせず,かつ初期化をしっかりやっているぶんには,prototype はいらないんだろうか。むぅ…。

さらに追記

あ,そっか。メンバ関数なんか,まさに 1. のケースに相当するわけか。そうしないとインスタンス毎に仮装関数テーブルが必要になってしまうから…。うむ,納得。逆にインスタンス毎に上書きしてしまうような変数などは,prototype に書いても大したメリットはないと(デメリットもそんなにないけど)。。。ふむふむ。

*1:同名のメンバが見つかるまで __proto__ のたらい回しチェインを辿り続ける。どこかで「JavaScript は継承ではなく委譲によるオブジェクト指向」というフレーズを聞いたことがあるけど,このことなのか。