継承ってなんだろう? (2)
最初に注意
自分はつい最近JavaScriptをかじったばかりの,ただの初級者です.以下の文章は単に自分はこう考えている,というだけで,誤りが含まれている可能性は十分にあります.もし読まれる方がいたら,その点を十分にご理解ください.また誤りの指摘は大歓迎です.(以上、コピペ)
prototype継承
プロトタイプチェーンによるプロパティ検索のことを,プロトタイプベースの継承とか単にプロトタイプ継承と呼ばれることがあります.たとえば,JavaScriptはプロトタイプベースの継承をサポートするオブジェクト指向言語である,みたいなフレーズをたびたび見かけます.
継承って様々なコンテキストで使用される概念で,だいたい似ているけど微妙に違ってたりするのが悩ましいところです.ただ,プロトタイプチェーンによるプロパティ検索という仕組みは,たしかにクラスベースの継承に似ている部分が多そうです.そこでもう少しいろいろといじってみたいと思います.
例のPointオブジェクト,コンストラクタ関数で初期化した部分を,Pointオブジェクトの連想配列ではなくprototypeオブジェクトに設定するように書き換えてみます.
function Point(){} Point.prototype.x = 0; Point.prototype.y = 0; Point.prototype.toString = function(){ return "(" + this.x + "," + this.y + ")" ; }; var point = new Point(); document.write(point.toString(), "<br />"); for (prop in point) { document.write("name:=" + prop + "," + "value:=" + point[prop], "<br />"); } // 実行結果 // ---------------------------------------------------- // (0,0) // name:=x,value:=0 // name:=y,value:=0
x, yはPoint.prototypeに設定されているはずですが,for (prop in point)で取得されてます.プロパティ検索がうまく働いているようです.では,次のコードはどうでしょう?
var point = new Point(); point.x = 1; point.y = 2; document.write(point.toString(), "<br />"); for (prop in point) { document.write("name:=" + prop + "," + "value:=" + point[prop], "<br />"); } // 実行結果 // ---------------------------------------------------- // (1,2) // name:=x,value:=1 // name:=y,value:=2
Point.prototypeオブジェクトで定義したのと同名のKeyをpointオブジェクトに設定してみました.結果は予想通り,(1,2)が出力されてますね.ちなみに,ここで値を設定したことによってPoint.prototypeで定義された値が変更されることはないはずです.確認してみましょう.
var point = new Point(); point.x = 1; point.y = 2; var point2 = new Point(); document.write(point2.toString(), "<br />"); for (prop in point2) { document.write("name:=" + prop + "," + "value:=" + point2[prop], "<br />"); } // 実行結果 // ---------------------------------------------------- // (0,0) // name:=x,value:=0 // name:=y,value:=0
pointオブジェクトに対する設定がpoint2オブジェクトに影響を与えていません.プロトタイプオブジェクトに対する変更は,プロトタイプオブジェクトをプロトタイプチェーンにもつすべてのオブジェクトに影響を与えるはずですので,pointオブジェクトに対するに対する設定はprototypeオブジェクトに影響を与えていないことになります.ここまで,全体的にうまく動作しているようです.
さて,コンストラクタ関数で初期化する方法と,プロトタイプオブジェクトで初期化する方法,どちらがよいのでしょうか?まず,メソッドについてはプロトタイプオブジェクトがよいといわれています.というのも,コンストラクタ関数で作成すると,オブジェクトを作成した数だけメソッドも作成されることになります.これはプロトタイプオブジェクトで1つだけ作成するのと比較して明らかにムダです.
プロパティはどうでしょうか?プロパティがreadonlyならプロトタイプオブジェクトでよいでしょうが,ほとんどの場合,オブジェクト単位で値は変化するはずです.値の書き込みは結果的にオブジェクト自身の連想配列に対して行なわれるため,プロトタイプオブジェクトに初期値を設定する必要はあまりないかもしれません.
ここまでを踏まえ,Pointオブジェクトの定義を以下のように変更します.
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.x = void 0; Point.prototype.y = void 0; Point.prototype.toString = function(){ return "(" + this.x + "," + this.y + ")" ; };
Point.prototype.xを用意したのは気分的な要素が大きいのですが,Pointオブジェクトにおけるxとかyという存在は,オブジェクトの中に隠蔽するのでなく公開すべきもの,と考えました.プロトタイプオブジェクトはオブジェクトの共通的な振る舞いを定義し,Pointオブジェクトはオブジェクト固有の情報を持たせる,と考えた場合,xやyの値は個々のオブジェクトに設定するしかないのだけど,xやyというKey(=インタフェース)があるのだという(メタ)情報は,共通的な振る舞いとしてプロトタイプオブジェクトに存在していてもよいかな?と思いました.
Point.prototype.xの初期値に何を入れておくかですが,0でもなくnullでもなくundefinedにしてみました.undefinedはvoid 0で表現することが出来ます.
おまけ
とあるサイトで,こんな実装を見かけました.
function Point(x, y) { this.x = x; this.y = y; } Point.prototype = { x : void 0, y : void 0, toString : function(){ return "(" + this.x + "," + this.y + ")" ; } }
このほうがすっきりと見やすいでしょう,ということなんですが……
var point = new Point(1, 2); document.write(point.toString(), "<br />"); document.write(point.constructor, "<br />"); // 実行結果 // ---------------------------------------------------- // (1,2) // function Object() { [native code] }
point.constructorにさえ目をつぶれば,問題ないと思います.ただ,本当に目をつぶっていいのか,というのが問題なんですね.たぶん個人で小規模なプログラムを書いているうちはいいんでしょうけど,だんだん規模が多くなって,フリーのライブラリなども使用するようになると,このような行儀の悪い実装をしたことで足をすくわれることが多くなってきます.これは逆の立場で,ライブラリの中に行儀の悪い実装が混じっていると困ることがあるのと一緒です.
次回は...
継承についてもう少し.Pointを継承したオブジェクトをどうやって作るか?みたいなことを書く予定