uhyohyo.net

JavaScript初級者から中級者になろう

九章第一回 オブジェクト指向とは

このページの最終更新日:

九章では、DOMから少し離れてオブジェクト指向について解説します。

オブジェクト指向とは

オブジェクトそのものについては、一章で少し解説しました。では、オブジェクト指向とは何なのでしょう。

オブジェクト指向とは何かというのは諸説ありますが、大まかにいうと、プログラムをいい感じに部品に細分化し、部品の組み合わせでプログラムをつくるというものです。このそれぞれの部品がオブジェクトというわけです。この章では、このような考え方のもとオブジェクト指向を実践するための言語機能を紹介します。

注意として、この章に書いてあることは少し古くさいです。これは、JavaScriptのオブジェクト指向的機能が進化を続けているため、この章で紹介するのは最新の方法ではないからです。

とはいえ、基礎的な考え方は同じなので、ぜひこの章で身につけてください。

例えば、RPGを作りたい場合を考えてみましょう。RPGにはいろいろなものが登場します。それら1つ1つが部品となるでしょう 。例えば、「勇者」がいます。「敵」もいます。「アイテム」とか「武器」というのもありますね。RPGはこれらの組み合わせで動いているということができます。

なので、これらをオブジェクトとして作り、それを使ってプログラムを作ればよいのです。ちょっとやってみましょう。


var yuusha = new Object();
yuusha.name = "勇者";	//勇者の名前
yuusha.power = 10;	//勇者の攻撃力

var teki = new Object();
teki.name = "ザコ敵";	//敵の名前
teki.hp = 100;		//敵のHP

//攻撃する
console.log(yuusha.name + "は "+ teki.name + "に" + yuusha.power + "ダメージをあたえた!");

実行すると「勇者は ザコ敵に10ダメージをあたえた!」と出ます。

この例では「勇者」と「ザコ敵」の情報がそれぞれオブジェクトに入ってます。オブジェクトの作り方は、まずnew Object()として空のオブジェクトを作り、そこにプロパティをくっつけるというやり方です。

また、オブジェクトは、「情報」だけでなく「機能」をもつこともできます。すなわち、プロパティだけでなくメソッドも持てるということです。


var yuusha = new Object();
yuusha.name = "勇者";	//勇者の名前

//自己紹介してもらう
yuusha.jikoshokai = function(){
  console.log("私の名前は" + this.name + "です。");
};

yuusha.jikoshokai();

「私の名前は勇者です。」と出ます。

さて、今回は、勇者オブジェクトにjikoshokaiというメソッドをもたせました。勇者に自己紹介をしてもらうメソッドです。今回、thisという謎のものが出ています。まずはこれを解説します。コードと結果を比べると、 this.name が "勇者"、すなわちyuusha.nameを表しています。ということは、thisはyuushaです。

実は、何かというと、あるオブジェクトのメソッドが呼び出されたときに、thisはそのオブジェクト自体を表します。つまり、yuusha.jikoshokai()として呼び出した場合、その中ではthisはyuushaになったのです。

この機能はオブジェクト指向プログラミングをする上で重要です。メソッドはオブジェクトが持つ機能なので、メソッド内で自分が何のオブジェクトなのか知らないというのはおかしいですね。

さて、ここまで少し解説してきたのはまだまだこれは本当のオブジェクト指向ではありません。例えば、「勇者」が5人いた場合を考えます。勇者も1人で1つの部品ですから、勇者が5人いる場合オブジェクトは5つになります。今回自己紹介もしてもらいましょう。


var yuusha1 = new Object();
yuusha1.name = "勇者1";
yuusha1.jikoshokai = function(){
console.log("私の名前は" + this.name + "です。");
};
var yuusha2 = new Object();
yuusha2.name = "勇者2";
yuusha2.jikoshokai = function(){
console.log("私の名前は" + this.name + "です。");
};
var yuusha3 = new Object();
yuusha3.name = "勇者3";
yuusha3.jikoshokai = function(){
console.log("私の名前は" + this.name + "です。");
};
var yuusha4 = new Object();
yuusha4.name = "勇者4";
yuusha4.jikoshokai = function(){
console.log("私の名前は" + this.name + "です。");
};
var yuusha5 = new Object();
yuusha5.name = "勇者5";
yuusha5.jikoshokai = function(){
console.log("私の名前は" + this.name + "です。");
};

//みんなに自己紹介してもらう
yuusha1.jikoshokai();
yuusha2.jikoshokai();
yuusha3.jikoshokai();
yuusha4.jikoshokai();
yuusha5.jikoshokai();

ちゃんとそれぞれオブジェクトを作って、nameプロパティとjikoshokaiプロパティを作っています。

しかし、何だか格好悪い気がしませんか。ここで、あることに気づくかもしれません。jikoshokaiは5人とも全く同じメソッドであるということです。thisのおかげでjikoshokaiの中にyuusha1〜yuusha5の変数が出てきてませんから、全部共通になっています。そこで、こうしようと思うかもしれません。


//先に自己紹介関数を作っておく
var jikoshokai = function(){
  console.log("私の名前は" + this.name + "です。");
};

var yuusha1 = new Object();
yuusha1.name = "勇者1";
yuusha1.jikoshokai = jikoshokai;
var yuusha2 = new Object();
yuusha2.name = "勇者2";
yuusha2.jikoshokai = jikoshokai;
var yuusha3 = new Object();
yuusha3.name = "勇者3";
yuusha3.jikoshokai = jikoshokai;
var yuusha4 = new Object();
yuusha4.name = "勇者4";
yuusha4.jikoshokai = jikoshokai;
var yuusha5 = new Object();
yuusha5.name = "勇者5";
yuusha5.jikoshokai = jikoshokai;

//みんなに自己紹介してもらう
yuusha1.jikoshokai();
yuusha2.jikoshokai();
yuusha3.jikoshokai();
yuusha4.jikoshokai();
yuusha5.jikoshokai();

これは、みんな同じ関数なんだから、関数は最初に1個作っておいて、勇者1〜5にはそれぞれ代入するだけで済ませようということです。yuusha1.jikoshokai=jikoshokai;というようにjikoshokaiが2つ出てきていてややこしいですが、前者は勇者のオブジェクトのプロパティ名、後者は変数名で別物です。

注意としては、もはやyuusha1.jikoshokaiyuusha2.jikoshokaiなどはもはや同じ関数オブジェクトであるにも関わらず、呼び出すとthisの値は依然としてそれぞれの呼び出しで異なっているということです。これは、thisの値は関数オブジェクトに保存されているものではなく、あくまで呼び出し時の状況によって決まるということです。つまり、繰り返しになりますが、thisの値は例えばyuusha1.jikoshokai();の場合はyuusha1になるというように、呼び出し時に決定されます。

さて、サンプルは前に比べて少しすっきりしたような気がしますが、実はまだオブジェクト指向的な感じがしません。いままでのやり方では、yuusha1〜yuusha5という別々のオブジェクトを、同じプロパティ名を持つように気をつけて作っているだけです。

yuusha1〜yuusha5は同じ「勇者」なのですから、本質的に同じ種類のオブジェクトであるということができます。同じ種類のオブジェクトは統一して扱えるような方法が欲しいですね。それがオブジェクト指向において重要な部分の1つです。

具体的にどうすればいいかというと、同じ種類のオブジェクトに対して共通の初期化方法共通の機能(メソッド)とを定めます。初期化というのはオブジェクトを作ることです。例えばyuusha1の初期化に該当する部分は


var yuusha1 = new Object();
yuusha1.name = "勇者1";
yuusha1.jikoshokai = jikoshokai;

です。このパターンはyuusha2やyuusha3などと共通であり、違うのは名前だけです。なので、これが勇者オブジェクトの共通の初期化方法であるということです。

このような言語機能は他のオブジェクト指向言語ではクラスとして知られているものですが、以下で紹介するJavaScriptのオブジェクト指向的機能はクラスとは趣が違います。

実は、オブジェクト指向は2種類に分類されることがあります。1つはクラス指向、もう1つはプロトタイプ指向です。JavaScriptは後者に属するため、クラス指向の言語とは少し違った方法でオブジェクト指向を実現しています。

ただし、JavaScriptにもクラスの構文がES2015で導入されました。これについては後々紹介します。しかしJavaScriptに特有の事情は健在ですので、ぜひこの章でJavaScript的オブジェクト指向に慣れ親しむことを推奨します。

さて、まず共通の初期化方法について見ます。これは、コンストラクタを作ることによって実現します。コンストラクタとは、ある種の関数です。例えば、勇者のコンストラクタを作りたければ関数を作ります。


function Yuusha(){
}

コンストラクタを使ってオブジェクトを初期化するのに使うのがnew演算子です。これは今までもnew Object()とかの形で出てきましたが、あまり詳しく説明していませんでした。

実はこれ、演算子の一種ということになっています。といっても特殊な形をもつ演算子です。new Object()の例を見て分かるように、 new コンストラクタ(引数) という形で使います。

new演算子が使われると、コンストラクタが呼び出されます。そこでオブジェクトの初期化をすればいいのです。具体的には次のようにします。


function Yuusha(n){
  this.name = n;
  this.jikoshokai = jikoshokai;
}

var yuusha1 = new Yuusha("勇者1");
var yuusha2 = new Yuusha("勇者2");
var yuusha3 = new Yuusha("勇者3");

yuusha1.jikoshokai();
yuusha2.jikoshokai();
yuusha3.jikoshokai();

実は、コンストラクタがnew演算子で呼び出された場合、新しいオブジェクトが作成され、それが関数内でのthisになります。そして、そのオブジェクトがnew演算子の返り値となります。したがって、コンストラクタはthisとして与えられた新しいオブジェクトを初期化すればいいことになります。それをやっているのが上のプログラムです。

ちなみに、newによって作られたオブジェクトのことをコンストラクタから見てインスタンスと呼びます。つまり、上のサンプルではyuusha1,yuusha2,yuusha3は全てYuushaのインスタンスです。

このサンプルは最初に比べてだいぶすっきりしたプログラムになりました。特に、勇者オブジェクトの初期化処理はコンストラクタにまとまっていていいですね。

しかし、まだ完璧というわけではありません。そのあたりを次回解説します。