3.2 コンストラクタとプロトタイプ
前節ではオブジェクトを作成して使う方法について述べました。あるオブジェクトについてこのような立場をとる人間、オブジェクトをクライアントといいます。クライアントは「顧客」とか「依頼人」という意味です。つまりオブジェクトから見れば「お客」なのです。それ故オブジェクトはクライアントを中心に振舞わなければなりませんし、クライアントとの契約が守れないときはそれなりの対処をしなければいけません (「2. 例外」を参照)
この節で解説するのはオブジェクトを定義する方法です。JavaScript についてよく知らない多言語プログラマは「JavaScript はオブジェクトのクライアントとしてはまあまあだが、クラスを定義できないから駄目だ」と言うかもしれません。確かに JavaScript にはクラスを定義するための構文が存在しません。しかしそれは言語が貧弱だからではなく、JavaScript がクラスベースではなくプロトタイプベースの設計を選んだからです (JavaScript 2.0 ではクラスが使えますが)。プロトタイプ理論に従えば「オブジェクトを継承」させることも可能なのです。クライアントの前で仕事をするのはクラスではなくオブジェクトであることを忘れないで下さい
コンストラクタの定義
new
演算子を使ってコンストラクタ (とクライアントが考えているもの) を呼び出そうとするとコンストラクタの内部メソッド[[Construct]]が呼び出されます。このメソッドが呼び出される以下のように処理が行われます (ECMAScript 3rd Edition 13.2.2 [[Construct]] より作成)
(一)オブジェクトを作成し、その内部プロパティ[[Class]]に "Object"
を設定する
(二)コンストラクタの prototype
プロパティがオブジェクトであれば、作成されたオブジェクトの内部プロパティ[[Prototype]]に設定する。オブジェクトでなければ Object:
:prototype
の値が使用される
(三)コンストラクタの内部メソッド[[Call]]を呼び出す。引数には[[Construct]]呼び出しに使用されたものが使用される。また this
引数には新しく作成したオブジェクトを使う
(四)[[Call]]呼び出しがオブジェクトを返した場合はそのオブジェクトを返す。そうでなければ最初に作成したオブジェクトを返す
これからコンストラクタの定義について述べるわけですが、そのコンストラクタのコードが実行されるのは上記の第3ステップです。つまり制御がコンストラクタ関数に入ったときには既にオブジェクトは作成されています。このオブジェクトは this
引数としてコンストラクタに渡されます。では実際にコンストラクタを記述してみましょう。別に難しいことはありません
function MyObject() {
this; // 作成されたオブジェクト
}
普通は値を返す必要はありません (上記第4ステップ参照)。呼び出し側は次のようになるでしょう
var obj = new MyObject;
obj; // 作成されたオブジェクト
引数の括弧がありませんがこれは正しいものです。このようにするとコンストラクタに引数は渡されません
これだけではあまり面白くありません。コンストラクタがしなければならないことは﹁オブジェクトを作成すること﹂(概念的な話)、﹁オブジェクトのメンバを初期化すること﹂、﹁オブジェクトが作成できないときに何か対処すること﹂の3つです。素直な設計では作成はコンストラクタが呼び出されたときに終了しているので、ここでは後の2つについてコードを書くことにしましょう。メンバの初期化は this
引数にプロパティをくっつけるだけです
function MyObject(n) {
this.myProperty = n; // プロパティ
}
var obj = new MyObject(7);
obj.myProperty; // 7
コンストラクタは関数でもあるので﹁1. 関数﹂で述べたテクニックが全て使えます。エラーハンドリングについては省略します
メソッドの定義
function MyObject() {
this.myMethod = function() {
};
}
var obj = new MyObject();
obj.myMethod(); // 呼び出す
このメソッド呼び出しは正しく動作しますが JavaScript のメソッド定義からは逸脱しています。例えば以下のコードを見てみましょう
var str = new String();
str.concat; // concat オブジェクト
str.hasOwnProperty("concat"); // false
このことから分かるように String
インスタンスの conc
at
メンバはインスタンスの直接のプロパティ (メソッド) ではないのです。しかしそうは見えません。では本当は何処にプロパティがあるのかというと String::prototype
オブジェクトのプロパティになっています
String.prototype.hasOwnProperty("concat"); // true
つまり MyObject コンストラクタは次のようにする必要があったのです
function MyObject() {
MyObject.prototype.myMethod = function() {
};
// 或いは次でも可
this.constructor.prototype.myMethod = function() {
};
}
ただ、1つのコンストラクタにプロトタイプは1つしか無いのでコンストラクタが呼び出される度にメソッドをセットするのは無駄です。次のようにコンストラクタから外してしまうのが一般的です
function MyObject() {
}
MyObject.prototype.myMethod = function() {
};
プロトタイプ
p
rototype
プロパティを持つインスタンス) に共通のメンバを与えることができます。つまりプロトタイプは静的オブジェクト指向言語のクラスのような役割をしているのです。しかしプロトタイプの仕事はこれだけではありません
コンストラクタとプロトタイプは密接に結びついており、コンストラクタから作成されたオブジェクトはそのコンストラクタに固有なプロトタイプ (例えば MyObject.prototype
) を参照します。この参照は内部プロパティ[[Prototype]]で普通はコード上には現れないものです (JavaScript 1.3 は __proto__
プロパティとしてこの参照を公開しています)。そして上記の myMethod のようにコンストラクタのプロトタイプにセットされたメンバはこの参照を介して探索されます。具体的には以下のようなことが起こります
(一)オブジェクトのメンバにアクセスしようとする。実装は与えられた名前のプロパティがオブジェクトにあるか調べる (上の例の場合は無い)
(二)見つからなかった場合はプロトタイプオブジェクトから与えられた名前のメンバを探す (上記の場合は MyObject.prototype.myMethod が見つかる)
(三)それでも見つからない場合はプロトタイプのプロトタイプから探す
(四)以下、プロトタイプが無くなるまで続ける。プロトタイプが無い場合は内部プロパティ[[Prototype]]には null
がセットされている
このようなプロトタイプの繋がりを勿体付けて プロトタイプチェイン (prototype chain) と呼びます。プロトタイプチェインを使うと同じプロトタイプチェインを辿ることのできるオブジェクトの間に階層的な関係を持ち込むことができます
では簡単な例で少し復習してみましょう。以下は人間オブジェクトの例です
// コンストラクタ
function Person(nAge) {
this.m_nAge = nAge;
}
// 年齢を返すメソッド
Person.prototype.getAge = function() {
return this.m_nAge;
};
var Don = new Person(22); // 勿論これは冗談
var Exeal = new Person(21);
Don.getAge == Exeal.getAge; // true
Don.getAge(); // 22
最後の2行では Don インスタンスから getAge プロパティを探しますが、失敗します。次に Don のプロトタイプ Person::prototype (= Don.__proto__) から探し、プロパティを見つけます
クラスメンバ
クラスメンバ (静的メンバ) はインスタンス毎に与えられるメンバではなくクラス毎に存在するメンバです。JavaScript にはクラスが無いのでコンストラクタメンバとでも呼ぶことにしましょう。コンストラクタメンバは特定のインスタンスが無くても存在可能なメンバで constructor
プロパティもこれに該当します
コンストラクタメンバの定義方法は非常に簡単です。コンストラクタにプロパティを設定するだけです
// コンストラクタ
function Person(nAge) {
this.m_nAge = nAge;
if(Person.m_nPopulation != undefined)
++Person.m_nPopulation;
}
Person.m_nPopulation = 0;
var Don = new Person(22);
var Exeal = new Person(21);
Person.m_nPopulation; // 2
Don.m_nPopulation; // 残念ながらこれは駄目
継承
function Person(nAge) {
this.m_nAge = nAge;
}
Person.prototype.getAge = function() {
return this.m_nAge;
};
function Programmer(nAge, strProject) {
/* Person メンバの継承を実現するコード */
}
var Exeal = new Programmer(21, "EJS");
Exeal.getAge();
まずはプロパティの継承を考えましょう。以下のようにすれば巧くいくような気がします
function Person(nAge) {
this.m_nAge = nAge;
}
function Programmer(nAge, strProject) {
this.m_nAge = nAge; // 真似する
this.m_strProject = strProject; // 新しく追加するプロパティ
}
しかしこれでは2つのコンストラクタの間には何の関係もありません。また Person が変更されると Programmer も変更しなければなりません
(JavaScript の) コンストラクタの仕事にはプロパティの追加以外にインスタンスの初期化も含まれます。上のような簡単なコンストラクタはただ引数を代入しているだけですが、プロパティの数が増えたり複雑なものになると Programmer コンストラクタで同じコードを書くのは馬鹿らしくなります。それにこのままでは Person コンストラクタのクライアントである Programmer コンストラクタが Person の内部事情について先天的な知識を持つ必要があるので、オブジェクト指向の持つ再利用性を著しく欠いてしまいます。ちょっと嫌な方法ですがここでは Programmer コンストラクタから Person コンストラクタを呼び出してインスタンス初期化の一部委譲することにしましょう。新しく作成されたインスタンスに Person コンストラクタを適用するには以下の2つの方法があります
// 1つ目
function Programmer(nAge, strProject) {
this.__super = Person; // 新インスタンスを介して
this.__super(nAge); // 継承元コンストラクタを呼ぶ
this.constructor = Programmer; // コンストラクタが Person にセットされるので元に戻す
delete this.__super;
/* Programmer コンストラクタの処理 */
}
// 2つ目
function Programmer(nAge, strProject) {
Person.call(this, nAge);
this.constructor = Programmer;
/* Programmer コンストラクタの処理 */
}
2つ目の方法は ECMAScript 3rd Edition でのみ使用できます
さて、少々安直ですがプロパティの継承はできました。次はメソッドの継承です。メソッドの実体は Person.prototype に集められているのでこれを Programmer.prototype にコピーすれば良いような気がしますが、次のようにしても巧くいきません
/*
* コンストラクタの定義...
*/
Programmer.prototype = Person.prototype;
Programmer.prototype.getProjectName = function() {
return this.m_strProject;
};
var Exeal = new Person(21);
Exeal.getProjectName; // undefined でない!
プロトタイプをコピーしようとしても実際には参照されるだけです。Programmer.prototype は Person.prototype の中身を参照しているので、ここにメソッドを追加すると Person にも影響が及んでしまいます
勿論参照でなく純粋に複製できたとしても正解ではありません。プロトタイプが継承に使われるのはメンバ探索がプロトタイプチェインを辿るからです。プロトタイプを単純にメソッドの置き場所と考えてはいけません。関連するコンストラクタとプロトタイプは階層的であり、Programmer プロトタイプでの探索に失敗した後、Person プロトタイプを探索するようにしなければいけません。そのためには Programmer プロトタイプのプロトタイプが Person プロトタイプである必要があります。JavaScript なら次のようにすれば良いでしょう
Programmer.prototype.__proto__ = Person.prototype;
﹁でも ECMAScript 仕様外の機能は使いたくない﹂という声が聞こえてきそうですが、これ以外にも方法はあります。しかしこれもちょっと嫌な方法ですが次のようにします
Programmer.prototype = new Person();
直感的には分かりにくいかもしれないので順に見ていきましょう
(一)Person インスタンスを作成し、Programmer コンストラクタのプロトタイプにセットする
(二)Person インスタンスのプロトタイプは Person のプロトタイプである
(三)Programmer.prototype のプロトタイプは Person のプロトタイプである
(四)Programmer のプロトタイプのプロトタイプは Person のプロトタイプである
嘘みたいな方法ですがちゃんと動きます
Programmer.prototype = new Person();
var Exeal = new Programmer(21, "EJS");
Exeal.getAge(); // 21
Exeal.getProjectName(); // "EJS"
Exeal instanceof Person; // true
﹁ちょっと嫌な﹂と書いたのはコンストラクタ呼び出しが新インスタンスの作成以上のことをするからです。例えばコンストラクタメンバで使った m_nPopulation はメンバの継承で1になってしまいます。オブジェクトの構成が簡単であれば色々と対処できますが、継承元のクライアントは継承元のコンストラクタに対して、実際に実用インスタンスを作成するために呼び出したのかメソッドを継承したいだけなのかを通知する手段がありません。この継承の方法に問題が無いのは継承元のコンストラクタがそのように設計されているときだけです。そしてそのようなスペックは継承元が提示しない限りクライアントからは分かりません
この方法ではコンストラクタメンバまでは継承されませんがこれは妥当なことと思えます。コンストラクタメンバはコンストラクタごとに存在するのです
このように JavaScript における継承は継承元コンストラクタの呼び出しによるプロパティのセットと、プロトタイプによるメソッドの継承の2段階で構成されます
多重継承
オーバーライド
function Person() {
}
// 眠る
Person.prototype.sleep = function() {
this.goToBed();
};
この Person からメンバを継承して Programmer を作ると Programmer インスタンスでも sleep メソッドが使えます。しかしプログラマの寝床はベッドではありません。Programmer インスタンスの場合、﹁ベッドに行く﹂という振る舞いを変更しなければいけません。このためには Progammer.prototype にメソッドを設定し直します
function Person() {
}
Person.prototype = new Person();
// 眠る
Programmer.prototype.sleep = function() {
this.relaxOnYourChair();
};
このようにすると Programmer インスタンスの sleep メソッドの呼び出しでは Programmer.prototype.sleep が呼び出されるようになります。このように継承先でメソッドの定義を変更することを オーバーライド といいます
オーバーライドされた元のメソッドを呼び出すには、つまり基底プロトタイプのメソッドを呼び出すには Function::apply
か Function::call
メソッドを使います
var Exeal = new Programmer();
// 今日はベッドで眠れそう
Person.prototype.sleep.call(Exeal);
ただしこの呼び出し自体は Programmer が Person を継承しているかどうかを何も考慮していません。Java には super
キーワードがあり、基底クラスにアクセスできますが ECMAScript の仕様には基底コンストラクタをポイントするための仕組みがありません
プロパティはオーバーライドできません。継承先でプロパティを定義し直すと単純に上書きになります
残りの話題
JScript 、JavaScript 、ECMAScript におけるオブジェクト指向のオフィシャルな (?) 話題は以上のようなものです。JavaScript のメンバの種類、コンストラクタとプロトタイプ、プロトタイプチェインによる継承が可能になりました。これらを習得したことで (習得して頂けましたね?) あなたは JavaScript のオブジェクトを自由に操り、仕事をさせることができます。次節からは更にオブジェクトの実装側のテクニックを取り上げていくことにします
![Valid XHTML 1.1!](/web/20060428053254im_/http://www.interq.or.jp/student/exeal/dss/image/valid-xhtml11.png)