uhyohyo.net

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

九章第三回 継承

継承とは

今回は継承というものについて解説します。継承とは、あるオブジェクトの特徴をもったままで、さらに拡張したオブジェクト(の型)をつくるというものです。

例えば、今回もRPGの例で考えてみます。「敵」というオブジェクトを作ることにします。

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

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

これは、前回同様のコードです。

さて、ひとくちに「敵」といっても、「スライム」とか「魔法使い」とか「ドラゴン」とかいろいろな種類の敵がいます。このとき、「魔法使い」という種類と個々の魔法使いは、今までみてきた「勇者」という型と個々の勇者オブジェクトの関係と同じです。すなわち、コンストラクタとインスタンスの関係ですね。

では、ここで「敵」と「魔法使い」や「ドラゴン」の関係はどうでしょう。「魔法使い」や「ドラゴン」はそれぞれ攻撃方法は違うことでしょう。しかし、HPがあるという点はどれも同じです。つまり、HPは「敵」の特徴で、それは「魔法使い」や「ドラゴン」にも当然あります。そして、攻撃方法は「魔法使い」や「ドラゴン」独特の特徴なのです。

ここで、「魔法使い」や「ドラゴン」は、 それであると同時に「敵」でもあるのです。つまり、「敵」というグループの中に「魔法使い」や「ドラゴン」というグループがあることになります。

つまり、「魔法使い」や「ドラゴン」は「敵」から派生したものなのです。このとき、これらは「敵」を継承しているといいます。

継承のやり方

さて、それでは継承はどうやればいいのでしょうか。まず最初にサンプルを見てみましょう。「敵」を作り、さらにそれを継承する「ドラゴン」を作ります。

//敵を作る
function Teki(){
  this.hp = 100;
}

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

//攻撃してHPを教えてもらう
Dragon.prototype.attack = function(){
  console.log("ドラゴンの攻撃! ドラゴンのHPは"+this.hp+"だ!");
};

var boss = new Dragon();	//インスタンスをつくる
boss.attack();	//攻撃してもらう
        

※注: 今回紹介している継承の方法にはまだ多少の問題があります。そのあたりの話は、十一章第七回を参照してください。

強調してある部分が継承と関係がある部分なのですが、それは除いて動作の確認をしてみましょう。

変数bossに「ドラゴン」のインスタンスを作成して代入しています。そして、そのattackメソッドを呼び出しています。attackメソッドでは、自らのプロパティhpを表示しています(なぜ攻撃するとHPが分かるのかは気にしないことにしましょう)。

ここで、hpというプロパティはDragonにはありませんね。Tekiのコンストラクタで作って代入しています。つまり、このhpは、Dragonの特徴ではなくTekiの特徴ということです。継承ができていますね。ちゃんとメソッドも継承できます。

さて、それでは継承の仕組みを説明していきます。まず、順番が逆になりますが、 Dragon.prototype = new Teki;の部分です。 new Teki という書き方はインスタンスを作っているように見えますが、関数呼び出しの()がありません。実は、newを使うときは()を省略することができます。その場合、引数を渡すことができないので、引数を受け取る場合はすべてundefinedとなります。これは書き方の問題なので、new Teki()と書いても問題ありません。ここではインスタンスを作っています。

そして、それの代入先がなんとDragonのprototypeです。

今までは、コンストラクタを作った直後prototypeの中には何も入っていなくて、そこにいろいろ付け足していく感じでした。しかし、今回は、まずprototypeにTekiのインスタンスを代入してから、そのあとattackメソッドを付け足しています。このとき何が起こるのでしょう。

prototypeは、インスタンスがプロパティなどを持っていないときにそこから探すというものでした。そして、その探す先がTekiのインスタンスです。ですから、Tekiのインスタンスが持つべきプロパティを探すことができるのです。

ということは、Dragonのインスタンスは、Dragonであると同時にTekiのインスタンスとしての役目も果たしているということです。ただし、当然ながら、Dragonのprototypeで既にプロパティがみつかった場合(TekiからDragonに拡張するにあたって変更・追加された場合)はTekiまではいきません。

ちなみに、DragonのprototypeがTekiのインスタンスなのですが、Tekiのインスタンスがプロパティを持っていなかったらどうなるのでしょう。そう、Tekiのprototypeを今度は探します。

このように、prototypeをだんだんさかのぼって探していくことは、プロトタイプチェーンと呼ばれたりします。

プロトタイプチェーンの終端

ここでオブジェクトについて振り返ります。

オブジェクトを作るとき、

new Object()

のような表記をしました。今思えば、これはObjectのインスタンスを作っているということだったのです。つまり、オブジェクトは、Objectのインスタンスであるということです。

では、TekiとかDragonとか、

new Object()

ではない(Objectのインスタンスではない)ものはどうなのでしょう。

実は、これらはObjectを継承しているのです。つまり、継承先のオブジェクトのインスタンスは同時に継承元のインスタンスとしての役割も果たすから、本質的にすべてのオブジェクトはObjectのインスタンスであるということができます。

さて、すべてのオブジェクトがObjectを継承しているとはどういうことでしょう。もう一度振り返ると、インスタンスのプロパティが無いときはコンストラクタのprototypeを探すのでした。例えば、Dragonのインスタンスの場合は、Dragon.prototypeを探します。

そして、それがTekiのインスタンスだったから、Dragon.protoypeそれ自体に存在しないプロパティの場合は次にTeki.prototypeを探すことになるので、DragonのインスタンスはTekiのインスタンスとしての役割も果たしているということになっているのでした。

では、特に何も継承していないものはどうでしょう。実は、初期状態でprototypeは普通のオブジェクトです。普通のオブジェクトとは、つまりObjectのインスタンスです。

だから、すべてのオブジェクトはObjectを継承しているということができるのです。

ちなみに、Object.prototypeにもメソッドなどはあります。つまり、Object.prototypeにあれば、そのメソッドはすべてのオブジェクトが持っているということです。これについては第十一章第四回で多少説明しています。

さて、これでやっと

Dragon.prototype = new Teki;

の説明が終わりました。次は

apply

	Teki.apply(this, arguments);

です。これには、知らない単語が2つも含まれています。

まず、applyの説明です。これは、見て分かるとおりメソッドです。Tekiが持っていますね。で、Tekiは何かというと、関数オブジェクトでした。つまり、関数が持つメソッドということです。

ちなみに、「関数が持つメソッド」とはどういうことかというと、実は関数はFunctionのインスタンスだったのです。つまり、Function.prototypeにこのapplyはあります。

さて、applyは、関数を実行するメソッドです。関数を実行するなら、

関数名()

のようにして呼び出せばいいように思えます。しかし、applyには他にない特徴があります。

引数は2つです。1つめは関数内のthis,2つめは呼び出す関数の引数です。

呼び出す関数の引数は、配列で指定します。では、関数内のthisとはなんでしょう。ここでは、thisが引数として渡されています。実は、これは、呼ばれた関数内でthisが何を表すかを表しています。

今回の場合、Dragonのコンストラクタの中でTekiを呼び出しています。これなぜかというと、Dragonのインスタンスが作られるとき、それはTekiのインスタンスとしての役目も果たすから、Tekiとしての初期化もしなければいけないのです。具体的には、Tekiのコンストラクタを呼び出すのです。

コンストラクタでは、これから作るオブジェクトがthisとなるのを利用するのでした。ここで、普通に

Teki();

としたのでは、thisはそうなりません。そこで、thisを操作できるapplyを利用するのです。applyの第一引数にthisを設定すれば、Tekiの中で、thisはDragonの中でのthis(新しく作るオブジェクト)と同じになるのです。これにより、new Teki()として新しいオブジェクトを作ったのと同じように(ただしthisはDragonのインスタンスですが)処理させることができます。

さて、次にargumentsです。これは、applyの第二引数で渡されています。これは呼び出す関数(今回の場合Teki)に渡す引数を配列で指定するのでした。では、このargumentsは配列なのでしょうか。実は、ちょっと違います。

argumentsは、関数の情報が入ったオブジェクトなのですが、配列とよく似ています。arguments[0],arguments[1]・・・のように添字を指定することで、それぞれ第一引数,第二引数・・・を取得することができるのです。つまり、

function aaa(a,b){
            }

という関数の中で、arguments[0]はaと同じ、arguments[1]はbと同じです。

また、配列同様にlengthプロパティもあります。これらを利用することで、引数がいくつか分からない関数も作ることができます。まだargumentsには機能がありますが、今回は省略します。

さて、applyの第二引数には本来配列を指定するのですが、argumentsは配列とよく似ているので、argumentsも使うことができます。すると、Dragonが受け取った引数をそのままTekiに渡すことができるのです。これは、Teki(継承元)とDragon(継承先)で引数が同じときのみ使える技というわけです。Dragonで引数が増えたりした場合などは、自分で配列を作ってapplyで呼び出すか、callを使う方法があります。

callは、apply同様関数がもつメソッドです。第一引数はapplyと同じですが、第二引数以降で関数に渡す引数を1つずつ指定します

まとめると、

	Teki.apply(this, arguments);

の行は、Dragonコンストラクタの中で、Tekiのコンストラクタにも処理をしてもらうというものでした。Dragonのインスタンスは同時にTekiのインスタンスでもあるので、これはやるべきです。

instanceof

ここでひとつ余談です。instanceofという演算子を紹介しておきます。

これは、オブジェクト instanceof コンストラクタの形で、オブジェクトがあるコンストラクタのインスタンスかどうか真偽値で返してくれる演算子です。

例えば、

変数 instanceof Array

とすると変数が配列かどうか判定できます(ただし配列の場合は、一章第三回で紹介したArray.isArrayのほうがよいでしょう)。これは、配列はArrayのインスタンスだからです。

さっきのTekiとDragonも使って試してみましょう。

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

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

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

console.log(boss instanceof Dragon);	//インスタンスがDragonのインスタンスかどうか
console.log(boss instanceof Teki);	//インスタンスがTekiのインスタンスかどうか
          

結果はどちらもtrueです。確かに、DragonのインスタンスがTekiのインスタンスとしても扱われています。

サンプル

最後に、復習的なサンプルです。

//敵を作る
function Teki(n){
  this.name = n;
}
//現れる
Teki.prototype.appear = function(){
  console.log(this.name+"が あらわれた!");
};
//攻撃
Teki.prototype.attack = function(){
  console.log(this.name+"のこうげき!ダメージをうけた!");
};

//ドラゴンを作る
function Dragon(){
  Teki.apply(this,arguments);
}
Dragon.prototype = new Teki;
//ドラゴンの攻撃は強い
Dragon.prototype.attack = function(){
  console.log(this.name+"のこうげき!大ダメージをうけた!");
}

var zako = new Teki("ザコ");	//インスタンスをつくる
var boss = new Dragon("ボス");

zako.appear();	//ザコ現れる
boss.appear();	//ボス現れる
zako.attack();	//ザコの攻撃
boss.attack();	//ボスの攻撃
          

ザコとボスを作っています。コンストラクタにはどちらも引数で名前を指定しています。その引数を処理してプロパティnameに代入するのはTekiの仕事ですが、Dragonのインスタンスを作った場合でも、applyしていることでちゃんとTekiの仕事も行われています。

その後、appearです。appearは、Tekiにしかありません。しかし、DragonもTekiのインスタンスなので、どちらもTekiのappearを使っています。

次のattackでは、TekiにもDragonにもあり、中身が違います。ザコのほうはTekiのインスタンスなのでTekiの、ボスはDragonなのでDragonのを使っています。

このように、継承を利用することの利点は、まず「敵」という大元に、appearとかattackとか基本的な機能を作っておけば、すべての種類の敵(継承先)でそれが使えるということです。また、このように同名のメソッドを継承先に新たに作れば、そのメソッドを書き換えたかのように動作します。いずれの場合にも、attackというメソッドを呼び出せば、「攻撃」の処理をうまくやってくれるわけです。プログラムを組む側は、attackをするのが「敵」なのか、はたまた「ドラゴン」なのか「魔法使い」なのか(プログラム中に出てきませんでしたが)は気にする必要がありません。これがオブジェクト指向です。

※継承についてのより詳しい説明が十一章第七回にあります。実際にプログラムで継承を利用する際は、こちらも読んでおくことをおすすめします。