クラス構文の基礎は、下記を参照。
クラス構文(ES2015)の基本
クラス構文には、継承を行うための方法も用意されている。
class サブクラス extends スーパークラス{ サブクラスの定義 };
メソッドの追加、オーバーライド
サブクラスでメソッドを定義すると、そのインスタンスは、スーパークラスのメソッドとサブクラスのメソッド、両方を使用できるようになる。
class Person{ constructor(name, age){ this.name = name; this.age = age; }; sayName(){ console.log(this.name+'です。'); }; }; class Student extends Person{ showCard(){ console.log('これが学生証です。'); }; }; const tom = new Student('Tom', 20); tom.sayName(); // Tomです。 tom.showCard(); // これが学生証です。
thisとsuper
サブクラスのメソッド定義部分のthis
は、生成されるインスタンスを指す。
そのため、this
を使うことで、自身(のプロトタイプ)が持っているメソッドにアクセスすることが出来る。
class Person{ constructor(name, age){ this.name = name; this.age = age; }; saySelf(){ console.log('This is super.'); }; }; class Student extends Person{ saySelf(){ console.log('This is sub.'); }; checkThis(){ console.log(this); }; checkMethod(){ this.saySelf(); }; }; const tom = new Student('Tom', 20); tom.checkThis(); // Student { name: 'Tom', age: 20 } tom.checkMethod(); // This is sub.
サブクラスはthis
の他に、super
というキーワードも使うことが出来る。
これはスーパークラスを指すものであり、これを使えば、スーパークラスのメソッドにアクセスすることが出来る。
class Person{ constructor(name, age){ this.name = name; this.age = age; }; saySelf(){ console.log('This is super.'); }; }; class Student extends Person{ saySelf(){ console.log('This is sub.'); }; checkThis(){ console.log(this); }; checkMethod(){ this.saySelf(); super.saySelf(); }; }; const tom = new Student('Tom', 20); tom.checkThis(); // Student { name: 'Tom', age: 20 } tom.checkMethod(); // This is sub. // This is super.
また、this
で指定したメソッドを自身が持っていなかった場合、スーパークラスにそのメソッドがないか探し、存在する場合はそれを実行する。
class Person{ constructor(name, age){ this.name = name; this.age = age; }; saySelf(){ console.log('This is super.'); }; }; class Student extends Person{ checkMethod(){ this.saySelf(); }; }; const tom = new Student('Tom', 20); tom.checkMethod(); // This is super.
ここで発生しているのは、プロトタイプチェーンである。
これについては後述する。
constructorの使い方
上記の例では、新しくメソッドを追加しただけで、プロパティはスーパークラスのそれと同一であった。
では、プロパティを追加したり、変更したりする場合は、どうすればいいのか。
サブクラスでもconstructor
メソッドを記述することで、サブクラス固有のプロパティを定義できる。
だがこの場合、様々な注意点がある。
super()が必須
サブクラスでconstructor
メソッドを記述する場合、その中でsuper()
を実行する必要がある。
どんなケースでも必要であり、super()
がないとエラーが発生する。
また、super()
は1度しか呼べず、複数回呼び出しても、エラーが発生する。
super()はプロパティの値の初期化を行う
Person
のサブクラスStudent
を定義し、そのインスタンスtom
を生成した。
スーパークラスとサブクラス、両方でconstructor
を定義している。
class Person{ constructor(name, age){ this.name = name; this.age = age; console.log('スーパークラスのconstructorが呼ばれました。'); }; sayName(){ console.log('私の名前は'+this.name+'です。'); }; }; class Student extends Person{ constructor(name, age){ console.log(1); super(); console.log(2); console.log('サブクラスのconstructorが呼ばれました。'); console.log(this); }; }; const tom = new Student('Tom', 20); // 1 // スーパークラスのconstructorが呼ばれました。 // 2 // サブクラスのconstructorが呼ばれました。 // Student { name: undefined, age: undefined } tom.sayName(); // 私の名前はundefinedです。
ログの結果から、サブクラスでsuper()
を使用すると、そのタイミングでスーパークラスのconstructor
を実行することが分かる。
そして、スーパークラスで定義したプロパティ(このケースではname
とage
)の値がundefined
になっている。
プロパティ自体は存在するが、中身はundefined
になるのである。
これは、super()
はスーパークラスのconstructor
を呼び出すのだから、当然の結果である。
super()
を引数なしで実行すれば、そのままスーパークラスのconstructor
が引数なしで実行され、このような結果になる。
super()
に引数を与えれば、それに応じた値が設定される。
class Person{ constructor(name, age){ this.name = name; this.age = age; console.log('スーパークラスのconstructorが呼ばれました。'); }; sayName(){ console.log('私の名前は'+this.name+'です。'); }; }; class Student extends Person{ constructor(name, age){ super(name, age); console.log('サブクラスのconstructorが呼ばれました。'); console.log(this); }; }; const tom = new Student('Tom', 20); // スーパークラスのconstructorが呼ばれました。 // サブクラスのconstructorが呼ばれました。 // Student { name: 'Tom', age: 20 }
プロトタイプではなく、インスタンス自身がプロパティを持つ
constructor
で定義したプロパティは生成されるインスタンス自身が持つが、サブクラスでもそれは変わらない。
サブクラスでconstructor
を定義していなくても、同様である。
class Person{ constructor(name){ this.name = name; }; }; class Student extends Person{ sayName(){ console.log(this.name); console.log(super.name); }; }; const tom = new Student('Tom'); tom.sayName(); // Tom // undefined console.log(Object.getOwnPropertyNames(tom)); // [ 'name' ]
継承とプロトタイプチェーン
constructor
で定義したプロパティは生成されるインスタンス自身が持つ。
だがそれ以外のプロパティ(メソッド)は、インスタンスではなくプロトタイプが持つことになる。
そのメソッドを定義したクラスのプロトタイプに格納されていく。
そしてサブクラスからは、スーパークラスのprototype
に参照できる。
つまり、サブクラスからスーパークラスへのプロトタイプチェーンが発生するということである。
プロトタイプチェーンそのものについては、下記を参照。
プロトタイプの基礎
Object.creat()とプロトタイプチェーン
以下のようなコードがあったとする。
class Person{ constructor(name){ this.name = name; }; sayName(){ console.log('私の名前は'+this.name+'です。'); }; }; class Student extends Person{ constructor(name, grade){ super(name); this.grade = grade; }; sayGrade(){ console.log('私は'+this.grade+'年生です。'); }; }; const tom = new Student('Tom', 2); // tomは、自身のプロパティとしてnameとgradeを持っている console.log(Object.getOwnPropertyNames(tom)); // [ 'name', 'grade' ] tom.sayName(); // 私の名前はTomです。 tom.sayGrade(); // 私は2年生です。
生成されたインスタンスtom
が持っているプロパティは、name
とgrade
である。
だが、sayName
やsayGrade
を呼び出せている。
これらのメソッドは、どこにあるのだろうか。
まずsayGrade
。
これは、Student
のprototype
に入っている。
そしてこれはtom
のプロトタイプであるため、tom
からアクセスすることが可能になるのである。
class Person{ constructor(name){ this.name = name; }; sayName(){ console.log('私の名前は'+this.name+'です。'); }; }; class Student extends Person{ constructor(name, grade){ super(name); this.grade = grade; }; sayGrade(){ console.log('私は'+this.grade+'年生です。'); }; }; const tom = new Student('Tom', 2); // tomは、自身のプロパティとしてnameとgradeを持っている console.log(Object.getOwnPropertyNames(tom)); // [ 'name', 'grade' ] console.log(Object.getOwnPropertyNames(Student.prototype)); // [ 'constructor', 'sayGrade' ] console.log(Student.prototype.isPrototypeOf(tom)); // true
次にsayName
。
これは、Student.prototype
にもない。
sayName
を持っているのは、Person.prototype
である。
Person.prototype
はStudent.prototype
のプロトタイプであるため、tom
はプロトタイプチェーンを辿って、sayName
にアクセスできる。
class Person{ constructor(name){ this.name = name; }; sayName(){ console.log('私の名前は'+this.name+'です。'); }; }; class Student extends Person{ constructor(name, grade){ super(name); this.grade = grade; }; sayGrade(){ console.log('私は'+this.grade+'年生です。'); }; }; const tom = new Student('Tom', 2); // tomは、自身のプロパティとしてnameとgradeを持っている console.log(Object.getOwnPropertyNames(tom)); // [ 'name', 'grade' ] console.log(Object.getOwnPropertyNames(Student.prototype)); // [ 'constructor', 'sayGrade' ] console.log(Student.prototype.isPrototypeOf(tom)); // true console.log(Object.getOwnPropertyNames(Person.prototype)); // [ 'constructor', 'sayName' ] console.log(Person.prototype.isPrototypeOf(Student.prototype)); // true
ちなみに、Person.prototype
のプロトタイプはObject.prototype
であり、Object.prototype
のプロトタイプはnull
、つまりここが、プロトタイプチェーンの終端である。
つまり、クラス構文を使っていても、行われているのは従来のプロトタイプチェーンによる継承と、何も変わらないのである。
console.log(Object.prototype.isPrototypeOf(Person.prototype)); // true console.log(Object.prototype.__proto__ === null); // true
このような仕組みになっているため、当然、マスキングも発生する。
class Test{ saySelf(){ console.log('このメソッドは Test で定義されています。'); }; }; class Sub1 extends Test{ }; class Sub2 extends Test{ saySelf(){ console.log('このメソッドは Sub で定義されています。'); }; }; class Sub3 extends Test{ constructor(){ super(); this.saySelf = ()=>{ console.log('このメソッドは Subのconstructor で定義されています。'); }; }; saySelf(){ console.log('このメソッドは Sub で定義されています。'); }; }; const sub1 = new Sub1(); const sub2 = new Sub2(); const sub3 = new Sub3(); sub1.saySelf(); // このメソッドは Test で定義されています。 sub2.saySelf(); // このメソッドは Sub で定義されています。 sub3.saySelf(); // このメソッドは Subのconstructor で定義されています。
3つのサブクラスがあり、どれもTest
というスーパークラスを継承している。そしてTest
には、saySelf
が定義されている。
Sub1
にはsaySelf
というメソッドはないため、プロトタイプチェーンを辿り、スーパークラスのそれにアクセスする。
Sub2
では、自身のプロトタイプにsaySelf
が定義されているため、スーパークラスのそれにアクセスすることはない。
Sub3
では、自身のconstructor
でも、saySelf
を定義している。constructor
で定義されたものは、プロトタイプではなくインスタンス自身が持つことになる。
そちらが先にアクセスされるため、プロトタイプで定義されているsaySelf
が呼び出されることはない。