プロトタイプ=見本となるオブジェクト

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つある。

  1. 見本となるオブジェクトが、見本として相応しくない具体的な値(firstName = "右京", lastName = "杉下")を持っている
  2. 新しいオブジェクトに対してプロパティを設定する処理を抽象化できていない

まずは前者に対応する。見本として相応しい値は状況によって異なる(例えば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オブジェクトの複製とでもいうべきオブジェクトは、実際には複製ではない。「プロトタイプチェーン」をキーワードに調べるとよい。

(ひどい)