第4回 JavaScriptでオブジェクト指向プログラミング連載:Ajax時代のJavaScriptプログラミング再入門(3/4 ページ)

» 2007年09月25日 00時00分 公開

(2)プロトタイプ・オブジェクトの変更はリアルタイムに認識

 プロトタイプ・オブジェクト配下のメンバが(インスタンスにコピーされるわけではなく)暗黙的な参照を通じて、必要都度にアクセスされるという事実には、もう1つ大きなメリットがある。それは、インスタンスを生成した「後」に、基となるプロトタイプ・オブジェクトにメンバを追加した場合にも、これを認識できるという点である。

 例えば、以下のような例を見てみよう。

var Animal = function() {};
Animal.prototype.name = "サチ";
var anim = new Animal();

Animal.prototype.sex = "メス"; // インスタンスの生成後にメンバを追加

window.alert(anim.sex); // 「メス」

リスト10 インスタンスの生成後にメンバを追加した例

 もっとも、この性質は、先ほどの「暗黙的な参照」を理解していれば、さほど驚くには当たらないだろう(むしろ当然ともいえる性質である)。

 以上が、プロトタイプ・オブジェクトの基本であるが、ここでもう1つ、プロトタイプ・オブジェクトをよりシンプルに定義するための記法を紹介しておく。

■プロトタイプをオブジェクト・リテラルで定義する

 ここまでのコード例では、プロトタイプ・オブジェクトに対して、ドット演算子で個々のメンバを追加してきた。もちろん、これはこれで正しい記法なのだが、メンバの数が多くなってきた場合、どうしてもドット演算子による記法ではコードが冗長になりがちだ。

 ささいなことであるかもしれないが、毎回、「Animal.prototype.メンバ名 = 〜」のように記述しなければならないのはタイプ量という観点でもうれしくないし、そもそもクラス名(本稿ではAnimal)が変更になった場合に、すべてのメンバ定義について変更しなければならないという点も好ましくない。

 そこで登場するのが、オブジェクト・リテラル表現である。リテラルとは、任意の式内に直接に記述可能なデータ値(表現)のこと。本連載でも、関数リテラルや配列リテラルについて紹介してきたが、リテラル表現を利用することで、より記述上の制約を受けずに柔軟なコードを記述できるというメリットがあることは、すでに実感いただけているのではないだろうか。

 オブジェクトにもリテラル表現があるというならば、これを使わない手はない。以下は、その具体的なコード――Animalクラス(Animal.prototype)に対して、リテラル表現を使って、getVoice/toStringメソッドを追加する例だ。

var Animal = function(name, sex){
  this.name = name;
  this.sex = sex;
}

Animal.prototype = {
  getVoice : function() {
    window.alert(this.name + "「チュウ!」");
  },
  toString : function() {
    window.alert(this.name + " " + this.sex);
  }
};

var anim = new Animal("トクジロウ", "オス");
anim.toString(); // 「トクジロウ  オス」

リスト11 オブジェクト・リテラルを使ったプロトタイプの定義

 なるほど、リテラル表現を利用することで、「Animal.prototype.〜 =」のような式を記述する必要がなくなった分だけ、コードがすっきりと見やすくなった。

 ちなみに、オブジェクト・リテラルの「{名前 : 値, ……}」という表記は、JavaScriptにおける連想配列(ハッシュ)の記法でもある(後述のコラム「オブジェクトと連想配列」を参照)。通常、プロトタイプ・オブジェクトに対して複数のメンバをまとめて追加する場合には、このオブジェクト・リテラルの記法を用いるのが好ましい。

■プロトタイプ・チェーン − JavaScriptの継承機構 −

 プロトタイプ・ベースのオブジェクト指向を理解するうえで、もう1つ、忘れてはならない重要なキーワードとして「プロトタイプ・チェーン」がある。プロトタイプ・チェーンとは、プロトタイプ・ベースのオブジェクト指向における継承機構であるといってもよいだろう。

var Animal = function() {}
Animal.prototype = {
  walk : function() {
    window.alert("トコトコ");
  }
};

var Hamster = function() {};
Hamster.prototype = new Animal();

Hamster.prototype.gnaw = function() {
  window.alert("ガジガジ…");
};

var ham = new Hamster();
ham.walk(); // 「トコトコ」 [A]
ham.gnaw(); // 「ガジガジ…」

リスト12 Animalクラスを継承するHamsterクラスを定義する例

 注目していただきたいのは、リスト内の太字の部分――プロトタイプ・オブジェクト(Hamster.prototype)にAnimalクラスのインスタンスを格納しているという点だ。これによって、Hamsterクラスのインスタンスでは、Animalクラスのwalkメソッドを呼び出すことが可能になる。

 この動作は、先ほどの「暗黙的な参照」を理解していれば、その延長線上の概念として理解は容易であるはずだ。

 JavaScriptでは、まず現在のインスタンスからメンバの検索を行う。そこで該当するメンバが存在しない場合には、次にインスタンスの基となるオブジェクトのプロトタイプから適合するメンバを検索する。そして、それでも該当するメンバが検出されなかった場合には、さらに、そのプロトタイプに格納されたオブジェクトの基となるオブジェクトのプロトタイプから適合するメンバを検索するというわけだ。

プロトタイプ・チェーン

 つまり、リスト12内の[A]でwalkメソッドを呼び出すと、まず(1)hamインスタンス自身のメンバを、次に(2)Hamsterクラスのprototypeプロパティ内を、そして、(3)Animalクラスのprototypeプロパティ内を、順に検索していくことになる。結果、Animal.prototypeで定義されたwalkメソッドを検出し、これを実行するというわけだ。

 このように、JavaScriptではプロトタイプにインスタンスを設定することで、インスタンス間の継承関係を形成することができる。もちろん、この継承関係はさらに多階層にすることも可能で、その場合にも順に階層をさかのぼって、最上位のObject.prototypeに行き当たるまでメンバの検索が行われることになる*4。そして、このようなプロトタイプの連なりを称して、「プロトタイプ・チェーン」と呼ぶ。

*4 第1回でも軽く触れたように、Objectオブジェクトはすべてのクラスの基本(基底オブジェクト)となるものだ。すべてのオブジェクトは暗黙的にObjectオブジェクトを継承し、Object.prototypeを参照している。


 さて、このプロトタイプ・チェーン。JavaScriptにおける継承機構であると述べたが、C#やVisual Basicのようなクラス・ベースのオブジェクト指向とは大きく異なるポイントがある。というのも、クラス・ベースのオブジェクト指向では継承関係が静的に決まるのに対して、JavaScript(プロトタイプ・チェーン)では継承関係を自由に変更可能であるという点だ。

 例えば、以下の例を見てみよう。

var Animal = function() {}
Animal.prototype = {
  walk : function() {
    window.alert("トコトコ");
  }
};

var SuperAnimal = function() {}
SuperAnimal.prototype = {
  walk : function() {
    window.alert("ダーッ!");
  }
};

var Hamster = function() {};
Hamster.prototype = new Animal(); // Animalを関連付け
var ham1 = new Hamster();
ham1.walk(); // 「トコトコ」 [A]

Hamster.prototype = new SuperAnimal (); // SuperAnimalを関連付け
var ham2 = new Hamster();
ham2.walk(); // 「ダーッ!」 [B]
ham1.walk(); // ??? [C]

リスト13 プロトタイプ・チェーンを動的に切り替える例

 ここでは、Hamster.prototypeにAnimalオブジェクトを関連付けた状態でインスタンスham1を、その後、SuperAnimalオブジェクトに切り替えた状態でインスタンスham2を生成している。このため、それぞれ[A][B]ではプロトタイプ・チェーンをたどって、Animal/SuperAnimalクラスのwalkメソッドを実行しているわけだ。

 ここまでは、ごく直感的に理解できる挙動だと思う。では、[C]の結果はどうなるだろう。インスタンスham1を生成したタイミングでは、Hamster.prototypeにはAnimalオブジェクトが関連付いていた。しかし、[C]の時点ではすでにHamster.prototypeにセットされているのはSuperAnimalオブジェクトである。とすると、[C]では現在のプロトタイプであるSuperAnimalオブジェクトのwalkメソッドが呼び出され、「ダーッ!」が出力されるのだろうか。

 しかし、結果は「トコトコ」となる。このことから、JavaScriptにおいて、いったん形成されたプロトタイプ・チェーンはその後の変更にかかわらず保存されることが理解できる。この点は間違えやすいポイントの1つでもあるので、注意していただきたい。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。