uhyohyo.net

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

十六章第十二回 クラスの継承

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

今回は前回の続きで、クラスの構文について解説します。前回紹介しなかったクラスの機能がひとつあります。それは継承です。

以前継承について紹介したときは、prototypeを駆使して継承を実現していました。Object.createなどを使うと多少は楽になりましたが、結構苦労するし分かりにくいポイントでした。クラスの構文では、継承をもっと楽に行うことができます。

例によって、まず例を見ましょう。


class Teki{
  constructor(name, hp){
    this.name = name;
    this.hp = hp;
  }
  attack(){
    console.log(`${this.name}の攻撃!`);
  }
}

class Dragon extends Teki{
  hello(){
    console.log(`${this.name}のHPは${this.hp}です。`)
  }
}

var boss = new Dragon('どらごん', 3000);

boss.attack(); // どらごんの攻撃!
boss.hello();  // どらごんのHPは3000です。

このように、クラスの宣言時にextendsというキーワードによって継承元のクラスを指定できます。この例では、DragonはTekiを継承しています。

よって、DragonのインスタンスであるbossはTekiのメソッドattackとDragonのメソッドhelloを利用できます。とても簡単ですね。

これの裏では、以前解説したprototypeチェーンがちゃんとあります。hasOwnPropertyObject.getPrototypeOfを使って確かめてみましょう。


console.log(Dragon.prototype.hasOwnProperty('hello')); // true
console.log(Dragon.prototype.hasOwnProperty('attack')); // false
console.log(Object.getPrototypeOf(Dragon.prototype) === Teki.prototype); // true

上の例からは、Dragonのインスタンスであるbossはnameとかhpのプロパティを持っていることが分かります。これらのプロパティはTekiのコンストラクタで代入されているものです。ということは、コンストラクタの継承(Dragonのコンストラクタが呼ばれたらTekiのコンストラクタも呼ぶ)も自動で行われていることが分かります。

ちなみに、継承元の指定は式でできますので、下のようなこともできます(この例はとくに意味はないですが)。


class Foo extends (class {}){}

静的プロパティの継承

静的プロパティは、前回も話に出たような、コンストラクタに直接ついているプロパティ(あるいはメソッド)のことです。継承を行った場合、実は静的プロパティも継承されます。つまり、こういうことです。


class Foo{
  static prop(){
    return 'hi';
  }
}
class Bar extends Foo{
}

console.log(Bar.prop()); // 'hi'

この例では、Fooについている静的メソッドpropを、Barに付いているものとして呼び出すことができました。

これはある種のprototypeチェーンによって実現されています。具体的には次のようになっています。


console.log(Object.getPrototypeOf(Bar) === Foo); // true

super呼び出し

さっきの例では、Dragonのコンストラクタは省略されていました。その結果Tekiのコンストラクタが自動で呼び出されています。では、Dragonのコンストラクタを作りたい場合はどうでしょうか。


class Teki{
  constructor(name, hp){
    this.name = name;
    this.hp = hp;
  }
  attack(){
    console.log(`${this.name}の攻撃!`);
  }
}

class Dragon extends Teki{
  constructor(name, hp){
    console.log('がおーーーー');
  }
  hello(){
    console.log(`${this.name}のHPは${this.hp}です。`)
  }
}

var boss = new Dragon('どらごん', 3000);

boss.attack(); // どらごんの攻撃!
boss.hello();  // どらごんのHPは3000です。

実は、こうするとエラーが出ます。クラス構文を使って継承する場合、子のコンストラクタ内で親クラスのコンストラクタを必ず呼ばなければいけないのです。子クラスのコンストラクタを自分で用意する場合はこの点に気をつけなければなりません。

そして、親クラスのコンストラクタを呼ぶための構文が用意されています。コンストラクタの中ではsuperという特別なキーワードが用意されており、これを関数として呼ぶことで親クラスのコンストラクタを呼んだことになります。すなわち、次のようにします。


class Teki{
  constructor(name, hp){
    this.name = name;
    this.hp = hp;
  }
  attack(){
    console.log(`${this.name}の攻撃!`);
  }
}

class Dragon extends Teki{
  constructor(name, hp){
    super(name, hp);
    console.log('がおーーーー');
  }
  hello(){
    console.log(`${this.name}のHPは${this.hp}です。`)
  }
}

var boss = new Dragon('どらごん', 3000);

boss.attack(); // どらごんの攻撃!
boss.hello();  // どらごんのHPは3000です。

一般にコンストラクタではthisのプロパティに代入するなどしてオブジェクトの初期化を行いますが、super呼び出しはthisを触る前である必要があります(thisに全く触らないでもsuperは呼び出す必要があります)。子クラスというのは親クラスの拡張だから、まず親クラスで初期化されたオブジェクトをいじるべきだということです。以前applyなどを使って親クラスのコンストラクタを呼び出していたのに比べるととても簡単ですね。

また、前回コンストラクタが返り値を返す場合の挙動について述べたのを覚えているでしょうか。そのようなクラスを継承すると、面白い挙動を示します。次の例を見てください。


class Foo{
  constructor(x, y){
    return [x, y];
  }
}

class Bar extends Foo{
  constructor(x, y, z){
    super(x, y);
    this.push(z);
  }
}

console.log(new Bar(1, 2, 3)); // [1, 2, 3]

new Bar(1, 2, 3)とすると、まずBarのコンストラクタが呼ばれます。その中でsuper呼び出しによりFooのコンストラクタが呼ばれます。Fooのコンストラクタは、配列を作って返すコンストラクタでした。super呼び出しによって呼ばれたコンストラクタがこのように返り値を返した場合、thisの値がその値で置き換わります。よって、super(x, y);の後のthisはnew Foo(x, y)の結果と同等(この例の場合は[1, 2])となります。よって、Barのコンストラクタ内ではsuperの後のthis.push(z);で配列に要素を加えることができたのです。この場合、new Barの結果はこのthisになるので、[1, 2, 3]が返されたのです。

このように、値を返すようなコンストラクタを継承するとその返り値もある意味で継承されます。thisをいじる前にsuperを呼ばなければいけない理由はここにもあります。親クラスのコンストラクタによってthisが変更されるかもしれないからですね。

superメソッド呼び出し

実は、superというキーワードにはもうひとつ機能があります。それは、親クラスのメソッドを呼び出すことができる機能です。

たとえば、ドラゴンの攻撃は強力なので勇者を毒状態にできるという場合を考えます。攻撃はattackメソッドなので、Dragonにもattackメソッドを作ることになります。このとき、普通の攻撃(Tekiクラスの攻撃)に追加機能を与えるように実装するときれいです。それには、次のようにします。


class Teki{
  constructor(name, hp){
    this.name = name;
    this.hp = hp;
  }
  attack(){
    console.log(`${this.name}の攻撃!`);
  }
}

class Dragon extends Teki{
  constructor(name, hp){
    super(name, hp);
    console.log('がおーーーー');
  }
  attack(){
    super.attack();
    console.log('勇者は毒状態になった!');
  }
  hello(){
    console.log(`${this.name}のHPは${this.hp}です。`)
  }
}

var boss = new Dragon('どらごん', 3000);

boss.attack(); // どらごんの攻撃!勇者は毒状態になった!
boss.hello();  // どらごんのHPは3000です。

ポイントは17行目のsuper.attack();です。このように、super.メソッド名とすると、親クラスのメソッドを得ることができます(super[文字列]の形でもいいです)。この場合、Dragonのattackメソッドの中でsuper.attackとしたので、これはその親クラスのattackメソッド、すなわちTeki.prototype.attackとなります。

この文法にはひとつ特別な点があります。それは、super.メソッド名として得たメソッドを呼び出す場合、thisの値は現在の値と同じになるということです。

上の例ではboss.attack()としたので、Dragonのattackメソッド中でのthisの値はbossになっています。そこでsuper.attack()によりTekiのattackメソッドを呼び出したので、その中でもthisはbossのままです。よって、そこで参照されたthis.nameがちゃんと"どらごん"になっているのです。

このsuper参照の機能により親クラスのメソッド呼び出しが楽になっています。

なお、この文法はsuperというオブジェクトのメソッドを参照しているように見えますが、そうではなく、この形でひとつの構文になっています。よって、これ以外の形でsuperを使おうとするのは文法エラーとなります。例えばこういうのはエラーです。


class Teki{
  constructor(name, hp){
    this.name = name;
    this.hp = hp;
  }
  attack(){
    console.log(`${this.name}の攻撃!`);
  }
}

class Dragon extends Teki{
  constructor(name, hp){
    super(name, hp);
    console.log('がおーーーー');
  }
  attack(){
    super.attack();
    console.log('勇者は毒状態になった!');
    console.log(super); // 文法エラー
    return super.name;
  }
  hello(){
    console.log(`${this.name}のHPは${this.hp}です。`)
  }
}

これがエラーとなるのは、superという変数が存在しているわけではないことを意味しています。superが使えるのは、コンストラクタ内で関数として呼び出す場合と、上のように親クラスのメソッドを参照する場合のみです。

クラスの機能はこれで全てです。今までのJavaScriptらしからぬ文法ですがとても便利なので、使える時は使うとよいでしょう。

new.target

ややクラスに関連する話題があるのでついでに紹介しておきます。それはnew.targetです。これはES2015で追加されたメタプロパティのひとつです。メタプロパティという言葉はfunction.sentの話のときにちらっとでてきました。

new.targetは関数の中で使うことができ、newで呼ばれたときはそのときのコンストラクタが入っています。また、ただの関数として呼ばれたときはundefinedとなります。

ひとつの使い方は、関数がnewで呼ばれたのかそうでないのかを判別することです。また、クラスの間に継承関係があるときに親クラスのコンストラクタ内でnew.targetを見ると子クラスを得ることができます。


class Teki{
  constructor(){
    console.log('コンストラクタは', new.target.name);
  }
}

class Dragon extends Teki{
  constructor(){
    super();
  }
}

var zako = new Teki(); // コンストラクタは Teki
var boss = new Dragon(); // コンストラクタは Dragon