クラス構文

【JavaScript】 JSにおける継承とは何を指しているのか

更新日:2022/01/25

オブジェクト指向言語には重要な概念として継承があります。
JavaScriptにもありますが、他の言語とは少し異なります。

ここでは、JavaScriptの継承についてお伝えします。

 

継承とはprototypeへの参照である

多くの言語において継承とは、クラスに定義されているメソッド等を他クラスで引継ぎ、利用可能とする仕組みを指します。

しかしJavaScriptは少し違います。
JavaScriptの継承とは元となるオブジェクトのprototypeプロパティを、新しいオブジェクトのプロトタイプチェーンに組み込むことで、元となるオブジェクトの機能を使用可能にする仕組みを指します。

javascript 継承

この仕組みは、コンストラクター関数またはclass構文でおこなうことができます。

次のコードは、コンストラクター関数aを継承するオブジェクトobjを作成しています。

コンストラクター関数からインスタンスを作成


function a(){};
a.prototype.method = function(){
    // 何らかの処理
};
  // a{ 
  //    prototype:{ method : function(){} }
  // }

const obj = new a;
  // obj{ 
  //    [[プロトタイプ]]:a.prototypeへの参照 ← コンストラクター関数aを継承している
  // }

次のコードは、classでの継承です。

classからインスタンスを作成


class a{
    method(){
       // 何らかの処理
    }
};
  // a{ 
  //    prototype:{ method : function(){} }
  // }

const obj = new a;
  // obj{ 
  //    [[プロトタイプ]]:a.prototypeへの参照 ← コンストラクター関数aを継承している
  // }

他言語でクラスを使っている人は違和感を感じると思います。
クラスから生成されたインスタンスがそのクラスを継承しているのは、クラスの概念からするとおかしいのです。

実はJavaScriptのclass構文はソースコードが読み込まれて評価されるときに、コンストラクター関数に変換されます。
つまりコンストラクター関数からインスタンスを作成するパターンと同じなのです。

JavaScriptでは一般的なクラスの概念を一度捨てた方がいいですね。

 

JavaScriptの継承パターン

JavaScriptは、コンストラクター関数からインスタンスを作成することを継承と呼ぶことができますが、ここでは多階層の継承パターンを考えてみます。

class構文を使用した多階層継承

class構文で多階層の継承をおこなう場合、extendsキーワードを使用します。


class a{
    value  = "a";
    #privateValue = "a";
    constructor() {  }
    method1(){ return this.value; }
    method2(){ return this.#privateValue; }
}
class b extends a{
    value  = "b";
    #privateValue = "b";
    constructor() { super();}
    method1(){ return super.method1() + this.value; }
    method2(){ return super.method2() + this.#privateValue; }
}
class c extends b{
    value  = "c";
    #privateValue = "c";
    constructor() { super(); }
    method1(){ return super.method1() + this.value; }
    method2(){ return super.method2() + this.#privateValue; }
}

const obj = new c;

上のコードを実行すると、次のようなオブジェクトが作成されます。


obj: {
    value:  "c"
    #privateValue: "a"
    #privateValue: "b"
    #privateValue: "c"

    [[プロトタイプ]]: {
        method1: function method1()​​{ return super.method1() + this.value; }
        method2: function method2(){ return super.method2() + this.#privateValue; }

        [[プロトタイプ]]: {
            method1: function method1()​​{ return super.method1() + this.value;  }
            method2: function method2(){ return super.method2() + this.#privateValue; }

            [[プロトタイプ]]: {
                method1: function method1()​​{ return this.value;  }
                method2: function method2(){ return this.#privateValue; }
            }
        }
    }
}

class構文で定義したメソッドが、プロトタイプチェーン内でネストされているのがわかります。

またパブリックプロパティは、派生クラスのみ有効です。
基底クラスのパブリックプロパティは保持されません。
プライベートプロパティはクラスごとに保持され、クラスのメソッド内でthis値経由でアクセスできます。
そのため各メソッドを実行すると、次のような結果になります。


console.log(  obj.method1() , obj.method2() ); // "ccc" "abc"

プライベートおよびパブリックプロパティは、ECMAScript2022で導入された機能です。古いブラウザでは使用できない可能性があります。

コンストラクター関数を使用した多階層継承

前項のclass構文をコンストラクター関数で書き換えてみます。


function a(){
    let privateValue;
    const aClass = function (){
        this.value = "a";
        privateValue = "a";
    }
    aClass.prototype ={
        method1(){ return this.value; },
        method2(){ return privateValue; }
    };
    return aClass;
}

function b(){
    let privateValue;
    const aClass = a();
    const bClass = function (){
        aClass.call(this);
        this.value = "b";
        privateValue = "b";
    }
    bClass.prototype={
        method1(){
            return  aClass.prototype.method1.call(this) + this.value;
        },
        method2(){
            return  aClass.prototype.method2.call(this) + privateValue;
        }
    };
       // bClass.prototypeのプロトタイプチェーンにaClass.prototypeをセット
    Object.setPrototypeOf( bClass.prototype, aClass.prototype ); 
    return bClass;
}

function c(){
    let privateValue;
    const bClass = b();
    const cClass = function (){
        bClass.call(this);
        this.value = "c";
        privateValue = "c";
    }
    cClass.prototype={
        method1(){
            return  bClass.prototype.method1.call(this) + this.value;
        },
        method2(){
            return  bClass.prototype.method2.call(this) + privateValue;
        }
    };
        // cClass.prototypeのプロトタイプチェーンにbClass.prototypeをセット
    Object.setPrototypeOf( cClass.prototype, bClass.prototype );
    return cClass;
}

const obj = new ( c() );

a()、b()、c()の各関数は内部で変数privateValueを定義してります。
この変数は、同じく内部で定義しているコンストラクター関数とprototypeプロパティ内のメソッドのみからアクセス可能です。
つまり、変数privateValueはプライベートな変数として機能します。
いわゆるクロージャー的な手法ですね。

そしてprototypeプロパティのプロトタイプチェーンに下階層のオブジェクトをセットして、最後にコンストラクター関数を返しています。

上のコードを実行すると、次のようなオブジェクトが作成されます。


obj: {
    value:  "c"

    [[プロトタイプ]]: {
        method1: function method1()​​{ return bClass.prototype.method1.call(this) + this.value; }
        method2: function method2(){ return bClass.prototype.method2.call(this) + privateValue; }

        [[プロトタイプ]]: {
            method1: function method1()​​{ return aClass.prototype.method1.call(this) + this.value;  }
            method2: function method2(){ return aClass.prototype.method2.call(this) + privateValue; }

            [[プロトタイプ]]: {
                method1: function method1()​​{ return this.value;  }
                method2: function method2(){ return privateValue; }
            }
        }
    }
}

変数privateValueはオブジェクト外に配置されているので、ここでは確認できません。

次に関数cを実行した結果に対して、new演算子を適用します。
( c() )となっているのは、外側のカッコがないと関数cに対してnew演算子が適用されてしまうからです。

各メソッドを実行すると、次のような結果になります。


console.log(  obj.method1() , obj.method2() ); // "ccc" "abc"

前項のclass構文と同じ結果を得ることができました。

しかし、とても面倒ですね。
多階層継承の継承は、class構文を使用したほうが簡単ですね。

更新日:2022/01/25

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。