クラスの継承のメカニズム - プロトタイプチェイン

《 初回公開:2022/11/17 , 最終更新:2024/02/15 》

【 目次 】

prototypeと__proto__とconstructor

prototypeプロパティ

クラスはprototypeプロパティを持ち、それはスーパクラスのインスタンスのようだ。

class SuperCls { }
class MyCls extends SuperCls { }

// MyCls.prototypeはSuperClsのインスタンスであるがMyClsのインスタンスでは無い
console.log(MyCls.prototype instanceof SuperCls);   // true
console.log(MyCls.prototype instanceof MyCls);      // false

それに対して、クラスのインスタンスはprototypeプロパティを持たない。

let myObj = new MyCls()
console.log(myObj.prototype);   // undefined

__proto__プロパティ

インスタンスは__proto__プロパティを持ち、その値はそのインスタンスが属するクラスのprototypeプロパティを指す。

console.log(myObj.__proto__ === MyCls.prototype);   // true

クラスも__proto__プロパティを持ち、その値はクラスのスーパークラスを指す。

console.log(MyCls.__proto__ === SuperCls);  // true

従って、prototypeプロパティはクラスにだけだが、
__proto__プロパティはインスタンスもクラスもを持っている事になる。


__proto__プロパティの使用は現在では非推奨で、

これは非推奨扱いで、代わりに Object.getPrototypeOf/Reflect.getPrototypeOf および Object.setPrototypeOf/Reflect.setPrototypeOf を推奨しています (とはいえ、オブジェクトの [[Prototype]] の設定は、性能が気になる場合には避けるべき低速の操作ですが)。

本稿では、簡略化のため以降も__proto__をコードとして使っているが、正しくはこれをObject.getPrototypeOf(obj)のようなコードに置き換える必要があります。

しばしば、__proto__は[[prototype]]と表現される。
Chromeブラウザ等のデバッガーでもそう表示されている。

Note: ECMAScript 標準に基づき、someObject.[[Prototype]] という記法は、 someObject のプロトタイプを示します。これは JavaScript の __proto__ プロパティ(現在は非推奨)と同等です。
その関数のすべてのインスタンスの [[Prototype]] を代わりに指定する関数の func.prototype プロパティと混同するべきではありません。

__proto__ は [[Prototype]] の値を設定,取得するためのgetter/setterらしい。

constructorプロパティ

クラスのconstructorプロパティ

クラスのprototypeプロパティにはconstructorプロパティが格納されていて、その値はクラス自身を指している。
クラスのnameプロパティよりクラス名を確認できる。

console.log(MyCls.prototype.constructor.name);  // MyCls
console.log(SuperCls.prototype.constructor.name);  // SuperCls

クラスの直接のconstructorプロパティ(クラスのprototypeのconstructorプロパティではなく)はFunctionを指している。

console.log(MyCls.constructor.name);    // Function
console.log(SuperCls.constructor.name); // Function

インスタンスのconstructorプロパティ

インスタンスはconstructorプロパティを持ち、その値はインスタンスのクラスを指す

console.log(myObj.constructor === MyCls); //true

そして前述したように、クラスの__proto__プロパティはスーパークラスを指すので、
インスタンスのコンストラクタの__proto__はスーパークラスを返す

console.log(myObj.constructor.__proto__ === SuperCls);  //true

インスタンスのconstructorプロパティは、インスタンスでは無く、実はクラスのprototypeに格納されている

console.log(myObj.constructor === MyCls.prototype.constructor); //true
console.log(MyCls.prototype.constructor === MyCls); //true

console.log(myObj.hasOwnProperty("constructor"));   // false
console.log(MyCls.prototype.hasOwnProperty("constructor")); // true

クラスのprototypeはスーパークラスのインスタンスであるが、そのconstructorプロパティは自分自身を指すように差し替えられている。

Objectクラスのprototypeプロパティ

Objectクラスのインスタンスの__proto__プロパティにはObjectクラスのprototypeプロパティが格納されていて

let obj = new Object();
console.log(obj.__proto__ === Object.prototype);    // true

更に、Objectクラスのprototypeプロパティの__proto__はnullに

console.log(Object.prototype.__proto__);    // null

プロトタイプチェイン

オブジェクトが継承関係にある時、オブジェクト間の継承は__proto__プロパティによってリンクされていて、プロトタイプチェインと呼ばれている。

インスタンスオブジェクトのプロトタイプチェイン(インスタンスのプロパティの継承)

インスタンスの__proto__プロパティはそのインスタンスが属するクラスのprototypeプロパティを指すが
更にクラスのprototypeプロパティの__proto__プロパティはインスタンスが属するクラスのスーパークラスのprototypeプロパティとなる。

console.log(myObj.__proto__.__proto__ === SuperCls.prototype);   // true
console.log(myObj.__proto__.__proto__.constructor.name);        // SuperCls

インスタンスの__proto__プロパティをたどっていくと基底クラス(extendsを使って他のクラスから継承されていないクラス,ここでの例ではMyClsのスーパクラスSuperClsを指す)のprototypeプロパティを経てObjectクラスののprototypeプロパティにたどり着き、
更にその先のObjectクラスのprototypeプロパティはnullになる。

つまりプロトタイプチェインをたどって、次々と__proto__プロパティの指す先のオブジェクトを調べると

myObj => MyCls.prototype => SuperCls.prototype => Object.prototype => null

そして

[インスタンス].__proto__ が [クラス].prototypeを指すという事は[インスタンス]が[クラス]のインスタンスである事を示している。

クラスオブジェクトのプロトタイプチェイン(クラスのプロパティの継承)

インスタンスと異なりクラスの__proto__プロパティはスーパークラスを指すが、
更にそのスーパクラスの__proto__プロパティをたどっていくと、基底クラスの__proto__プロパティはFunction.prototypeを指す。

ここで、インスタンスの__proto__プロパティはそのインスタンスが属するクラスのprototypeプロパティを指すので、 基底クラスはFunctionクラスのインスタンスということに。
更にFunction.prototypeの__proto__プロパティはObject.prototypeを指すので、Function.prototypeの__proto__プロパティはObjectクラスのインスタンスということになる。
そして最後のObject.prototypeはnullを指す。

つまり

MyCls => SuperCls(Functionクラスのインスタンス) => Function.prototype(Objectクラスのインスタンス) => Object.prototype => null

基底クラス(extendsを使って他のクラスから継承されていないクラス)はFunctionクラスのインスタンスでその先はObjectクラスという事か。

extendsにより継承元が指定されていないクラスはObjectクラスを継承

基底クラスつまりextendsにより継承元が指定されていないクラスはObjectクラスを継承していて

class SuperCls{ }

これは、下記のコードと等価であり

class SuperCls extends Object{ }

そして、FunctionクラスもObjectクラスを継承している。

class Function extends Object { ... }

インスタンスもクラスも最後はObjectクラスのインスタンスにたどりつく事になる。

プロトタイプチェインをコードで再検証

これまで述べた事をコードで検証。

インスタンスの継承関係

クラス間の継承関係が以下のようになっている時。

class SuperCls { }
class MyCls extends SuperCls { }
let myObj = new MyCls();

インスタンスは__proto__プロパティを持ち、その値はクラスのprototypeプロパティを指す

console.log(myObj.__proto__ === MyCls.prototype);   //true

インスタンス自体はprototypeプロパティを持たない

console.log(myObj.prototype);   // undefined

でもインスタンスに後からprototypeプロパティを定義する事はできるが、これは混乱するだけなのでやめた方が良い。

myObj.prototype = "後から定義";
console.log(myObj.prototype);   // 後から定義

インスタンスの属するクラス名は、[インスタンス].constructor.nameで得る事ができる。

console.log(myObj.constructor.name);    // MyCls

インスタンスは__proto__プロパティの更に__proto__プロパティはそのインスタンスが属するクラスのスーパクラスのprototypeプロパティを指している。

console.log(myObj.__proto__.__proto__ === SuperCls.prototype);   // true
console.log(myObj.__proto__.__proto__.constructor.name);   // SuperCls

インスタンスの__proto__プロパティをたどる事でインスタンスの継承関係を調べる事ができる。

console.log(myObj.__proto__.__proto__.__proto__ === Object.prototype);   // true
console.log(myObj.__proto__.__proto__.__proto__.constructor.name);   // Object

__proto__プロパティをたどると最後(Object.prototype.__proto__)はnullに

console.log(Object.prototype.__proto__ === null);   // true

この事を利用して、
インスタンスから__proto__をたどってインスタンスの継承関係を調べてみると

class SubCls extends MyCls { }

let o = new SubCls();
let s = "";
o = o.__proto__;
while (o) {
    if (s) {
        s += "=>"
    }
    s += o.constructor.name;
    o = o.__proto__;
}
console.log(s); // SubCls=>MyCls=>SuperCls=>Object

SubClsクラスのprototypeプロパティはMyClsのインスタンスであって、
同様にMyClsクラスのprototypeプロパティはSuperClsのインスタンス。
そして、extendsが省略されているSuperClsクラスのprototypeプロパティはObjectのインスタンス。

console.log(SubCls.prototype instanceof MyCls); // true
console.log(MyCls.prototype instanceof SuperCls);   // true
console.log(SuperCls.prototype instanceof Object);  // true

すなわち、クラスのprototypeプロパティはスーパークラスのインスタンス。

クラスのコンストラクタ

すべてのクラスのコンストラクタはFunctionクラスである。

console.log("SubSubCls.constructor.name=", SubSubCls.constructor.name);
console.log("SubCls.constructor.name=", SubCls.constructor.name);
console.log("SuperCls.constructor.name=", SuperCls.constructor.name);

実行結果

SubCls.constructor.name= Function
MyCls.constructor.name= Function
SuperCls.constructor.name= Function

これらのクラスがFunctionクラスのインスタンスである事を示している。

console.log(SubCls instanceof Function);    // true
console.log(MyCls instanceof Function); // true
console.log(SuperCls instanceof Function);  // true

そして、組み込みクラスや組み込みクラスを拡張したクラスも含め、 すべてのクラスはFunctionクラスのインスタンスである。

class SubArray extends Array { }

for (let cls of [Array, String, Date, SubArray]) {
    console.log(`${cls.name}.constructor.name=${cls.constructor.name}`);
}

実行結果

Array.constructor.name=Function
String.constructor.name=Function
Date.constructor.name=Function
SubArray.constructor.name=Function
Object.constructor.name=Function
Function.constructor.name=Function

Objectクラスも例外ではない。
驚く事に、FunctionクラスもFunctionクラスのインスタンス。

console.log(Object instanceof Function);    // true
console.log(Function instanceof Function);  // true

クラスの継承関係

クラスも__proto__プロパティを持ち、その値はクラスのスーパークラスを指す

console.log(SubCls.__proto__ === MyCls);    //true
console.log(MyCls.__proto__ === SuperCls);  //true
console.log(SuperCls.__proto__ === Function.prototype); //true
console.log(Function.prototype.__proto__ === Object.prototype); //true
console.log(Object.prototype.__proto__ === null);   //true

上記のコードからわかるように、クラスの__proto__プロパティをたどるとFunction.prototypeにたどり着き、更にその先はObject.prototypeに、そしてその先はnullに。

クラスのprototypeプロパティはクラス自身を指すのでFunction.prototypeのconstructorとObject.prototype.constructorはというと

console.log("Function.prototype.constructor.name=", Function.prototype.constructor.name);
console.log("Object.prototype.constructor.name=", Object.prototype.constructor.name);

実行結果

Function.prototype.constructor.name= Function
Object.prototype.constructor.name= Object

これらの事を考慮して、prototypeプロパティのconstructor.nameからインスタンスの継承をたどると

let s = "";
let c = SubCls;
while (c) {
    if (s) {
        s += "=>"
    }
    if (c === Function.prototype || c === Object.prototype) {
        s += `${c.constructor.name}.prototype`
    } else {
        s += c.name;
    }
    c = c.__proto__;
}
console.log(s);

実行結果

SubCls=>MyCls=>SuperCls=>Function.prototype=>Object.prototype

すべてのクラスのコンストラクタはFunionであるのだが、
これとは別にクラスの継承関係を示す__proto__プロパティはクラスの継承元クラスを指している。
しかし、基底クラス(SuperClsのように他のクラスを継承していないクラス - extendsされていないクラス)においては__proto__プロパティはFunctionやObjectでは無く、
Function.prototypeを指していて、更にその先のSuperCls.__proto__.__proto__はObject.prototypeを指しているという事に注意が必要。

これはクラスの実態はfunctionであって、functionはFunctionクラスのインスタンス。
インスタンスの__proto__はそれが属するクラスのprototypeプロパティを指しているのは前述したとおりで、
クラスはFunctionクラスのインスタンスなので、それが属するクラスすなわちFunctionクラスのインスタンスの__proto__はFunctionクラスのprototypeプロパティを指す。

console.log(SuperCls.__proto__ === Function.prototype); // true

そしてFunctionクラスはObjetクラスを継承していて、FunctionクラスのprototypeプロパティはObjetクラスのインスタンスになる。
そこで、Function.prototypeの__proto__はObject.prototypeを指す事に。

console.log(Function.prototype.__proto__ === Object.prototype); // true

組み込みクラスや組み込みクラスを拡張したクラスも含め、クラスの継承関係を__proto__プロパティをたどって調べてみると

class SimpleCls { }
class SubArray extends Array { }
class SubObject extends Object { }

for (let c of [SimpleCls, Array, String, Date, Function, Object, SubArray, SubObject]) {
    s = "";
    while (c != null) {
        if (s) {
            s += "=>"
        }
        if (c === Function.prototype || c === Object.prototype) {
            s += `${c.constructor.name}.prototype`
        } else {
            s += c.name;
        }
        c = Object.getPrototypeOf(c);   // c = c.__proto__
    }
    console.log(s);

実行結果

SimpleCls=>Function.prototype=>Object.prototype
Array=>Function.prototype=>Object.prototype
String=>Function.prototype=>Object.prototype
Date=>Function.prototype=>Object.prototype
Function=>Function.prototype=>Object.prototype
Object=>Function.prototype=>Object.prototype
SubArray=>Array=>Function.prototype=>Object.prototype
SubObject=>Object=>Function.prototype=>Object.prototype

プロトタイプチェインによるプロパティの探索

プロトタイプチェインによる継承のメカニズムは

(クラスオブジェクトも含めて)オブジェクトのプロパティにアクセスする時、
オブジェクト自身にプロパティが存在しない場合、オブジェクトはそのオブジェクトの__proto__プロパティが保持するプロパティに同名のプロパティが存在すればその値を返す。

オブジェクトがプロパティを検索する手順は、
オブジェクト自身にプロパティが無い場合、__proto__にプロパティが存在しないか探しに行く。
そしてそこにもプロパティが存在しない場合は更にさかのぼって__proto__.__proto__にプロパティが無いかと__proto__がnullになるまで検索する。
nullまでたどり着いた場合はundefinedを返す。

例によってクラスの継承関係がSubCls=>MyCls=>SuperClsである時、

class SuperCls { }
class MyCls extends SuperCls { }
class SubCls extends MyCls { }

SuperClsのprototypeプロパティのメンバーに変数vを追加すると

SuperCls.prototype.v = "vvv";

SubClsクラスのインスタンスからも読込みできることになる。

let subObj = new SubCls();
console.log(subObj.v);  // vvv

それはプロトタイプチェインをたどってSuperCls.prototypeのvにたどりつく事ができるからで、

console.log(subObj.__proto__.__proto__.__proto__.v === SuperCls.prototype.v);   //true

__proto__がnullになるまで検索してもたどりつけない未定義のプロパティはundefinedになる。

console.log(subObj.x);  // undefined

もちろん、SuperClsのprototypeプロパティのメンバーである変数vは、MyClsのインスタンスからもSuperClsのインスタンスからも同様に読み込める。

let myObj = new MyCls();
console.log(myObj.v);   // vvv

let superObj = new SuperCls()
console.log(superObj.v);    // vvv

でもそれはインスタンスがプロパティを保持しているわけではなくSuperCls.prototypeが保持していて

console.log(subObj.hasOwnProperty("v"));    // false
console.log(SuperCls.prototype.hasOwnProperty("v"));    // true

subObjに直接、同名のプロパティを設定するとsubObj自身が別の同名のプロパティを保持するようになり、
SuperCls.prototype.vはsubObjから読込みできなくなる。

subObj.v = "subObj_v"
console.log(subObj.hasOwnProperty("v"));    // true
console.log(subObj.v);  // subObj_v

しかし、そのプロパティが消滅したのでは無くて

console.log(SuperCls.prototype.v);  // vvv

myObjやsuperObjからは依然として読み取り可能。

console.log(myObj.v);   // vvv
console.log(superObj.v);    // vvv

メソッドについても同様でSuperCls.prototypeに定義されたものは

SuperCls.prototype.f = function () {
    console.log("SuperCls.prototype.f");
}

継承したクラスから呼び出し可能。

subObj.f(); // SuperCls.prototype.f

クラスのメンバーは何処に格納されているか

クラスが以下のように定義されているとして

class SuperCls {
    constructor() {
        this.fieldVar = "SuperClsのインスタンスのfieldVarプロパティ"
    }
    method() {
        console.log("SuperClsのメソッド");
    }
}
class MyCls extends SuperCls { }

フィールド変数

クラスのメンバーのうち、フィールド変数はクラスのインスタンスに格納されている。
この例のインスタンス変数fieldVarはSuperClsのインスタンス変数として定義されていて 、 MyClsのインスタンスmyObjのインスタンス変数fieldVarの値は

let myObj = new MyCls()

console.log(myObj.fieldVar);    // SuperClsのインスタンスのfieldVarプロパティ

それにもかかわらず、インスタンス変数fieldVarは
SuperClsを継承したMyClsのインスタンスmyObjに直接,格納されている。

console.log(myObj.hasOwnProperty("fieldVar"));  // true

従ってプロトタイプチェインをたどる必要が無い。

それはインスタンス変数の性質上、インスタンス毎に別の値を保持するために必要な事。

インスタンスメソッド

それに対して、SuperClsのインスタンス変数fieldVarと同様にSuperClsのメンバーであるインスタンスメソッドの場合は、インスタンスmyObjからアクセス可能ではあるが、

myObj.method(); // SuperClsのメソッド

インスタンス変数とは異なり、インスタンスmyObjに格納されているわけでは無く

console.log(myObj.hasOwnProperty("method"));    // false

SuperClsクラスのprototypeプロパティに格納されている。

console.log(SuperCls.prototype.hasOwnProperty("method"));   // true

これは、メソッドの場合はインスタンス変数とは異なり同じメソッドを共有しても問題は起きないので、継承したクラス間で共有される。
従って、メソッドはプロトタイプチェインが利用される事になる。

プロトタイプベースにおけるプロパティの格納先とenumerable属性の値 - 愚鈍人」も参照。

classをfunctionに置き換えると

クラスの定義で

class MyCls extends SuperCls { }

これは以下のコードと等価であって

class MyCls extends SuperCls {
    constructor() {
        super();
    }
}

前項でのクラスの定義を

class SuperCls {
    constructor() {
        this.fieldVar = "SuperClsのインスタンスのfieldVarプロパティ"
    }
    method() {
        console.log("SuperClsのメソッド");
    }
}
class MyCls extends SuperCls { }

これまでみてきた事をもとに従来のfunctionによるクラスの定義に書き換えると、
プロトタイプチェインを実装するためのプロトタイプの連結は、以下のコードのようになる事が推察される。

function SuperCls() {
    this.foo = "SuperClsのインスタンスのfooプロパティ"
}
SuperCls.prototype.method=function(){
    console.log("SuperClsのメソッド");
}
function MyCls() {
    SuperCls.call(this);
}
MyCls.prototype = new SuperCls();
MyCls.prototype.constructor = MyCls;
delete MyCls.prototype.foo;

参考までに

ページのトップへ戻る