クラスは別のクラスに拡張することができます。 技術的には、プロトタイプの継承に基づいた素晴らしい構文があります。

別のクラスから継承するためには、"extends" と括弧 {..} の前に親クラスを指定する必要があります。

ここでは、RabbitAnimal から継承しています:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }

}

// Animal から継承
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.hide(); // White Rabbit hides!

extends キーワードは、実際には Rabbit.prototype から Animal.prototype[[Prototype]] 参照を追加します。

イメージ "/article/class-inheritance/animal-rabbit-extends.png" が見つかりませんでした

なので、現在 rabbit は自身のメソッドと Animal のメソッド両方へのアクセスを持ちます。

extends の後では任意の式が指定できます

クラス構文では単にクラスではなく、extends の後に任意の式を指定することができます。

例えば、親クラスを生成する関数呼び出します:

function f(phrase) {
  return class {
    sayHi() { alert(phrase) }
  }
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

ここでは、 class Userf("Hello") の結果を継承しています。

多くの条件に依存したクラスを生成するための関数を使用し、それらから継承できるような高度なプログラミングパターンに対して、これは役立つ場合があります。

メソッドのオーバーライド

では、前に進めてメソッドをオーバライドをしてみましょう。今のところ、RabbitAnimal から this.speed = 0 をセットする stop メソッドを継承しています。

もし Rabbit で自身の stop を指定すると、代わりにそれが使われるようになります。:

class Rabbit extends Animal {
  stop() {
    // ...これは rabbit.stop() のために使われる
  }
}

…しかし、通常は親メソッドを完全に置き換えるのではなく、その上に組み立てて、その機能の微調整または拡張を行うことを望んできます。私たちはメソッド内で何かをしますが、その前後またはその処理の中で親メソッドを呼び出します。

クラスはそのために "super" キーワードを提供しています。

  • super.method(...) は親メソッドを呼び出します。
  • super(...) は親のコンストラクタを呼び出します(我々のコンストラクタの内側でのみ)。

例えば、私たちのうさぎが止まったとき自動的に隠れさせましょう。:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stopped.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }

  stop() {
    super.stop(); // 親の stop 呼び出し
    this.hide(); // その後隠す
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // White Rabbit runs with speed 5.
rabbit.stop(); // White Rabbit stopped. White rabbit hides!

これで Rabbit は処理の中で親 super.stop() を呼び出す stop メソッドを持っています。

アロー関数は super を持っていません

チャプター アロー関数ふたたび で述べた通り、アロー関数には super がありません。

もしアクセスすると、外部の関数から取得されます。例えば:

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // 1秒後親の stop 実行
  }
}

アロー関数での superstop() での super と同じです。なので、意図通りに動きます。ここのように “通常の” 関数を指定すると、エラーになります。:

// Unexpected super
setTimeout(function() { super.stop() }, 1000);

コンストラクタのオーバライド

コンストラクタに対しては、少し用心が必要です。

今まで、Rabbit は自身の constructor を持っていませんでした。

仕様(specification)によると、クラスが別のクラスを拡張し、constructor を持たない場合、次のような constructor が生成されます。

class Rabbit extends Animal {
  // 独自のコンストラクタを持たないクラスを拡張するために生成されます
  constructor(...args) {
    super(...args);
  }
}

ご覧の通り、基本的にはすべての引数を渡して親の constructor を呼び出します。それは自身のコンストラクタを書いていない場合に起こります。 では、カスタムのコンストラクタを Rabbit に追加してみましょう。それは name に加えて earLength を指定します。:

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }

  // ...
}

// 動作しません!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this は定義されていません

おっと! エラーになりました。これではうさぎを作ることができません。何が間違っていたのでしょう?

簡単な回答: 継承したクラスのコンストラクタは super(...) を呼び出し、(!) this を使う前にそれを行わなければなりません。

…しかしなぜ? ここで何が起きているのでしょう? 確かにこの要件は奇妙に見えます。

もちろん、それへの説明があります。詳細を見てみましょう。それであなたは何が起こっているのかを本当に理解するでしょう。

JavaScriptでは、“継承しているクラスのコンストラクタ関数” とその他すべてで区別があります。継承しているクラスでは、該当するコンストラクタ関数は特別な内部プロパティ [[ConstructorKind]]:"derived" が付けられます。

違いは:

  • 通常のコンストラクタを実行するとき、this として空のオブジェクトを作り、それを続けます。
  • しかし、派生したコンストラクタが実行されると、そうは実行されません。親のコンストラクタがこのジョブを実行することを期待しています。

なので、もし独自のコンスタクタを作っている場合には、super を呼ばないといけません。なぜなら、そうしないとそれを参照する this を持つオブジェクトは生成されないからです。 結果、エラーになるでしょう。

Rabbit を動作させるために、this を使う前に super() を呼ぶ必要があります。:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    super(name);
    this.earLength = earLength;
  }

  // ...
}

// 今は問題ありませんn
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10

Super: internals, [[HomeObject]]

super の内部をもう少し深く見てみましょう。ここで面白いことがいくつか見られます。

まず最初に、今まで私たちが学んだすべてのことだけでは、 super が動作するのは不可能です。

たしかに技術的にどのように動くのでしょうか?オブジェクトメソッドが実行されるとき、this として現在のオブジェクトを取ります。もし super.method() を呼び出す場合、どうやって method を取得するでしょう?当然ながら、我々は現在のオブジェクトのプロトタイプから method を取る必要があります。技術的に我々(またはJavaScriptエンジンは)どうやってそれをするのでしょうか?

恐らく、this.__proto__.method とすることで this[[Prototype]] からメソッドを取得できる?残念ながらそれは動作しません。

それにトライしてみましょう。簡単にするために、クラスなしで単純なオブジェクトを使用します。

ここでは、rabbit.eat() は親オブジェクトの animal.eat() メソッドを呼び出す必要があります。:

let animal = {
  name: "Animal",
  eat() {
    alert(this.name + " eats.");
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {
    // これがおそらく super.eat() が動作する方法です
    this.__proto__.eat.call(this); // (*)
  }
};

rabbit.eat(); // Rabbit eats.

(*) でプロトタイプ(animal) から eat を取り、現在のオブジェクトコンテキストでそれを呼び出します。 .call(this) はここでは重要であることに注意してください。なぜなら、シンプルな this.__proto__.eat() は現在のオブジェクトではなくプロトタイプのコンテキストで親の eat を実行するためです。

また、上のコードは実際に期待通り動作します: 正しい alert になります。

今度はもう1つのオブジェクトをチェーンに追加しましょう。 私たちは物事がどのように壊れるかを見ていきます:

let animal = {
  name: "Animal",
  eat() {
    alert(this.name + " eats.");
  }
};

let rabbit = {
  __proto__: animal,
  eat() {
    // ...bounce around rabbit-style
    // 親 (animal) メソッドを呼び出す
    this.__proto__.eat.call(this); // (*)
  }
};

let longEar = {
  __proto__: rabbit,
  eat() {
    // ...do something with long ears
    // 親 (rabbit) メソッドを呼び出す
    this.__proto__.eat.call(this); // (**)
  }
};

longEar.eat(); // Error: 最大呼び出しスタックサイズを超えました

コードはこれ以上動作しません! longEar.ear() を呼び出そうとするとエラーになります。

これは明白ではないかもしれませんが、もし longEar.eat() 呼び出しのトレースをすると、それがなぜかがわかります。行 (*)(**) は共に、this の値は現在のオブジェクト (longEar) です。それが肝心です: すべてのオブジェクトメソッドはプロトタイプなどではなく、現在のオブジェクトを this として取得します。

したがって、行 (*)(**) は共に、this.__proto__ の値は全く同じで、rabbit です。それらは両方とも、無限ループで rabbit.eat を呼んでいます。

これは何が起きているかを示す図です。:

  1. longEar.eat() の中で、行 (**)this=longEar となる rabbit.eat を呼び出します。

    // longEar.eat() の中では this = longEar です
    this.__proto__.eat.call(this) // (**)
    // なので次のようになります
    longEar.__proto__.eat.call(this)
    // つまり呼ばれるのは
    rabbit.eat.call(this);
  2. 次に rabbit.eat の 行 (*) で、チェーンの中でより高次へ呼び出しを渡したいですが、this=longEar なので、 this=__prto__.eat は再び rabbit.eat です!

    // rabbit.eat() の中でも this = longEar です
    this.__proto__.eat.call(this) // (*)
    // なので次のようになります
    longEar.__proto__.eat.call(this)
    // なので (再び)
    rabbit.eat.call(this);
  3. …したがって、それ以上高次へ登ることができないので、rabbit.eat はエンドレスで自身を呼び出します。

this だけを使ってこの問題を解くことはできません。

[[HomeObject]]

その解決策を提供するため、JavaScriptはもう1つ関数のための特別な内部プロパティを追加しています: [[HomeObject]] です。

関数がクラスまたはオブジェクトメソッドとして指定されたとき、その [[HomeObject]] プロパティはそのオブジェクトになります。

メソッドはそれらのオブジェクトを覚えているため、これは実際には “バインドされていない” 関数の考え方に反しています。また、[[HomeObject]] は変更することはできないため、このバインドは永遠です。なので、これは言語上の非常に重要な変更です。

しかし、この変更は安全です。[[HomeObject]] は プロトタイプを解決するために、super の中で親メソッドを呼び出すためだけに使われます。従って、互換性を損ねることはありません。

super がどのように動くのか見てみましょう – 平易なオブジェクトを使って再び。:

let animal = {
  name: "Animal",
  eat() {         // [[HomeObject]] == animal
    alert(this.name + " eats.");
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {         // [[HomeObject]] == rabbit
    super.eat();
  }
};

let longEar = {
  __proto__: rabbit,
  name: "Long Ear",
  eat() {         // [[HomeObject]] == longEar
    super.eat();
  }
};

longEar.eat();  // Long Ear eats.

すべてのメソッドは内部の [[HomeObject]] プロパティで、そのオブジェクトを覚えています。そして super は親のプロトタイプの解決のために [[HomeObject]] を使います。

[[HomeObject]] はクラスと単純なオブジェクト両方で定義されたメソッドのために定義されています。しかし、オブジェクトの場合、メソッドは指定された方法で正確に指定されなければなりません。: "method: function()" ではなく method() として指定する必要があります。

下の例では、上の例との比較のために非メソッド構文を使っています。[[HomeObject]] プロパティはセットされず、継承は動作しません。:

let animal = {
  eat: function() { // 短縮構文: eat() {...} にする必要があります
    // ...
  }
};

let rabbit = {
  __proto__: animal,
  eat: function() {
    super.eat();
  }
};

rabbit.eat();  // super 呼び出しエラー([[HomeObject]] が無いため)

静的メソッドと継承

class 構文は静的なプロパティに対しても継承をサポートしています。

例:

class Animal {

  constructor(name, speed) {
    this.speed = speed;
    this.name = name;
  }

  run(speed = 0) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  static compare(animalA, animalB) {
    return animalA.speed - animalB.speed;
  }

}

// Animal から継承
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbits = [
  new Rabbit("White Rabbit", 10),
  new Rabbit("Black Rabbit", 5)
];

rabbits.sort(Rabbit.compare);

rabbits[0].run(); // Black Rabbit runs with speed 5.

これで、継承された Animal.compare が呼び出されると想定される Rabbit.compare を呼ぶことができます。

どのように動くのでしょう?繰り返しますが、プロトタイプを使用します。 すでに推測したように、extendsRabbitAnimal への [[Prototype]] 参照を与えます。

従って、Rabbit 関数は Animal 関数から継承します。そして Animal 関数は通常 Function.prototype を参照する [[Prototype]] を持っています。なぜならそれは何も extend していないからです。

ここで、それを確認してみましょう。:

class Animal {}
class Rabbit extends Animal {}

// 静的プロパティとメソッドの場合
alert(Rabbit.__proto__ == Animal); // true

// 次のステップは Function.prototype です
alert(Animal.__proto__ == Function.prototype); // true

// オブジェクトメソッドの "通常の" プロトタイプチェーンに加えて
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true

このように RabbitAnimal のすべての静的メソッドへアクセスです。

組み込みで、静的継承はありません

組み込みのクラスはこのような静的な [[Prototype]] 参照は持っていないことに注意してください。例えば、ObjectObject.defineProperty, Object.keys やその他を持っていますが、ArrayDate などはそれらを継承しません。

これは DateObject の構造を示す図です。:

DateObject の間のリンクはないことに注意してください。ObjectDate は独立して存在します。Date.prototypeObject.prototype から継承しますが、それだけです。

このような差異は歴史的な理由で存在します。: JavaScript言語の幕開けにクラス構文と静的メソッドを継承することは考えられませんでした。

ネイティブは拡張可能です

Array, Map やその他のような組み込みのクラスもまた拡張可能です。

例えば、ここでは PowerArray はネイティブの Array から継承しています。:

// メソッドを追加する(もっと追加することもできます)
class PowerArray extends Array {
  isEmpty() {
    return this.length == 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false

1つとても興味深いことに留意してください。filter, map やその他組み込みのメソッドは – 正確に継承された型の新しいオブジェクトを返します。 それらはそうするために constructorプロパティに依存しています。

上の例です,

arr.constructor === PowerArray

なので、arr.filter() が呼ばれた時、それは内部で new PowerArray と同じ結果の新しい配列が作成されます。 そして、呼び出しのチェーンの下でさらに使い続けることができます。

さらにその振る舞いをカスタマイズすることもできます。静的な getter Symbol.species が存在する場合、そのようなケースで使うためにコンストラクタを返します。

例えば、ここでは Symbol.species によって、map, filter のような組み込みメソッドは “通常の” 配列を返します。:

class PowerArray extends Array {
  isEmpty() {
    return this.length == 0;
  }

  // 組み込みのメソッドはこれをコンストラクタとして使います
  static get [Symbol.species]() {
    return Array;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

// fileter はコンストラクタとして arr.constructor[Symbol.species] を使って新しい配列を作ります。
let filteredArr = arr.filter(item => item >= 10);

// filteredArr は PowerArray ではなく Array です
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty は関数ではありません

より高度な手段として使用し、不要な場合には結果の値から拡張機能を取り除くことができます。 あるいは、さらに拡張することもできます。

タスク

重要性: 5

これは Animal を拡張した Rabbit のコードです。

残念なことに、Rabbit オブジェクトを作ることができません。何が間違っているでしょう?直してください。

class Animal {

  constructor(name) {
    this.name = name;
  }

}

class Rabbit extends Animal {
  constructor(name) {
    this.name = name;
    this.created = Date.now();
  }
}

let rabbit = new Rabbit("White Rabbit"); // エラー: 定義されていません
alert(rabbit.name);

これは、子のコンストラクタは super() を必ず呼ばないといけないためです。

これが正しいコードです。

class Animal {

  constructor(name) {
    this.name = name;
  }

}

class Rabbit extends Animal {
  constructor(name) {
    super(name);
    this.created = Date.now();
  }
}

let rabbit = new Rabbit("White Rabbit"); // ok now
alert(rabbit.name); // White Rabbit
重要性: 5

私たちは Clock クラスを持っています。今のところ、毎秒時間を表示します。

Clock を継承した新たなクラス ExtendedClock を作成し、precision パラメータを追加してください – “時計のカチカチ” の間の ms の数値です。デフォルトでは 1000 (1秒) です。

  • あなたのコードはファイル extended-clock.js にしてください。
  • オジリナルの clock.js は変更しないでください。それを拡張してください。

タスクのためのサンドボックスを開く

重要性: 5

私たちが知っている通り、すべてのオブジェクトは通常 Object.prototype を継承しており、“一般的な” オブジェクトメソッドにアクセスできます。

ここでのデモンストレーションのように:

class Rabbit {
  constructor(name) {
    this.name = name;
  }
}

let rabbit = new Rabbit("Rab");

// hasOwnProperty メソッドは Object.prototype からです
// rabbit.__proto__ === Object.prototype
alert( rabbit.hasOwnProperty('name') ); // true

従って、"class Rabbit extends Object" は正確に "class Rabbit" と同じである、と言うのは正しいでしょうか?それとも違うでしょうか?

これは動作するでしょうか?

class Rabbit extends Object {
  constructor(name) {
    this.name = name;
  }
}

let rabbit = new Rabbit("Rab");

alert( rabbit.hasOwnProperty('name') ); // true

もし動かない場合、コードを直してください。

解答は2つパートがあります。

最初に、簡単な方は、継承しているクラスはコンストラクタで super() を呼ぶ必要があるということです。そうでなければ "this" が “定義済み” になりません。

なので、次のように直します:

class Rabbit extends Object {
  constructor(name) {
    super(); // 継承しているとき、親コンストラクタを呼ぶ必要があります
    this.name = name;
  }
}

let rabbit = new Rabbit("Rab");

alert( rabbit.hasOwnProperty('name') ); // true

しかし、これですべてではありません。

修正した後でさえ、"class Rabbit extends Object"class Rabbit では依然として重要な違いがあります。

知っている通り、 “extends” 構文は2つのプロトタイプを設定します。:

  1. コンストラクタ関数の "prototype" 間(メソッド用)
  2. コンストラクタ関数自身の間(静的メソッド用)

我々のケースでは、class Rabbit extends Object では次を意味します:

class Rabbit extends Object {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) true

従って、このように Rabbit 経由で Object の静的メソッドにアクセスすることができます。:

class Rabbit extends Object {}

// 通常は Object.getOwnPropertyNames と呼びます
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // a,b

また、extends を使わない場合 class Rabbit は2つ目の参照を持ちません。

比較してみてください:

class Rabbit {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) false (!)

// エラー、Rabbit にこのような関数はありません
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // Error

シンプルな class Rabbit では Rabbit 関数は同じプロトタイプを持っています。

class Rabbit {}

// (2) の代わりに、これは正しいです:
alert( Rabbit.__proto__ === Function.prototype );

ところで、Function.prototype は “一般的な” 関数メソッドを持っています。例えば call, bind などです。それらは究極的には両方のケースで利用可能です。なぜなら、組み込みの Object コンストラクタに対して、Object.__proto__ === Function.prototype だからです。

これはその図です:

従って、まとめると2つの違いがあります。:

class Rabbit class Rabbit extends Object
コンストラクタで super() を呼ぶ必要がある
Rabbit.__proto__ === Function.prototype Rabbit.__proto__ === Object
チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。