JavaScript入門 クラス プロトタイプ
プロトタイプ
ES5でも関数を使えば、クラスと同等の機能を実装出来ます。ただし、現在はこの方法は実装することは基本的にありません。この仕組みを学ぶとJavaScript言語仕様を理解するうえで非常に役立ちます。というもの、JavaScriptはプロトタイプベース言語と呼ばれ、言語仕様の根底には関数と密接に関わるプロトタイプ(prototype)と呼ばれる仕組みがあります。そして、ES6から仕様追加されたクラスも、裏側で動いている仕組みはプロトタイプであるためです。
コンストラクタ関数
ES5のバージョンまでは、JavaScriptのオブジェクトはコンストラクタ関数とnew演算子を使って生成していました。コンストラクタ関数とは、class内で使用するコンストラクタ(constructor)と同様の動きをする関数です。
構文:コンストラクタ関数の定義
function FunctionName( [引数1,引数2,・・・] ){
this.プロパティ名 = 値;
}
コンストラクタ関数の関数名(FunctionName)には、一般的な関数と区別するためにパスカルケースを使います。コンストラクタ関数のthisは、クラスのコンストラクタ内のthisと同様に、生成されるオブジェクトのインスタンスを参照します。また、インスタンス化を行うときも、クラスと同様にnew演算子を使います。
同じオブジェクト(obj)がコンストラクタ関数とクラスで生成されています。このように本質的にはクラスのコンストラクタもコンストラクタ関数も同じです。そのため、JavaScriptでは、「オブジェクトを生成するもの」という意味で、クラスとコンストラクタ関数をまとめてコンストラクタと呼ぶことがあります。
JavaScriptのオブジェクト
JavaScriptでオブジェクトと言う場合、以下の2種類の意味に分類できます。
Objectコンストラクタのインスタンス
1つ目は、Objectコンストラクタのインスタンスによって生成されるインスタンスのことです。なお、オブジェクトリテラル{ }は、Objectコンストラクタのインスタンス化(new Object)を簡略化して記述できるようにしたものなので、これもObjectコンストラクタのインスタンスになります。
{}はnew Objectと同様
console.log( {} instanceof Object );
>true;
非プリミティブ型を表すオブジェクト
2つ目は、文字列や数値などのデータ型の視点でみたときに非プリミティブ型に分類される値(オブジェクト)のことです。コンストラクタによって生成されるインスタンスは、すべて非プリミティブ型に分類されます。プリミティブ型以外はすべて非プリミティブ型に分類されるため、関数や配列もオブジェクトになります。そのため、厳密にはコンストラクタ関数も「関数」であるため、オブジェクトです。
プロトタイプとは?
特徴1:プロトタイプ(prototype)は関数オブジェクトに保持される特別なプロパティ
関数もオブジェクトの一種なので、関数にもプロパティを保持できます。プロトタイプ(prototype)は、インスタンス化に関係する特別なプロパティとして、関数オブジェクトに保持されています。
プロトタイプは関数に自動的に設定されているプロパティ
function Test(){}
//関数はオブジェクトの一種
Test.prop = "値"; //Test関数はオブジェクトなので、プロパティに値を設定できる
console.log( Test.prop );
> 値
//prototypeプロパティの存在確認
console.log( "prototype" in Test );
>true //関数を定義するとprototypeプロパティが自動的に設定される
上記のコードでは、console.log( “prototype” in Test);の結果としてtrueが返されているため、Testコンストラクタ関数にprototypeプロパティが存在することを確認できます。このprototypeプロパティは、関数を定義したときに自動的に設定されます。また、このprototypeに設定されている値はオブジェクトになります。
prototypeにはオブジェクトが格納されている
function Test(){ }
console.log( typeof Test.prototype );
> object
特徴2:prototypeオブジェクトには関数(メソッド)を格納する
prototypeオブジェクト(prototypeプロパティに設定されているオブジェクト)に登録された関数は、インスタンスから実行可能なメソッドになります。
function Person( name ){
this.name = name;
}
Person.prototype.hello = function(){ //Personコンストラクタのprototypeオブジェクトのhellプロパティに無名関数を登録
console.log( `こんにちは、${ this.name }`);
}
const taro = new Person("山田太郎");
taro.hello(); //helloメソッド実行
>こんにちは山田太郎
const hanako = new Person("山田花子");
hanako.hello();
>こんにちは山田花子
上記のコードでは、taro.hello();としたときに、prototypeに登録したhello関数で使われるthisが呼び出し元オブジェクト(taro)を参照してします。そのため、prototypeに登録される関数は、クラスのメソッドと同様に扱われることがわかります。
特徴3:prototypeはインスタンス化の際に__proto__にコピーされる
new演算子によってコンストラクタからインスタンスを作成するとき、コンストラクタ関数のprototypeプロパティに格納されているオブジェクトへの参照が、インスタンスの__proto__という特別なプロパティにコピーされます。
prototypeと__proto__は同じオブジェクトを保持する
function Test() { }
Test.prototype.hello = function() { console.leg("こんにちは") };
const instance = new Test;
console.log( instance.__proto__ === Test.prototype ); //__proto__とprototypeは同じオブジェクト
>true
instance.__proto_.hello(); //__proto__を通してメソッドを実行
>こんにちは
prototypeはコンストラクタ関数のプロパティですが、__proto__はコンストラクタ関数のprototypeオブジェクトへの参照が保持されるインスタンスのプロパティです。インスタンス化されたオブジェクトから、__proto__を通してprototypeに登録した関数を実行できます。
特徴4:__proto__は省略することが可能
__proto__は、記述を省略できます。そのため、一般的にはインスタンスのメソッドの実行時は、__proto__は記述しません。
function Test(){ }
Test.prototype.hello = function() { console.log( "こんにちは" ) };
const instance = new Test;
console.log( instance.__proto__.hello === instance.hello ); //__proto__の関数と一致
>true
instance.hello(); //__proto__は省略可能
>こんにちは
クラス記法とプロトタイプ
クラス記法を使った場合でも、裏側で動作する仕組みはプロトタイプベースです。そのため、クラスで定義したメソッドの場合も同様に、__proto__を通してメソッドが実行されます。
class Test{
hello(){ console.log( "こんにちは" ) };
}
const instance = new Test;
Test.prototype.hello();
>こんにちは
instance.__proto__.hello();
>こんにちは
instance.hello();
>こんにちは
また、クラスで定義したメソッドは、prototypeを通して上書きすることもできます。
クラスのメソッドをprototypeから変更
class Test{
hello() { console.log( "こんにちは" ); }
}
const instance = new Test;
Test.prototype.hello = function() { console.log( "hello" ) } //prototypeのhelloメソッドを上書き
instance.hello();
>Hello
prototypeと__proto__は同じオブジェクトへの参照を保持しているため、prototypeのメソッドに対して行った変更はすべてのインスタンスに反映されます。
このように、クラス記法を使った場合でも、JavaScriptのオブジェクトはプロトタイプの仕組みの上で動作しています。
プロトタイプチェーン
プロトタイプが他階層になる場合について説明します。プロトタイプが他階層になっているときの挙動は次の3つです。
- ほぼすべてのオブジェクトは、__proto__という特殊なプロパティを保持します。(例外的に保持しないケースはあります)
- オブジェクトのプロパティを参照するとき、オブジェクト内にプロパティが見つからなければ、暗黙的に__proto__オブジェクト内のプロパティやメソッドを探しにいきます。(そのため、__proto__を省略して__proto__のプロパティやメソッドにアクセスできます。)
- __proto__にも一致するプロパティが見つからなかった場合は、さらに__proto__のオブジェクトが持つ__proto__に一致するプロパティを探しにいくことになります。そのため、次のコードのように__proto__が他階層に連なっている状態の場合でも、obj.hello()と記述してメソッドを実行すれば、obj.__proto__.__proto__.hello();が実行されることになります。
__proto__が他階層になっている場合
const obj = {
__proto__:{
--proto__: {
hello(){
console.log( "こんにちは" );
}
},
},
};
obj.hello(); //obj.__proto__.__proto__.hello();が実行される
>こんにちは
この場合も、JavaScriptエンジンはメソッドが見つかるまで、__proto__をどんどんさかのぼっていき、最初に見つかったメソッドを実行します。このように、__proto__が連なっている状態をプロトタイプチェーンと呼びます。
- ❶自身のプロパティとしてhelloが存在するか確認して、あればそれを実行し、無ければ❷に進みます。
- ❷__proto__内にhelloがあるか確認します。あればそれを実行し、無ければ➌に進みます。
- helloが存在するため、helloを実行します。そのとき、仮にholloが存在しない、かつ__proto__が存在しない場合、または__proto__にnullが設定されている場合には、そこで探索は終了します。このときにundefinedが結果として返るため、関数としては実行するとエラーになります。
これが、プロトタイプチェーンの探索プロセスです。なお、先ほどのコードでは、プロトタイプチェーンを作成するために、オブジェクトにたいして直接__proto__を設定していますが、この方法は推奨されません。プロトタイプチェーンを作成するには、クラスの継承を行うか、もしくはObject.createメソッドなどを使って__proto__を含むオブジェクトを作成します。
構文:Object.createの使用方法
const resObj = Object.create( protoObj );
resObj:Object.createの引数に指定したオブジェクト(protoObj)への参照を__proto__に格納した空のオブジェクトが作成されます。仮にprotoObjが{ hello: function() {} }のようなオブジェクトの場合、Object.create( protoObj )を実行すると、resObjは次のようなオブジェクトとして生成されます。
{
__proto__: { hello: function() {} }
}
protoObj:__proto__の参照先として設定したいオブジェクトを渡します。
このObject.createを使うと、引数に渡したオブジェクトが戻り値の__proto__に格納されるので、次のようにプロトタイプチェーンを作成できます。
const fruit1 = {
apple: function () {
console.log("リンゴ");
},
};
//fruit1が__proto__に設定された新しいオブジェクトをfruit2に代入
const fruit2 = Object.create(fruit1);
//fruit2にbananaを追加
fruit2.banana = function (){
console.log("バナナ");
}
//fruit2が__proto_に設定された新しいオブジェクトをfruit3に代入
const fruit3 = Object.create(fruit2);
//fruit3にmelonを追加
fruit3.melon = function () {
console.log("メロン");
}
fruit3.apple(); //fruit3からプロトタイプチェーンに存在するメソッドを実行
>リンゴ
fruit3.banana(); //fruit3からプロトタイプチェーンに存在するメソッドを実行
>バナナ
fruit3.melon();
>メロン
fruit1をfruit2の__proto__に設定し、さらにfruit2をfruit3の__proto__に設定しています。そのため、fruit3のオブジェクトをconsole.log( fruit3 );と記述コンソールで確認できます。
そのため、fruit3の__proto__をたどると、bananaやappleが見つかるため、fruit3.banana()、fruit3.apple()のメソッドを実行できます。
プロタイプ継承
他のオブジェクトの機能を引き継ぐことを継承と呼び、他のコンストラクタのprototypeを継承することをプロトタイプ継承と呼びます。次の例では、Parentコンストラクタのprototypeを、Childコンストラクタで継承しています。
//親のコンストラクタの宣言
function Parent() { }
//子のコンストラクタを宣言
function Child(){ }
//親のプロトタイプにメソッドを追加
Parent.prototype.parentMethod = function() {
console.log("親のメソッド");
}
//親のプロトタイプを継承
Child.prototype = Object.create( Parent.prototype ); //※1
//子のプロトタイプにメソッドを追加
Child.prototype.childMethod = function(){ //※2
console.log("子のメソッド");
}
//インスタンス化
const childObj = new Child;
childObj.parentMethod();
>親のメソッド
childObj.childMethod();
>子のメソッド
上記コードでは、※1の時点で親のprototypeを子のプロトタイプに継承しています。このときのchild.prototypeの状態は、{ __proto__: { parentMethod() } }です。そのため、ここにchildMethodを追加(※2)、インスタンス化を行った場合、Child.prototypeは以下の図のような構造になります。
このように、親のメソッドがプロタイプチェーン上に存在することになります。余談ですが、上記のコードでは、親コンストラクタのprototypeを継承しているだけで、親コンストラクタのプロパティまでは継承していません。親コンストラクタのプロパティを子コンストラクタで継承するには、次のようにします。
親のプロパティを継承
function Parent( parentProp ){
this.parentProp = parentProp;
}
function Child( childProp, parentProp ){
Parent.call( this, parentProp ); //親コンストラクタをthisを束縛して実行
this.childProp = childProp;
}
const child = new Child( "子プロパティ","親プロパティ" );
console.log( child );
>{ parentProp: "親プロパティ", childProp: "子プロパティ" }
このコードでは、Childコンストラクタの中でParentコンストラクタをcallメソッドで実行することで、Parentコンストラクタのプロパティを継承しています。callで実行しているのは、ParentとChildのthisが参照する先のオブジェクトを一致させるためです。
thisは実行コンテキストによって参照先が変わるため、単にParent( parentProp )と実行すると、thisの参照先がChildのコンテキスト異なるオブジェクトになってしまいます。そんとあめ、callを使ってthisの参照先をChildと同じオブジェクトにしています。なお、現在のJavaScriptでは、callを使った記法は使いません。
プロトタイプチェーンの終端
「Objectクラスは、すべてのクラスを継承しているクラス」ですが、これはプロトタイプを継承していることを表してます。一部の例外を除き、プロトタイプチェーンの終端は、Objectコンストラクタのプロトタイプ(Object.prototype)になります。これは、クラスやコンストラクタ関数で他のプロトタイプを継承しなかった場合には、Objectコンストラクタのprototypeが自動的に継承されるためです。そのため、基本的にすべてのオブジェクトは、Objectコンストラクタのメソッドを使うことができる状態になっています。
const obj = new function(){ }; //無名関数をコンストラクタとして使用
console.log( obj.__proto__ ); //プロトタイプの中身を確認
なお、Objectコンストラクタのプロトタイプを継承したくない場合は、次のようにします
const obj = Object.create( null );
hasOwnPropertyメソッドとin演算子の仕組み
hasOwnPropertyメソッドとin演算子のプロトタイプベースの挙動の違いは以下の通りです。
hasOwnPropertyメソッド
自身のオブジェクトのプロパティとして存在するかどうかを確認します。
in演算子
プロトタイプチェーンまで含めてプロパティが存在するかを確認します。