さぁ!検索しよう!

はじめに

オブジェクト指向とは例えばゲームを作る際に、クラスという設計図を基にさまざまな役割・機能を持った実体(オブジェクト又はインスタンス)をいくつか作り、最終的にそれらが合わさってゲームが完成するという考え方をいいます。

上記の通り、オブジェクト指向でのプログラミングは、クラスを作りそのクラスをインスタンス化することで、オブジェクトが生成されます。

function Neko(s,c){//クラス
	this.size=s;//プロパティ
	this.color=c;
	this.appear=function(){//メソッド
		console.log(this.size+this.color+'の猫が現れた。');
	}
	this.leave=function(){
    		console.log(this.size+this.color+'の猫が去った。');
	}
}
var sironeko = new Neko('小さな','白色');//インスタンス化しオブジェクトsironekoが生成される。
sironeko.appear();

var kuroneko = new Neko('大きな','黒色');
kuroneko.leave();

インスタンスに新たにメソッドを追加した場合、クラスにも追加されるということはなく、その追加されたインスタンスのみ有効なメソッドとなります。

なので、下記のインスタンスsironekoへ新たに追加したleaveメソッドはあくまでsironekoのみが持つメソッドであって、その後新たに生成されたインスタンスkuronekoleaveを共有する(使う)ことができません。

function Neko(s,c){//クラス
	this.size=s;
	this.color=c;
	this.appear=function(){//メソッド
		console.log(this.size+this.color+'の猫が現れた。');
	}
}
var sironeko = new Neko('小さな','白色');//インスタンス化しオブジェクトsironekoが生成される。
sironeko.leave=function(){
	console.log(this.size+this.color+'の猫が去った。');
}
sironeko.leave();//小さな白色の猫が去った。
var kuroneko = new Neko('大きな','黒色');
kuroneko.leave();//Uncaught TypeError: kuroneko.leave is not a function

このように、クラスとインスタンスによるオブジェクト指向をクラスベースといいます。

しかし、JavaScriptはクラスベースでのオブジェクト指向は推奨されていません。

なぜなら、JavaScriptではnewによってコンストラクタ(クラス)をインスタンス化する度に、下記のようにコンストラクタ内にメソッド(関数)appear,leaveが定義されていることによって、コンストラクタ内でメソッドが毎回新しく、また使わないもの(ここではleave)まで無駄に定義されてしまい、それに加えてクラス内にそれぞれのメソッドの前後にvar this = {}とreturn this;が内部的に定義されてしまう訳です。

function Neko(s,c){//クラス(コンストラクタ)
	this.size=s;//プロパティ
	this.color=c;//プロパティ
	//var this={};
	this.appear=function(){//メソッド
		console.log(this.size+this.color+'の猫が現れた。');
	}
	//return this;
	//var this={};
	this.leave=function(){//メソッド
    		console.log(this.size+this.color+'の猫が去った。')
	}
	//return this;
}
var sironeko = new Neko('大きい','白色');
//この時点でコンストラクタ内でvar this={};→関数this.appear=function(){}→return this;の順で新しく定義される。
sironeko.appear();
var kuroneko = new Neko('小さい','黒色');
//この時点でコンストラクタ内でvar this={};→関数this.appear=function(){}→return this;の順で新しく定義される。
kuroneko.appear();

これでは、無駄にメモリが確保されてしまうため、パフォーマンスの低下に繋がります。

そこで代わりにプロトタイプベースよ呼ばれるオブジェクト指向の出番です。

プロトタイプベース

JavaScriptのオブジェクト指向にはクラスの概念は存在せず、オブジェクトから新たにオブジェクトを生成するプロトタイプベースのオブジェクト指向となります。

下記は先程のクラスベースによるコードをプロトタイプベースで書き直したものです。

function Neko(s,c){//コンストラクタ内はメンバ変数のみを記述する。そうすれば先程のようにvar this={};,return this;が追加されず、メモリが確保されないから。メンバ変数はvar this={};,return this;は追加されない。
	//全てメンバ変数
	this.size=s;
	this.color=c;
}
Neko.prototype.appear=function(){
	console.log(this.sise+this.color+'の猫が現れた。');
}
Neko.prototype.leave=function(){
	console.log(this.size+this.color+'の猫が去った。');
}
var sironeko = new Neko('小さな','白色');
sironeko.appear();
var toraneko = new Neko('やや小さな','茶色');
toraneko.appear();
var kuroneko = new Neko('大きな','黒色');
kuroneko.leave();
var mikeneko = new Neko('やや大きな','三色');
mikeneko.leave();

このようにプロトタイプベースの記述では、メソッドを別のオブジェクトとして定義しているため、インスタンス化した場合でもクラスベースのような無駄に関数が新たに定義されるといったことは起こらないのです。

また、上記のコード内にprototypeというものが記述されていますが、これを使うことでプロトタイプベースによるオブジェクト指向が実現できているのです。

プロトタイプ

プロトタイプ(prototype)とはコンストラクタ(関数)のプロパティであり、インスタンス化された各オブジェクトが共通で使う関数を定義する際に使います。

prototypeは下記のようにコンストラクタが定義されると目には見えませんが内部で生成されているのです。

function Hito(n) {
	this.name=n;
} 
Hito.prototype//内部で生成される

このHito.prototypeプロパティに下記のようにメソッド(関数)を持たせることで、インスタンス化された各オブジェクトが共通で使う関数を定義することができます。この関数をプロトタイプオブジェクトを呼びます。

プロトタイプオブジェクト内のプロパティ(下記ではconsole.log('私の名前は'+n+'です。');)は、同コンストラクタ(Hito(n))から派生されたインスタンスたちへ継承されます。

function Hito(n) {
	this.name=n;
} 
Hito.prototype.introduction=function(){
	console.log('私の名前は'+this.name+'です。');
}
var taro=new Hito('太朗');
taro.introduction();

このとき、Hito(n){}は子、Hito.prototype.introduction()は親という関係になります。

そして、子であるHito(n){}は親であるHito.prototype.introduction=function(){}内のプロパティを使うことができます。

インスタンス側でプロトタイプオブジェクトのプロパティの上書き

また、インスタンス側でプロトタイプオブジェクトのプロパティを後から上書きが可能です。このとき、上書きは指定したインスタンス(taro)のみ適用され、他の同コンストラクタから派生されたインスタンスには影響を及ぼしません。

function Hito(n) {
	this.name=n;
} 
Hito.prototype.introduction=function(){
	console.log('私の名前は'+this.name+'です。');
}
var taro=new Hito('太朗');
var jiro=new Hito('次朗');
taro.introduction();//私の名前は太朗です。
jiro.introduction();//私の名前は次朗です。
taro.introduction=function(){
	console.log('俺の名前は'+this.name+'だ。');
}
taro.introduction();//俺の名前は太朗だ。←上書きされる。
jiro.introduction();//私の名前は次朗です。←上書きされない。

インスタンス生成後にプロトタイプオブジェクトを新たに追加

(先程のプロパティの上書きとは別に、)インスタンスの生成後に新たにプロトタイプオブジェクトを定義した場合でも、生成後のインスタンスはそれを認識します。

function Hito(n) {
	this.name=n;
} 
Hito.prototype.introduction=function(){
	console.log('私の名前は'+this.name+'です。');
}
var taro=new Hito('太朗');
Hito.prototype.sex='男';
Hito.prototype.introduction2=function(){
  console.log('性別は'+taro.sex+'です。')
}
taro.introduction();
taro.introduction2();

プロトタイプチェーン

JavaScriptでは、インスタンスから(の)プロパティ(メンバ変数)やメソッドが呼ばれた(参照された)場合、下記の順で
該当のものが存在するか確認していきます。

  1. インスタンス(コンストラクタ)自身
  2. そのインスタンスのプロトタイプ
  3. そのインスタンスの他のプロトタイプ

このように辿っていく仕組みをプロトタイプチェーンといいます。

プロトタイプオブジェクト同士の継承を行なうことができ、これをプロトタイプチェーンと呼びます。

function Hito(n) {
	this.name=n;
} 
Hito.prototype.introduction=function(){
	console.log('私の名前は'+this.name+'です。');
}
var yuki = new Hito('ユキ');
yuki.introduction();
function Neko(){
}
Neko.prototype=new Hito('タマ');
//プロトタイプに別のインスタンス(new ●●●の形で)を代入することで、その別のインスタンスを自身のプロトタイプとして扱える。
//Hitoを継承したので、NekoインスタンスでHitoのプロパティ(メンバ変数)を使える。
var tama=new Neko();//私の名前はユキです。
tama.introduction();//私の名前はタマです。
//introductionメソッドはtamaインスタンスにはないが、Hitoインスタンスのコンストラクタを継承しているため使うことが出来る。

要点

プロトタイプベースにおいて押さえておくポイントは下記の通りです。

  • JavaScriptのオブジェクト指向はクラスベースではなく、プロトタイプベースである。
  • インスタンス化の度に毎回無駄に関数が定義されるのを防ぐために、コンストラクタ内はメンバ変数のみを定義し、(関数)メソッドはプロトタイプで定義する。

参考文献