前々回(第7回)、前回(第8回)で、次のことを説明しました。
- すべてのオブジェクトは、__proto__チェーン(長さ0かも知れない)を持つ。
- オブジェクトのプロパティ検索は、__proto__チェーンに沿って実行される。
- すべての関数オブジェクトは、prototypeオブジェクト(prototypeプロパティの値)を持つ。
- 関数がコンストラクタとして使われたとき、その関数のprototypeオブジェクトが、生成されたオブジェクトの__proto__オブジェクトとしてセットされる。
今回は、これらの事実を組み合わせて、クラス概念をシミュレートしてみます。
今回の内容:
- サンプルはカウンタ
- 兄弟は同じ気質を共有する
- コンストラクタとトレイツでクラスをシミュレート
- サンプルについてもっと説明
- 今回のまとめ
●サンプルはカウンタ
まずサンプルを挙げておきます。有界カウンタ(bounded counter)です。説明の都合から多少作為的な仕様ですが:
なお、以下で「省略可能な引数名のオシリにアンダスコアを付ける」のは僕の個人的なコーディングルールです。
Counter.UPPER_BOUND = 10;
Counter.MAX_COUNTERS = 3;Counter.counters = 0;
function Counter(initValue_) {
if (Counter.counters >= Counter.MAX_COUNTERS) {
throw new Error("too many counters.");
}
if (typeof initValue_ == 'undefined') {
this.value = 0;
} else {
if (0 <= initValue_ && initValue_ < Counter.UPPER_BOUND) {
this.value = initValue_;
} else {
throw new Error("Illegal initial value:" + initValue_);
}
}
Counter.counters++;
}Counter.prototype.inc = function() {
if (this.value + 1 >= Counter.UPPER_BOUND) {
throw new Error("counter overflow.");
}
this.value++;
};
Counter.prototype.dec = function() {
if (this.value <= 0) {
throw new Error("counter underflow.");
}
this.value--;
};
このカウンタに対応するJavaのコードを次のエントリに載せておきます。必要なら参照してください。
●兄弟は同じ気質を共有する
今日の日記内の説明図を見てください。この絵は、「オブジェクト生成の仕組み」の絵を少し変えたものです。絵にある3つのオブジェクト(赤い丸)は同じコンストラクタから生成されています。つまり、兄弟ですね。兄弟達は、同一の__proto__オブジェクトを共有し、それはコンストラクタのprototypeオブジェクトです(このことが納得できないなら、前回と前々回を読み直してください)。
「同一の__proto__オブジェクトを共有する」とはつまり、同一の__proto__チェーン(ただし自分自身を除いた残りのチェーン)を共有することです。JavaScriptのプロパティ検索メカニズムから、兄弟達は、自分自身に固有なプロパティ以外は同じプロパティを持つことになります。
複数のオブジェクトで共有される__proto__オブジェクトをトレイツオブジェクト(traits object)と呼ぶことがあります。トレイツとは気質のことです。家族・親族や同じ地域に住む人々は、なにかしら似た性格、価値観、行動様式を持つでしょう。その“気質”にあたるものを1個のオブジェクトに詰め込んでいる、と考えるのです。
__proto__オブジェクトをどう使おうと勝手ですが、多くの場合、それはトレイツオブジェクトとして使われます。__proto__オブジェクトに、デフォルト値やメソッド(関数を指すプロパティ)をセットしておき、複数のオブジェクトに共有させます。結果として、同じ__proto__オブジェクトを持つオブジェクト群は、似たような特徴、能力、役割、傾向性を持つことになります。
●コンストラクタとトレイツでクラスをシミュレート
第7回「プロトタイプ継承の正体」でも強調したように、プロトタイプオブジェクトという言葉は混乱を招くし、適切な用語とも思えないので、僕はトレイツオブジェクトという概念/用語を使うことにします。
JavaScriptではクラスがサポートされてませんが、コンストラクタ関数と、その関数のprototypeプロパティの値であるトレイツオブジェクトにより、クラスをシミュレート可能です。標語としては、
- コンストラクタ関数 + トレイツオブジェクト ≒ クラス
です。
コンストラクタ関数(newと共に使われた関数)が、“インスタンス”の生成と初期化を行い、インスタンスに紐<ひも>付いたトレイツオブジェクト(コンストラクタのprototypeオブジェクト=インスタンスの__proto__オブジェクト)が、兄弟達に共通の特徴と振る舞い(これがトレイツ)を提供することになります。
実際にクラスをシミュレートするコーディングは色々な流儀がありますが、典型的なコーディング・パターンは次のようなものです。
/* PseudClassがクラスもどきの名前 */function PseudClass {
// ここにインスタンス初期化コード
}PseudClass.prototype.method_1 = function(mayHaveArguments) {
// ここにmethod_1のコード
};PseudClass.prototype.method_2 = function(mayHaveArguments) {
// ここにmethod_2のコード
};/* 以下同様 */
●サンプルについてもっと説明
有界カウンタの例は、上記のコーディング・パターンに従って書いてあります。このサンプルを心底納得するには、いくつかの点に注意する必要があります。
- Counterは関数ですが、関数はオブジェクトなので(「関数オブジェクトの秘密」を参照)、UPPER_BOUND、MAX_COUNTERS、countersのようなプロパティを持てる。UPPER_BOUND、MAX_COUNTERSは定数のつもり、countersはクラス変数(Javaならクラス内static変数)のつもり。
- Counterを定義する前に、Counterのプロパティ初期化が出てくるが問題にはならない。その理由は、処理系がcompile-and-go方式だから(「コンパイル単位」を参照)。Counter関数自体の生成はコンパイル時に行われ、Counter.UPPER_BOUNDなどのセットは実行時に行われる。
function(mayHavaArguments) { ... }
は関数リテラルで、関数オブジェクトを表現する。関数リテラルから関数オブジェクトを作るのはコンパイル時。作られた関数オブジェクトを、トレイツオブジェクトであるCounter.prototypeにセットする代入文は実行時に遂行される。- コンパイルとトップレベルコードの実行が済めば、メモリ内にクラスをシミュレートするオブジェクト構造(プロパティで関連しあうオブジェクト達のグラフ、「オブジェクト構造」を参照)が出来上がる。
●今回のまとめ
- 同じコンストラクタから生成された兄弟オブジェクト達は、__proto__オブジェクトを、したがって__proto__チェーンを共有する。
- 共有される__proto__オブジェクトは、複数のオブジェクト達に共通の“気質”を持たせるために使われる。
- “気質”を表現するオブジェクトをトレイツオブジェクトと呼ぶ。JavaScriptの__proto__オブジェクトの役割は(ほとんどの場合)トレイツオブジェクトである。
- コンストラクタ関数とトレイツオブジェクト(コンストラクタのprototypeオブジェクト=インスタンスの__proto__オブジェクト)で、通常のクラス概念をシミュレートできる。
今回は、継承のシミュレーションまで話が及ばなかったので、これは次回にします。JavaScript 2.0(ECMAScript第4版)のclassの仕様に触れるかもしれません。