uhyohyo.net

JavaScript初級者から中級者になろう

十一章第七回 継承2

このページの最終更新日:

継承については九章第三回で解説しましたが、じつはあれはもう少しましにできます。第十一章のテーマはES5なので、ES5で可能になったよりよい継承の方法について説明します。

継承の仕組みは既に説明しました。すなわち、各インスタンスはコンストラクタのprototypeに結びついていて、インスタンスのプロパティを探すときは、自分のプロパティに無かったらprototypeのプロパティを探しに行くのでしたね。


function Teki(n){
  this.name = n;
}
//現れる
Teki.prototype.appear = function(){
  console.log(this.name + "があらわれた!");
};

var zako1 = new Teki("ザコ敵");	//敵をつくる
zako1.appear();	//現れる

console.log(zako1.name);	// "ザコ敵"
console.log(zako1.hasOwnProperty("name"));	// true
console.log(zako1.hasOwnProperty("appear"));	// false

このzako1はnameとappearという2つのプロパティがありますが、前にもみたようにnameとappearでは少し性質が違いましたね。nameを代入しているのはTekiコンストラクタ内です。

var zako1=new Teki("ザコ敵"); の行で this.name = n;

として代入しています。このthisはnewの返り値となるべきオブジェクトで、zako1になるものです。つまり、nameプロパティはzako1に直接くっついているわけです。

一方、 zako1.appear=... というような記述はありませんね。appearの実態はTeki.prototype.appearです。

ここでまず、新しいメソッドObject.getPrototypeOfを紹介します。これはインスタンスを引数に渡すと、対応するprototypeオブジェクトが返ってくるわけです。つまり、zako1の場合はTekiのインスタンスですから、Teki.prototypeが返ってくるわけです。

console.log(Object.getPrototypeOf(zako1) === Teki.prototype);	// true

このように、インスタンスの側からprototypeを取得できるのは、今までに無かったES5の新しい機能です(ただし、非標準の__proto__というやつはありました)

ついでに、isPrototypeOfも紹介しておきます。これはObject.prototypeにあるメソッドなので、全てのオブジェクトが持っています。これは、そのオブジェクトが、引数に渡したオブジェクトのprototypeであるかを判定して真偽値を返します。いま、zako1のプロトタイプに当たるのはTeki.prototypeなので次のようになります。


console.log(Teki.prototype.isPrototypeOf(zako1));	// true

また、このメソッドはプロトタイプチェーンをさかのぼって検証してくれます。例えば次のサンプルです。


//敵を作る
function Teki(){
}

//ドラゴンを作る
function Dragon(){
  Teki.apply(this,arguments);
}
Dragon.prototype = new Teki;

var boss = new Dragon();	//インスタンスをつくる

console.log(Dragon.prototype.isPrototypeOf(boss));	// true
console.log(Teki.prototype.isPrototypeOf(boss));	// true

これは、bossはDragon.prototypeにあるメソッドを使えるし、Teki.prototypeにあるメソッドも使えると解釈することができます。実はこれはinstanceofを使っても同じことができますね。instanceofとの違いは、コンストラクタに対して使うかprototypeオブジェクトに対して使うかですね。


console.log(boss instanceof Dragon);	// true
console.log(boss instanceof Teki);	// true

さて、ここでいよいよ本題となる継承がでてきました。継承にかかわるのはDragonコンストラクタ内の Teki.apply(this,arguments); と、プロトタイプの Dragon.prototype = new Teki; のところでしたね。

実は、後者に問題があるのです。そもそもコンストラクタというのは、そのオブジェクトが作られたときの処理を表すものでしたね。ですから、例えばTekiコンストラクタにログを残す処理を書いてみましょう。


function Teki(){
  console.log("新しい敵オブジェクトができました。");
}

//ドラゴンを作る
function Dragon(){
  Teki.apply(this,arguments);
}
Dragon.prototype = new Teki;

var zako = new Teki();
var boss = new Dragon();

これを実行すると、「新しい敵オブジェクトができました。」というログは3回でますね。一方、敵オブジェクトを作ったのはzakoとbossの2つだけのはずです。もう1回は何なのでしょうか。

もうお分かりだと思いますが、 Dragon.prototype = new Teki; の行で一度Tekiのインスタンスを作っていましたね。これが一番最初のログだったのです。

しかしこれは困りますよね。コンストラクタが呼ばれた時にもっと複雑な処理をしたい場合、実際にインスタンスを作ったわけではないのに(プログラム上は確かに作ってますが意味合いが違いますよね)コンストラクタが呼ばれたら不都合だという場合も多いことでしょう。

そこで、ES5ではこの問題を解決する方法があります。それがObject.createです。これは第一引数にプロトタイプオブジェクトを指定すると、それをプロトタイプに持つオブジェクトを作成します。つまりこんな具合です。


var zako2=Object.create(Teki.prototype);

このzako2を調べると、実はTekiのインスタンスになっています。


console.log(Teki.prototype.isPrototypeOf(zako2));	// true

ただし、newを使って作った場合とObject.createを作った場合では一つ違いがあります。Object.createの場合はコンストラクタが呼ばれません。つまり、さっきのvar zako2=Object.create(Teki.prototype);を実行しても「新しい敵オブジェクトができました。」というログは出ません。

newを使っていないのにインスタンスができるというのは不思議かもしれませんが、そもそもオブジェクトが他のコンストラクタのインスタンスであるかどうかは、オブジェクトと関連付けられたprototypeオブジェクトのみによって特徴付けられます。よって、newを使わなくてもprototypeを制御できればインスタンスを作ることができるのです。

当然ながら、Object.createでインスタンスを作ったあとに別にコンストラクタを呼べば、newを使わずにnewの挙動を再現することができます。


var zako2=Object.create(Teki.prototype);
Teki.call(zako2);

2行目のcallは、Teki関数を、zako2をthisとして引数なしで呼べという意味ですね。コンストラクタはnewで呼ばれるときはthisの値が特別になっていたので、call(またはapply)を使ってそれを再現すればできます。

さて、このObject.createを使えば、上述の問題を回避して継承することができます。具体的には、


Dragon.prototype = Object.create(Teki.prototype);

とすればいいのです。

ちなみに、Object.createには第二引数もあります。これはObject.definePropertiesの第二引数と同じ形のオブジェクトを渡すことができます。Object.createに第2引数が渡された場合、インスタンスを作った後に第二引数で指定されたようにプロパティをセットして返してくれるのです。これを使えば、Dragonのインスタンスが持つメソッドを定義するときにDragon.prototype.attack=...というように代入していくのではなく、Object.createで全て済ませるやり方も可能です。例えばこんな感じです。


function Dragon(){
  Teki.apply(this,arguments);
}
Dragon.prototype=Object.create(Teki.prototype,{
  attack:{
    configurable:true,
    value:function(){
      console.log("ドラゴンのこうげき!!");
    },
  },
});

constructorプロパティ

最後に、継承をするときはconstructorプロパティについても気をつける必要があります。これについても紹介します。

実は、constructorプロパティを使えばインスタンスのコンストラクタが何か調べることができます。例えば次のコードです。


//敵を作る
function Teki(){
}

var zako=new Teki();
console.log(zako.constructor === Teki);	// true

しかし、zakoにconstructorなんてプロパティを付けた覚えはありませんよね。ということは、これはprototypeにあるプロパティです。


console.log(Teki.prototype.hasOwnProperty("constructor"));	// true

実は、関数が作られた時点で、その関数のprototype.constructorにはその関数自身が自動的に入っている状態になります。

ところが、継承した場合を見てみましょう。


function Teki(){
}
function Dragon(){
  Teki.apply(this,arguments);
}
Dragon.prototype=Object.create(Teki.prototype);

var boss=new Dragon();
console.log(boss.constructor);

ここではDragonが出てきて欲しいのに、表示されるのはTekiです。これはなぜかというと、最初関数DragonができたときはちゃんとDragon.prototype.constructorにはDragonが入っていたのですが、直後にDragon.prototypeに別のオブジェクトを代入してしまっていますね。代入されたのはTekiのインスタンスでしたから、Dragon.prototype.constructorを調べることはTekiのインスタンスのconstructorプロパティを調べるのと同じです。だからTekiがでたわけです。

とはいえ、これだと困りますね。正しくDragonが表示されるようにする必要があります。そのためには、自分で代入しましょう。


function Teki(){
}
function Dragon(){
  Teki.apply(this,arguments);
}
Dragon.prototype=Object.create(Teki.prototype,{
  constructor:{
    value:Dragon
  }
});

var boss=new Dragon();
console.log(boss.constructor);

こうすれば、boss.constructorがDragonとなり正しくなります。

結局、今回のポイントは2つでした。継承するときはObject.createを使うことと、constructorに正しいコンストラクタを入れてあげることです。