uhyohyo.net

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

十一章第七回 継承2

継承については九章第三回で解説しましたが、じつはあれは今では少し古い方法となっています。第十一章のテーマはECMAScript5なので、ECMAScript5で登場したよりよい継承の方法について説明します。

継承の仕組みは既に説明しました。すなわち、各インスタンスはコンストラクタの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を取得できるのは今までに無かった、ECMAScript5の新しい機能です(ただし、非標準の__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のインスタンスを作っていましたね。これが一番最初のログだったのです。

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

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

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

このzako2を調べると、new Tekiとしてインスタンスを作った場合と同じ結果になります。

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

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

それ以外はnewを使った場合と同じです。zako2はTekiのインスタンスになっています。

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

例えばこのzako2を例にした場合、

Object.getPrototypeOf(zako2)===Teki.prototype	//true

です。これはzako2 instanceof Tekiと同値です。だから、newを使っていないけれどもzako2は確かにTekiのインスタンスだということができます。

ちなみに、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);

とすればいいのです。こっちのほうが不都合が少ないので、継承するときはこうするとよいでしょう(ただしIE8以下では動きませんが)。

ちなみに、Object.createには第二引数もあります。これは十一章第四回で解説したObject.definePropertiesの第二引数と同じ形のオブジェクトを渡すことができます。つまり、インスタンスを作った後に、第二引数で指定されたようにプロパティをセットして返してくれるのです。これを使って、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のインスタンスを調べるのと同じで、さっきのzako.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に正しいコンストラクタを入れてあげることです。