クラスの継承のメカニズム - プロトタイプチェイン
《 初回公開: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;
参考までに