30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

クラス構文(ES2015)による継承とプロトタイプチェーン

クラス構文の基礎は、下記を参照。
クラス構文(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を実行することが分かる。
そして、スーパークラスで定義したプロパティ(このケースではnameage)の値が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が持っているプロパティは、namegradeである。
だが、sayNamesayGradeを呼び出せている。
これらのメソッドは、どこにあるのだろうか。

まずsayGrade
これは、Studentprototypeに入っている。
そしてこれは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.prototypeStudent.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が呼び出されることはない。