C/C++ プログラマのための JavaScript 入門: prototype と継承
オブジェクトの話の2回目です。 今回は prototype の話から。 この辺までだといかにも入門って感じで楽なんですけどねぇ。 例によって「プロローグ」で挙げた文献を頻繁に参照しています。 これらの文献を参照しながらご覧になることをお薦めします。 またこの回では説明の都合上クラスとコンストラクタを同義に扱っています。 この点については脳内で適当に変換して読んでいただけると嬉しいです。
これまで説明したとおり関数もオブジェクトです。 変数やプロパティに値として代入できるほか, 自身にもプロパティやメソッドを追加することができます(そういう意味ではデータ型のオブジェクトの一種とみなせます)。 例えばこういう書き方もできます。
uniqueId.counter = 0; function uniqueId() { return uniqueId.counter++; } WScript.Echo("ID =", uniqueId()); // 0 WScript.Echo("ID =", uniqueId()); // 1 WScript.Echo("ID =", uniqueId()); // 2
ユーザ定義のプロパティ以外に組み込みのプロパティもいくつかあります。 そのうちのひとつが prototype プロパティです。 このプロパティは関数をコンストラクタとして実行する際に威力を発揮します。 prototype プロパティ自身はオブジェクト値で, 自由にプロパティやメソッドを追加できます。 prototype の特徴は, 各インスタンスから通常のプロパティのように参照できる, ということです。 prototype を使わずにインスタンスを生成する場合は以下のようになります。
function Rectangle(width, height) { this.width = width; this.height = height; this.area = function() { return this.width*this.height; }; } var r = new Rectangle(2, 2); var a = r.area(); WScript.Echo("r(2,2).area() =>", a); // 4
この方法では2つのプロパティと1つのメソッドが作成されますが, インスタンスを生成するたびに3つの値を(メモリ上に)用意する必要があります。 しかもメソッドには毎回同じ値が格納されます。 一方 prototype を使うと以下のように記述できます。
function Rectangle(width, height) { this.width = width; this.height = height; } Rectangle.prototype.area = function() { //コンストラクタが呼び出されるたびに初期化するのを防ぐため外に出す return this.width*this.height; }; var r = new Rectangle(2, 2); var a = r.area(); WScript.Echo("r(2,2).area() =>", a); // 4
Rectangle のプロパティであるはずの prototype.area() メソッドを r インスタンスのメソッドのように参照しているのが分かると思います。 もちろん prototype プロパティもオブジェクトなので内部に prototype プロパティを持ちます。 従って,あるインスタンスのプロパティを参照する際には(目的のプロパティが見つかるまで) prototype をどんどんさかのぼっていくことになります。 これを「プロトタイプ・チェイン」と呼びます。 一方, 参照ではなく値を設定する場合は「プロトタイプ・チェイン」は発生しません。
function MyClass() { } MyClass.prototype.className = "No Name"; var obj = new MyClass(); WScript.Echo("obj.className =", obj.className); // No Name obj.className = "My Name"; WScript.Echo("obj.className =", obj.className); // My Name WScript.Echo("MyClass.prototype.className =", MyClass.prototype.className); // No Name
参照におけるプロトタイプ・チェインの仕組みはオブジェクト指向プログラミングにおける「継承」そのものです。 これまでの例はクラス(=コンストラクタ)からインスタンスへの継承でしたが, クラスからクラスへの継承は可能でしょうか。 これも可能です。 ひとつの方法は prototype プロパティを他のオブジェクトに書き換える方法です。
function SuperClass() { } SuperClass.prototype.superClassName = "Super Class"; function SubClass() { } SubClass.prototype = new SuperClass(); // prototype を書き換える SubClass.prototype.subClassName = "Sub Class"; var obj = new SubClass(); WScript.Echo("obj.superClassName =", obj.superClassName); // Super Class WScript.Echo("obj.superClassName =", obj.subClassName); // Sub Class
SubClass.prototype に SuperClass のインスタンスを設定しているのがポイントです。 ただし問題もあります。 一番の問題は SubClass の constructor プロパティ値が SuperClass の値になってしまうことです。 これを回避するには constructor に自身のコンストラクタをオーバーライドさせます。 具体的には上記のコードに以下の文を追加します。
SubClass.prototype.constructor = SubClass;
また, ECMAScript 標準ではないようですが (JavaScript のみ動作可能。JScript では機能しない), __proto__ を使う方法もあります。(WSH では動作しないので注意)
<script type="text/javascript"> function SuperClass() { } SuperClass.prototype.superClassName = "Super Class"; function SubClass() { } SubClass.prototype.__proto__ = SuperClass.prototype; // SuperClass.prototype をセット SubClass.prototype.subClassName = "Sub Class"; var obj = new SubClass(); document.write("obj.superClassName = ", obj.superClassName); // Super Class document.write("<br/>"); document.write("obj.superClassName = ", obj.subClassName); // Sub Class </script>
SuperClass のインスタンスではなく SuperClass.prototype を __proto__ に代入(参照渡し)しているのがポイントです。
プロトタイプ・チェインを辿っていくと最終的に Object に行き着きます。 Object 自身の prototype は null で終端になっています。 言い換えれば全てのクラスは Object クラスのサブクラスであるということです (ユーザが作成したクラスも暗黙的に Object から継承されます)。 Number, String, Boolean, Function, Array といった組み込みクラスも Object のサブクラスです。 これまでちゃんと説明しなかった toString() や valueOf() や constructor といったプロパティ/メソッドは Object から継承しています。
なお, JavaScript では多重継承はサポートされていません。 これは継承の仕組みをプロトタイプ・チェインで実現しているからだと思いますが, 実際問題として多重継承は使いどころが難しいです。 JavaScript ではプロパティやメソッドがオブジェクトであることを利用して容易に集約やオーバーライドができるため, むしろそれらの特徴を生かしたプログラミングを考えるほうが得策かもしれません。
さて次回からは各データ型に対応する組み込みオブジェクトについて, もう少し詳しく見ていくことにしましょう。