【JavaScript】 プロトタイプとは?prototypeプロパティはプロトタイプではない件について
更新日:2020/03/04
JavaScriptはプロトタイプベースな言語だそうです。
そこで今回は、プロトタイプについて解説してみます。
プロトタイプの基礎
JavaScriptは本当のプロトタイプベースではないとかいろいろ議論があるようです。
そんなことは気にせず、ここではJavaScriptのプロトタイプの簡単な利用法と、メリットをお伝えします。
プロトタイプとは?
プロトタイプとは、JavaScriptの継承を実現するために欠かすことのできない仕組みです。
次のコードは、プロトタイプの簡単な例です。
// (1) コンストラクターを定義
const a = function() {
this.x = 100;
this.func1 = function () {
console.log( this.x );
}
};
// (2)プロトタイプを設定
a.prototype = {
test : function(){ console.log( this.x * 2 );},
test2 : function(){ console.log( this.x * 3 );},
};
// (3)コンストラクターからオブジェクト(インスタンス)を作成
const b = new a;
b.func1(); // 100 ← オブジェクトが持つメソッドが実行された
b.test(); // 200 ← プロトタイプが持つメソッドが実行された
(1)はaという名前のコンストラクターを定義しています。
(2)は、aが元から持っているprototypeプロパティに、test()メソッドとtest2()メソッドを追加しています。
(3)はコンストラクタaのインスタンスとしてbという名前のオブジェクトを作成しています。
bはコンストラクタaのコードにより、次のように x と func1 をメンバーとして所持しています。
b = { x : 100 , func1 : function () {...省略} };
このとき new演算子により、b はaが持つ"prototype"プロパティを継承します。
具体的にはbのメンバーとしてa.prototypeへの参照がセットされ、次のような構造になっています。
b = {
x : 100 ,
func1 : function () {...省略} ,
プロトタイプへの参照 : a.prototype {
test : function () {...省略},
test : function () {...省略},
},
};
ここでbのfunc1()を実行してみます。
このメソッドはbのメンバーなので、内部コードが普通に実行されます。
console.log( this.x ) // this.x = 100
100とコンソールに出力されました!
次にtest()というメソッドを実行します。
しかしb内に、testは定義されていません。
b = { x : 100 , func1 : function () {...省略} , プロトタイプへの参照 }; ← test はない!!
そこでプロトタイプへの参照から探します。
b[プロトタイプへの参照] → a.prototype { test : function () {...省略} };
プロトタイプへの参照はa.prototypeを指しており、そこにはtestという名前のメソッドがあります。
メソッドを見つけることができたので、その内部コードを実行します。
console.log( this.x * 2 ); // this.x = 100
200とコンソールに出力されました!
このようにプロパティが見つからないときは、暗黙的にプロトタイプが検索されます。
参考:【JavaScript】 コンストラクターとは?関数とは違うのか?
プロトタイプのメリット
プロトタイプのメリットは、静的なメソッドや変数を定義できることです。
次の例を見てください。
const a = function() {
this.x = 100;
this.func1 = function () {
console.log( this.x );
}
};
const a1 = new a; // a1 { x : 100 , func1 : function(){ } }
const a2 = new a; // a2 { x : 100 , func1 : function(){ } }
const a3 = new a; // a3 { x : 100 , func1 : function(){ } }
new式で、同じコンストラクターから3つのオブジェクトを作成しています。
このときコンピューターの内部コードでは、毎回プロパティxとfun1に値をセットしています。
xは変数として使用し値が変更される可能性があります。
しかしfunc1はメソッドとして使用し、今後値が変更される可能性がないという状況だとします。
このとき、インスタンス作成時に毎回func1というプロパティを作成し、そのプロパティにメソッドをセットするのは、いささか非効率です。
そこでプロトタイプにすることで、個別に作成するのを防ぐことができます。
const a = function() {
this.x = 100;
};
a.prototype.func1 = function () {
console.log( this.x );
}
プログラムコード全体でfunc1というプロパティが作成され、そのプロパティにメソッドをセットされるのは一回だけです。
それなのに、aのインスタンスからfunc1を呼び出すことができます。
効率がいいですね。
ただし、インスタンス内にプロパティがあるかどうか確認したあとプロトタイプ内を確認するという手間が増えるため、最初の例と比較して時間がかかります。
ほとんどのケースでは気にする必要がありませんが、数万回呼び出すなどの状況が想定されるときは、プロトタイプを使用しないことを検討してみてください。
prototypeプロパティはプロトタイプではない
prototypeプロパティはプロトタイプではありません。
なんだか全て否定された気分です
めちゃくちゃ重要な話です。
prototypeなのにエラーがでる
次の例を見てください。
const a = function() {
this.x = 100;
};
a.prototype.test = function(){console.log( this.x );};
const b = new a;
b.test(); // 100 ← ok
a.test(); // <span class="red">TypeError: a.test is not a function</span> (aはファンクションじゃないよ!)
a のインスタンスである b からは、 testメソッドを呼ぶことができます。
ですが、a 自身から testメソッドを呼ぶことができないのです。
これは、僕がプロトタイプを理解していない頃に、はまったコードです。
「prototypeプロパティ内を検索して、test()メソッドを実行してくれるはず」と思ったのです。
同様に、次のコードもエラーです。
b.prototype.test2 = function(){console.log( this.x * 3 );};
b.test2(); // TypeError: a.test is not a function
bにプロトタイプを追加しようと思っても、うまくいきません。
なんで、なんで、なんでーー???
叫びたくなります。
その答えは、
「prototypeプロパティはプロトタイプではない」
ということなんです。
プロトタイプの正体は[[Prototype]]
オブジェクトは[[Prototype]]という内部プロパティを持っています。
これが何度か出てきた、プロトタイプへの参照です。
[[Prototype]]は内部プロパティですが、ブラウザの開発ツールで確認することができます。
[Google Chrome]
Google Chromeは、__proto__ として表示されています。
[Google Firefox]
Firefoxは、<prototype> として表示されています。
それとは別に、prototypeプロパティが表示されているのがわかるでしょうか。
a.prototype.test = function(){console.log( this.x );}; は、こちらにセットされているわけです。
a.test(); とすると、__proto__や<prototype>を探しにいきます。
prototypeプロパティは見に行きません。
結果 test というメソッドが見つからず、「testはメソッドじゃないよ!」とエラーになってしまうのです。
もし実行したいなら、次のように記述しましょう。
a.prototype.test();
newをすると何がおきる?
コンストラクター関数からインスタンスを作成するとき、new式を使用します。
new式を使わなくても、オブジェクトは作成できるという意見があります。
確かにオブジェクトは作成できますが、インスタンスは作成できません。
JavaScriptのインスタンスとは、元となるコンストラクター関数からprototypeプロパティを継承したものです。
new式は、[[Prototype]]にprototypeプロパティへの参照をセットしています。
b[[Prototype]] に a.prototypeへの参照がセットされる。
この動作により、JavaScriptは継承機能を実現しているのです。
aも[[Prototype]]を持っている
const b = new a; をしたとき、b[[Prototype]] に a.prototypeへの参照がセットされます。
しかし、a自身も[[Prototype]]を持っています。
a{ prototype : { メソッドやプロパティ }, [[Prototype]] : { メソッドやプロパティ } }
しかしbがプロトタイプとして参照するのは、a.prototypeです。
そのため、a.prototypeにメソッドを追加すると、bから実行することができます。
Object.create() メソッド
Object.create() メソッドは新しいオブジェクトを作成するメソッドですが、引数として与えられたオブジェクトを[[Prototype]]にセットします。
const a = {
test : "hello",
f1:function () {
console.log(this.test);
}
};
const b = Object.create( a );
bの内容は次のようになっています。
b{ [[Prototype]] : { test : "hello", f1:function () { console.log(this.test); } }
オブジェクトaがそのまま、プロトタイプとしてセットされていますね。
b.f1() を実行すると、"hello"と表示してくれます。
ではnewをしたとき、どうなるのでしょうか?
const a = function() {
this.x = 100;
};
a.prototype = {
test : "hello",
f1:function () {
console.log(this.test);
}
};
const b = new a;
内部的に、次のコードと同じことがおこなわれたあと、コンストラクターの処理が実行されます。
this = Object.create( a.prototype );
そのため、結果が次のようになります。
b{ x : 100, [[Prototype]] : { test : "hello", f1:function () { console.log(this.test); } }
プロトタイプチェーン
メソッドやプロパティを探す仕組みは、プロトタイプを鎖状にたどっていくことからプロトタイプチェーンと呼ばれています。
ですが今までの例だと、一段階たどったくらいでチェーンとか大げさですよね。
実は今までの例は2段階のプロトタイプチェーンが構築されています。
オブジェクトはObjectから作成されている
オブジェクトはリテラル( { } )やObjectコンストラクターから作成されます。
この時、Object.prototypeを[[Prototype]]に取り込んでいます。
const a = { hello : console.log( "hello" ) };
上のコードを実行すると、aの内容は次のようになるわけです。
a { hello : console.log( "hello" ), [[Prototype]] : Object.prototype{ __defineGetter__: function(){}, __defineSetter__: function(){}, ・・・・・ } }
この時点でプロトタイプチェーンが構築されているのがわかりますでしょうか。
aは [[Prototype]]を介して、Object.prototypeを呼び出すことが可能になっていますね。
あまり使うことがありませんが、Object.prototypeには次のようなメソッドが定義されています。
Object.prototype.hasOwnProperty() : オブジェクト自身のプロパティか判定する
Object.prototype.valueOf() : オブジェクトのプリミティブ値またはオブジェクト自身を返す
Object.prototype.toString() : オブジェクトのタイプを文字列で返す
prototypeプロパティにセットしても同じ
ではprototypeプロパティにメソッドをセットした場合はどうでしょうか?
const a = function() {
this.x = 100;
};
a.prototype.test = function(){console.log( this.x );};
a.prototypeは、初期値として空のオブジェクトがセットされています。
空という表現をしていますが、実際にはObject.prototypeへのチェーンが作成されています。
そこにtestメソッドをセットしているので、次のようになっています。
a.prototype { test : console.log( this.x ), [[Prototype]] : Object.prototype{ __defineGetter__: function(){}, __defineSetter__: function(){}, ・・・・・ } }
a.prototypeが [[Prototype]]を介して、Object.prototypeを呼び出すことが可能になっていますね。
newをすると…
newで新しいオブジェクトを作成します。
const a = function() {
this.x = 100;
};
a.prototype.test = function(){console.log( this.x );};
const b = new a;
この結果、bは次のような構造になっています。
b{ x : 100. [[Prototype]] : a.prototype{ test : console.log( this.x ), [[Prototype]] : Object.prototype{ __defineGetter__: function(){}, __defineSetter__: function(){}, ・・・・・ } } }
[[Prototype]]が入れ子になっているのがわかりますね。
このプロトタイプの入れ子をたどって、メソッドやプロパティが検索されていくのです。
プロトタイプで継承みたいなことをやってみる
プロトタイプを使うと、継承のようなことができます。
次のような親クラスオブジェクトを用意します。
親クラスオブジェクト
const a = function() {
this.x = 100;
};
a.prototype.test = function(){ console.log( this.x ); };
次に子クラスオブジェクトです。
子クラスオブジェクト
const b = function() {
this.x = 200;
};
b.prototype = new a();
aのインスタンスをb.prototypeにセットすることで、aの持つプロトタイプだけでなく、メソッドやプロパティも取り込むことができます。
b.prototype{ x : 100, [[Prototype]] : a.prototype { test: function(){ } [[Prototype]] : Object.prototype{・・・・} } }
この状態でnewをすると、新しいオブジェクトの[[Prototype]]にセットされます。
const c = new b;
c{ x : 200, [[Prototype]] : b.prototype { x : 100, [[Prototype]] : a.prototype { test: function(){ } [[Prototype]] : Object.prototype{・・・・} } } }
test()メソッドを実行してみます。
c.test(); // 200
[[Prototype]] → [[Prototype]] とたどって、test()メソッドを探し当てることができました。
test()メソッドのコードは、次のようになっています。
console.log( this.x );
this.xがあるので、xプロパティを探します。
すぐに見つかりました。
値は200です。
xは親で100と定義されています。
それを200としてオーバーライドできました。
メソッドも同様に、オーバーライドできます。
親のメソッドやプロパティを呼び出すときは、次のようにします。
a.prototype.x
これで、継承っぽいことができますね。
とはいえJavaScriptはclass定義ができるので、継承するならclassを使った方がよさそうです。
class定義については、次のページをご覧ください。
■【JavaScript】 JSにおけるクラスとは?正体についても調べてみた
[[Prototype]]にコード上からアクセスする
[[Prototype]]は内部的なプロパティで、直接アクセスすることができません。
しかしいくつかの方法で、アクセスすることができます。
__proto__プロパティでアクセス
ブラウザ―で動作しているJavaScriptの場合__proto__プロパティで、[[Prototype]]にアクセスすることができます。
プロトタイプを取得
const a = function() {
this.x = 100;
};
a.prototype.test = function(){console.log( this.x );};
const b = new a;
console.log( b.__proto__); // Object { test: test(), … }
追加もできます。
プロトタイプをセット
b.__proto__.hello = function(){ console.log( "hello"); };
console.log( b.__proto__); // Object { test: test(), hello: hello(), … }
console.log( a.prototype); // Object { test: test(), hello: hello(), … }
b.__proto__は 元となったprototypeプロパティへの参照なので、a.prototypeを見ると同じ内容が表示されます。
__proto__はブラウザが独自に実装していたもので、JavaScriptの標準的な仕様ではありませんでした。
しかし ECMAScript(JavaScriptの実質的な仕様書)の2015年版で、ブラウザ限定で標準化されました。
内部的には、次で紹介するgetPrototypeOf/setPrototypeOfと同じ処理をおこなっています。
getPrototypeOf/setPrototypeOf でアクセス
__proto__よりも問題が少ないとされているのが、Object.getPrototypeOf( ) および Object.setPrototypeOf( ) メソッドです。
プロトタイプの取得は、Object.getPrototypeOf( 対象オブジェクト )を使用します。
プロトタイプを取得
const a = function() {
this.x = 100;
};
a.prototype.test = function(){console.log( this.x );};
const b = new a;
console.log( Object.getPrototypeOf( b ) ); // Object { test: test(), … }
プロトタイプのセットは、Object.getPrototypeOf( 対象オブジェクト , プロトタイプと置き換えるオブジェクト )を使用します。
プロトタイプをセット
const b = new a;
const c = Object.getPrototypeOf( b ) ; // 元となるプロトタイプを取得
c.hello = function(){console.log( "hello" ); }; // メソッド追加
Object.setPrototypeOf(b , c); // プロトタイプとしてセット
console.log( Object.getPrototypeOf( b ) ); // Object { test: test(), hello: hello(), … }
プロトタイプを変更すると重くなる
JavaScriptのプロトタイプ環境は高度に最適化されているため、後から変更するとブラウザの処理が重くなることがあるようです。
正直、後から__proto__などでプロトタイプを変更する場面が想像できないのですが、もしやるとしたら速度的な検証も必要ですね。
まとめ
プロトタイプについてウダウダと書きました…
プロトタイプがチェーンするってことがわかればいい気がします。
更新日:2020/03/04
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。