プロトタイプ=見本となるオブジェクト
JavaScriptを始めたころはprototype
オブジェクトが何なのかよく分からなかったけど、その名の通り「見本」あるいは「原型」なんだと分かれば、コンストラクタ関数とprototype
オブジェクトの関係は単純だと思える。
例として、人を表すオブジェクトを考えてみる。姓を表すlastName
プロパティと、名を表すfirstName
プロパティ、そしてそれらをつなげた文字列を返すgetFullName
メソッドを定義する。
var person = { firstName: '右京', lastName: '杉下', getFullName: function () { return this.lastName + ' ' + this.firstName; } };
さらに違う人物を表すオブジェクトを作ることになったとする。
var another = { firstName: '享', lastName: '甲斐', getFullName: function () { return this.lastName + ' ' + this.firstName; } };
さらに違う人物を……、いや、これだと何度も同じこと(getFullName
の関数)を書かなくてはいけない。こういうときは、適当な関数を用意し、そのprototype
プロパティに見本となるオブジェクトを代入するとよい。new
演算子を使ってその関数を呼び出すことで、そのprototype
オブジェクトの複製とでもいうべきオブジェクトが作られる。
var person = { firstName: '右京', lastName: '杉下', getFullName: function () { return this.lastName + ' ' + this.firstName; } }; // 適当な関数を用意して(このような関数は一般に大文字で始まる名前が付けられる) function Person() {} // 見本(prototype)となるオブジェクトを設定する Person.prototype = person; // personの複製のようなオブジェクトを作る var another = new Person(); // 姓・名は上書きする another.firstName = '享'; another.lastName = '甲斐'; // getFullName関数はそのままpersonと同じのを使えばよいので上書きしない // 実行してみる alert(another.getFullName()); // "甲斐 享" // さらにもう一人 var another1 = new Person(); another1.firstName = '尊'; another1.lastName = '神戸'; alert(another1.getFullName()); // "神戸 尊"
getFullName
の関数を書くのは1回きりで済むようになった。でも、これだと少し困ることがある。それはnew Person()
で作ったオブジェクトのプロパティを上書きするのを忘れたときに起こる。
var another2 = new Person(); another2.firstName = '薫'; // lastNameを上書きするのを忘れた! alert(another2.getFullName()); // "杉下 薫" (ノ∀`)
lastName
プロパティを上書きするのを忘れた結果、prototype
オブジェクト(=== person
オブジェクト)のlastName
プロパティがそのまま使われてしまった。
問題は2つある。
- 見本となるオブジェクトが、見本として相応しくない具体的な値(
firstName = "右京"
,lastName = "杉下"
)を持っている - 新しいオブジェクトに対してプロパティを設定する処理を抽象化できていない
まずは前者に対応する。見本として相応しい値は状況によって異なる(例えばPerson
がチャットの参加者を表すのであれば「名無し」が良いかもしれない)けど、ここでは空文字列(''
)にしておく。これで少なくともちぐはぐな名前になってしまうことは防げる。
var person = { firstName: '', lastName: '', getFullName: function () { return this.lastName + ' ' + this.firstName; } }; function Person() {} Person.prototype = person; var another2 = new Person(); another2.firstName = '薫'; // lastNameを上書きするのを忘れた! alert(another2.getFullName()); // " 薫" ( ̄- ̄ )
次は後者だが、これはPerson
関数内で行うようにすればよい。関数がnew
演算子によって実行される場合、this
にその関数によって生成される新しいオブジェクト(prototype
オブジェクトの複製とでもいうべきオブジェクト)がセットされる。
var person = { firstName: '', lastName: '', getFullName: function () { return this.lastName + ' ' + this.firstName; } }; function Person(firstName, lastName) { if (firstName != null) { this.firstName = firstName; } if (lastName != null) { this.lastName = lastName; } } Person.prototype = person; var ukyo = new Person('右京', '杉下'); var toru = new Person('亨', '甲斐'); alert(toru.getFullName()); // "甲斐 亨"
これでオブジェクトを作るたびにプロパティを上書きする処理を書かなくて済むようになった。このように、新しいオブジェクトの初期化処理を行う関数はコンストラクタ関数と呼ばれる。
あとはperson
オブジェクトだ。person
オブジェクトを直接触る機会はなさそうなので、Person.prototype
に直接代入する形に書き直してしまう。
function Person(firstName, lastName) { if (firstName != null) { this.firstName = firstName; } if (lastName != null) { this.lastName = lastName; } } Person.prototype = { firstName: '', lastName: '', getFullName: function () { return this.lastName + ' ' + this.firstName; } };
これでよし。
巷のJavaScriptの解説では、コンストラクタとprototype
オブジェクトに関して次のような例を示す。
function Person(firstName, lastName) { if (firstName != null) { this.firstName = firstName; } if (lastName != null) { this.lastName = lastName; } } Person.prototype.getFullName = function () { return this.lastName + ' ' + this.firstName; };
いきなりこのコードを見せられると、コンストラクタ関数がメインで、prototype
オブジェクトがそのおまけのように見えると思う。見本となるオブジェクト(prototype
オブジェクト)が先立つと考えた方が理解しやすいと思う。
また、prototype
は構文の何かに見えるかもしれない。実際は単なるオブジェクトで、そのプロパティへの代入でしかない。すべての関数には初めからprototype
オブジェクトが付いているので、そのオブジェクトを見本となるオブジェクトに仕立て上げてもよい。
prototype
オブジェクトのconstructor
プロパティは、便宜的なものでしかない。constructor
プロパティを使うような処理を書いたり、そのような処理が含まれたスクリプトを読み込む場合はともかく、言語仕様としてprototype
オブジェクトにconstructor
プロパティを定義していないからと言って何か問題が起きるわけではない。
// ... 省略 Person.prototype = { constructor: Person, // new Person().constructor === Person となるようにする // ... 省略
でもまあ、付けておくといいと思う。
は、実際には複製ではない。「プロトタイプチェーン」をキーワードに調べるとよい。prototype
オブジェクトの複製とでもいうべきオブジェクト
(ひどい)