JavaScript オブジェクト指向 プロトタイプなどのまとめ
2013.01.27
この記事は最終更新日から1年以上が経過しています。
何気にここらへんについては書いていない感じでしたので、ざっとではありますがまとめさせて頂きました。
まず、ここが変だよJavaScriptといったところで挙げられるのは
「厳密な意味でのクラスという概念がない。」ということではないでしょうか。
ここで言う「クラス」とはCSSが参照するclassではなく、
C#、Javaなどといったオブジェクト指向プログラミング言語で使用される「クラス」に該当します。
あくまで厳密な意味で無い。とのことなので、無いこともないのです。
では、どのように作るかと言いますと。
var Webcyou = function(){};
と、最もシンプルなクラスの例を挙げてみました。
var hoge = new Webcyou();
と、呼び出す際はクラスベースのオブジェクト指向構文でも用いられるnew 演算子を利用し、
インスタンス化し実行することができます。
コンストラクタ
Webcyou関数は、new演算子によって呼び出されているのですが、この関数にあたる部分を「コンストラクタ」(constructor)と呼びます。
「コンストラクタ」は新たに生成するオブジェクトを初期化する(自動的に呼び出される)関数となります。
よってJavascriptでいうクラスはオブジェクトだということになります。
コンストラクタとして関数オブジェクトを呼び出した場合、thisは新たに生成するオブジェクトを指します。
this.プロパティ名 = 値
このように、オブジェクトにプロパティを追加することもできます。
プロパティとメソッド
this.プロパティ名で「プロパティ」の追加同様に「メソッド」の追加も行うことができます。
以下はPersonクラスにnameとageのプロパティを追加し、toStringという名前のメソッドを追加した例となります。
var Person = function(name, age) { this.name = name; this.age = age; this.toString = function() { window.alert(this.name + " : " + this.age + "歳"); }; } var hito = new Person("ウェブ夫", "4"); hito.toString();// 「ウェブ夫 : 4歳」
toString関数を実行する「ウェブ夫:4歳」と表示致します。
このように、new演算子によってインスタンス化されたオブジェクトに対してもメンバーを追加できるのが
Javascriptの特徴となっております。
2つのインスタンス
ここでインスタンスを2つ作成しました。
var Person = function(name, age) { this.name = name; this.age = age; } var hito = new Person("ウェブ夫", "4"); hito.toString = function() { window.alert(this.name + " : " + this.age + "歳"); }; hito.toString();// 「ウェブ夫 : 4歳」 var hito2 = new Person("ウェブ子", "18"); hito2.toString();//エラーとなります。
コンストラクタからtoStringを出し、インスタンスにtoSring関数を追加しました。
その後、hitoにtoSring関数の呼び出しを行うと、「ウェブ夫:4歳」と表示されます。
その後、またhito2インスタンスの生成を行い、hito2にtoSring関数の呼び出しを行うとエラーとなります。
図にするとこんな感じでしょうか。
このことによって、インスタンスのメソッドの追加はあくまでインスタンスへの追加であり、
基であるクラス「Person」には追加されていないことが分かります。
つまり、JavaScriptは、クラスベースのオブジェクト指向言語と異なり、同一のクラスを基に作成されたインスタンスでも異なるメンバーを持つ可能性があるということになります。
また、インスタンスに追加したメンバーは、そのインスタンスのみ有効となります。
プロトタイプベース
インスタンスに追加した メソッドはそのインスタンスのみ有効。
よって、インスタンス共通のメソッドを定義しようとすると、インスタンスではなくコンストラクタに定義しないといけないことが分かった。
ここで起こっている処理について考えてみましょう。
クラス(コンストラクタ)はインスタンスを生成する度に、各インスタンスの為にメモリを確保しています。
上記の例からすると、Parsonを基に生成したhitoはname,age,toStringという3つメンバーを設定しています。
toStringに関しては言えば全てのインスタンスで全く同じ値を設定します。
メソッドが1つや2つでインスタンスも1つや2つだとまだ良いのですが、10、20登録されているクラスの場合、インスタンスを生成する度に10、20のメソッドを無駄にコピーしてしまう結果となってしまいます。
これでは、パフォーマンス低下にもつながり、好ましくはありません。
そこで利用するのがプロトタイプとなります。
プロトタイプを利用するとプロパティやメソッドを共有することができます。このプロトタイプもオブジェクトとして扱われます。
今まで出てきた、クラス(コンストラクタ)、プロトタイプ、インスタンスと、全てオブジェクトであるということが分かります。このようにJavaScriptでは、常に実体化されたオブジェクトであり新しいオブジェクトを
作成するのもクラスではなく、オブジェクトを基にしているということです。
※コンストラクタオブジェクト
クラスの名前を定義します。プロパティを追加すればクラスフィールドの様に扱え、
関数を追加すればクラスメソッドの様に扱えます。
※プロトタイプオブジェクト
JavaScriptにおけるすべてのオブジェクトはプロトタイプという名前のプロパティを公開しています。
プロトタイプオブジェクトのプロパティがクラスすべてのインスタンスに継承されます。
プロトタイプオブジェクトのプロパティの値が関数の場合はクラスのインスタンスメソッドの様に扱えます。
適宜、必要に応じてメンバーを追加することができます。
※インスタンスオブジェクト
クラスのインスタンスは、いわゆるオブジェクトです。
上記で行った様にプロパティを直接定義することができ、ほかのインスタンスとは共有されません。
と、振り返ったところで実際上記で記述したのをプロトタイプを使い置き換えます。
var Person = function(name, age) { this.name = name; this.age = age; } Person.prototype.toString = function() { window.alert(this.name + " : " + this.age + "歳"); }; var hito = new Person("ウェブ夫", "4"); hito.toString(); // 「ウェブ夫 : 4歳」 var hito2 = new Person("ウェブ子", "18"); hito2.toString(); // 「ウェブ子 : 18歳」
プロトタイプオブジェクトのメリット
それでは、プロトタイプオブジェクトを使う利点について挙げていきます。
※ 必要なメモリ量の削減(節約)
メモリの節約ということで内部的に起こっていることを調べていきましょう。
プロトタイプ・オブジェクトの内容はそれぞれのインスタンスから参照されるのですが
オブジェクトのメンバーを参照する場合、以下のような順序で検索されています。
1.インスタンス自身を検索
2.そのプロトタイプを検索
図にすると以下のようになるかと思います。
このようにまず、インスタンス自身のメンバーを検索し、そのプロトタイプのメンバーを検索する順序をとっているのが分かります。
それでは、プロトタイプが提供しているメンバーをインスタンス側で変更するとどうなるでしょうか?
var Person = function(){}; Person.prototype.name = "ウェブ夫"; var hito = new Person(); var hito2 = new Person(); window.alert(hito.name + " : " + hito2.name); //ウェブ夫 : ウェブ夫 hito.name = "ウェブ蔵"; window.alert(hito.name + " : " + hito2.name); //ウェブ蔵 : ウェブ夫
nameはPerson.prototypeで宣言されたプロパティではあるのですが、あるインスタンスに対して 施された変更は、異なるインスタンスには適応されていないのが確認できます。
このことによってプロトタイプの参照は読み込みの場合のみあり、書き込みに対しては「インスタンス自身」に行われていることが分かります。
それでは、上記の処理に関して図でみてみましょう。
初期状態では、インスタンスhito,hito2ともにプロトタイプを参照しています。
インスタンスhitoのhito.nameプロパティに対して新たな値を設定したところで、
インスタンスhitoはプロトタイプオブジェクトのnameプロパティの参照をする必要がなくなります。
インスタンス自身に設定しているnameプロパティを取得するのです。
逆に削除する場合も同様となります。
オペランドとして指定された配列要素やプロパティ/メソッドを削除するためのdelete演算子を用いてプロパティを削除した場合。
var Person = function(){}; Person.prototype.name = "ウェブ夫"; var hito = new Person(); var hito2 = new Person(); hito.name = "ウェブ蔵"; window.alert(hito.name + " : " + hito2.name); //ウェブ蔵 : ウェブ夫 delete hito.name; delete hito2.name; window.alert(hito.name + " : " + hito2.name); //ウェブ夫 : ウェブ夫
と、それぞれのインスタンスに対してnameプロパティをdelete演算子で削除したところ、「ウェブ夫:ウェブ夫」と表示致しました。
これは、インスタンスhitoには独自のnameプロパティが存在するので、delete演算子はこの値を削除しております。また、インスタンスhito2には独自のnameプロパティが存在しないので何も行っていないことになります。
この事によって、インスタンス側でメンバーの追加、削除を行っても、プロトタイプ・オブジェクトに対しては影響を及ぼすことが無いことが分かりました。
ちなみに以下の様な記述でプロトタイプオブジェクトのメンバーを削除することも可能です。
delete Person.prototype.name
このように行うとプロトタイプオブジェクトのメンバーを削除することができ、また、undefind(未定義)値を設定することによって擬似的にインスタンス側でプロトタイプオブジェクトが提供するメンバーを無効化することもできます。delete演算子はプロパティそのものを削除するのに対して、undefind(未定義)はプロパティの存在はそのままで値を未定義していることになります。
var Person = function(){}; Person.prototype.name = "ウェブ夫"; Person.prototype.age = "4歳"; var hito = new Person(); hito.name = undefined; for(key in hito){ window.alert(key +":" + hito[key]); }//name:undefined age:4歳
for inで列挙した場合、nameプロパティが表示されるのが確認できます。
プロトタイプオブジェクトの変更
プロトタイプオブジェクトを使いインスタンスが参照する場合のもう一つ大きな利点があります。
インスタンスを生成した後、プロトタイプオブジェクトにメンバーを追加しても、追加したメンバーを認識してくれるという点です。
どういう事かと言いますと、
var Person = function(){}; Person.prototype.name = "ウェブ夫"; var hito = new Person(); Person.prototype.age = "4歳"; window.alert("年齢:" + hito.age); //年齢:4歳
このように、インスタンスhitoを生成した後、ageを追加し呼び出してもプロパティageは呼び出してくれます。
オブジェクトリテラル定義
オブジェクトリテラルとは、中括弧({})の中に、プロパティ名と値のペアをカンマ(,)で区切って記述する方法となります。
これを行うことによって、Person.prototype.メンバー名 = 〜〜など記述すること無く、簡潔にまた柔軟性を持ったコードの記述が行えます。
var Person = function(name,age){ this.name = name; this.age = age; }; Person.prototype = { talks : function(){ window.alert(this.name + "です。はじめまして!"); //年齢:4歳 }, toString : function(){ window.alert( this.name + ":" + this.age + "歳"); //年齢:4歳 } }; var hito = new Person("ウェブ夫","4"); hito.talks(); //ウェブ夫です。はじめまして! hito.toString(); //ウェブ夫:4歳
プロトタイプ・チェーン
最後にプロトタイプチェーンとなんですが「チェーン」とは訳すると鎖。プロトタイプベースのオブジェクト指向は鎖の様に繋がって継承するとでも覚えていただければ。
var Person = function(){} Person.prototype = { hello : function(){ window.alert("こんちには!"); } }; var hito = function(){}; hito.prototype = new Person(); hito.prototype.talks = function(){ window.alert("最近どう?クルクルってパスタ巻いてる?"); }; var webO = new hito(); webO.hello(); //こんにちは! webO.talks(); //最近どう?クルクルってパスタ巻いてる?
このようにhitoのプロトタイプにクラス(コンスラクタ)Personを格納し、更にそのプロトタイプにメンバーを追加し、また新たにインスタンスwebOを生成、それぞれに設定したhello,talksを呼び出すと問題無く呼び出してくれます。
図にすると以下のようになります。
helloメソッドを実行すると、まず、インスタンスwebO自身のメンバーを検索します。無いので、hitoクラス(コンストラクタ)のプロトタイプを検索します。また存在しないので、先程のプロトタイプの基となるオブジェクトPersonのプロトタイプを検索します。
と、このようにプロトタイプにインスタンスを設定することによってインスタンス間の継承関係を構築することができます。
さらに追加すると、追加した分、階層をさかのぼり最上位のObject.オブジェクト prototypeまでメンバーの検索が行われます。