30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

プロトタイプの基礎

Prototypeプロパティ

JavaScriptのオブジェクトは、自身が持っているプロパティだけでなく、プロトタイプのプロパティにもアクセスすることが出来る。
オブジェクトがどんなプロトタイプを持っているかは、[[Prototype]]プロパティを見れば分かる。

[[Prototype]]プロパティは、Object.getPrototypeOf()で取得できる。
引数に、[[Prototype]]プロパティを調べたいオブジェクトを渡せばよい。

あるいは、全てのオブジェクトが持っている__proto__プロパティで確認することも出来る。
__proto__プロパティは[[Prototype]]プロパティの値を格納しており、これにアクセスすることで、参照するプロトタイプオブジェクトを変更することも出来る。

__proto__プロパティは、ES5の時点では標準化はされていないが、Node.jsや多くのブラウザで実装されている。

var obj = {};

// 両方共同じもの、つまりobjの[[Prototype]]を参照している
console.log(Object.getPrototypeOf(obj) === obj.__proto__); // true

var prototype = Object.getPrototypeOf(obj);
console.log(Object.getOwnPropertyNames(prototype));
// 以下の配列が返ってくる。これが、objの[[Prototype]]が持っているプロパティ。
// [ 'constructor',
//   'toString',
//   'toLocaleString',
//   'valueOf',
//   'hasOwnProperty',
//   'isPrototypeOf',
//   'propertyIsEnumerable',
//   '__defineGetter__',
//   '__lookupGetter__',
//   '__defineSetter__',
//   '__lookupSetter__',
//   '__proto__' ]

// objはtoStringメソッドを使えるが……
console.log(obj.toString());    // [object Object]

// obj自身はtoStringを持っていない
// objの[[Prototype]]が参照しているプロトタイプオブジェクトが、toStringを持っているに過ぎない
console.log(obj.hasOwnProperty('toString'));   // false
console.log(obj.__proto__.hasOwnProperty('toString')); // true

プロパティの検索

プロパティを読み込もうとした際、まず、オブジェクト自身がそのプロパティを持っていないか確認する。
持っていた場合はその値を返すが、持っていなかった場合、そのオブジェクトの[[Prototype]]が参照しているプロトタイプオブジェクトの中から、同名のプロパティがないかを検索する。
見つかった場合は、そのプロパティの値を返す。

var obj = {
    toString: function(){ return 'これは、オブジェクト自身のプロパティです。'; }
};

console.log(obj.toString());    // これは、オブジェクト自身のプロパティです。
delete obj.toString;
console.log(obj.toString());    // [object Object]
console.log(obj.__proto__.hasOwnProperty('toString')); // true

上記の例では、最初にobj.toStringを呼び出した時はオブジェクト自身がそのプロパティを持っていたため、その内容を返している。
だが直後にこのプロパティを削除しているため、二番目にそれを呼び出した際は、プロトタイプオブジェクトが持っているtoStringが呼び出されている。

プロトタイプはどのように決まるのか

ほぼ全ての関数は、prototypeプロパティを持っている。明示しなくても、このプロパティが自動的に作られる。
そして、インスタンスオブジェクトは、自身の[[Prototype]]プロパティに、コンストラクタのprototypeプロパティへの参照を格納する。

function Person(){};

// Personは、prototypeというプロパティを持っている
console.log(Person.hasOwnProperty('prototype'));   // true

// インスタンスを作成
var person1 = new Person();

// person1の[[Prototype]]には、Personのprototypeが格納されている
console.log(person1.__proto__ === Person.prototype);    // true

全てのオブジェクトで使えるisPrototypeOf()メソッドで、そのオブジェクトが、引数に渡したオブジェクトのプロトタイプかどうかを調べることも出来る。

function Person(){};

console.log(Person.hasOwnProperty('prototype'));   // true

var person1 = new Person();

console.log(person1.__proto__ === Person.prototype);    // true

// Person.prototypeが、person1のプロトタイプである
console.log(Person.prototype.isPrototypeOf(person1)); // true

上記のような性質から、あるコンストラクタのインスタンスは全て、共通のプロトタイプオブジェクトを参照する。
そのため、プロトタイプのプロパティは、全てのインスタンスからアクセスできる。

function Person(name){
    this.name = name;
};
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person('Tom');
var person2 = new Person('Bob');

person1.sayName();  // Tom
person2.sayName();  // Bob

// person1とperson2のプロトタイプは、共通
console.log(person1.__proto__ === person2.__proto__);   // true

リテラルで作られたオブジェクトの[[Prototype]]には、Object.prototypeが設定される。

var obj = {};

console.log(obj.__proto__ === Object.prototype);   // true
console.log(Object.prototype.isPrototypeOf(obj));  // true

プロトタイプの拡張と再定義

プロトタイプオブジェクトは、任意のタイミングで拡張できる。その変更は、既に作成済みのインスタンスにも反映される。
プロパティを使おうとする度に検索するため、そのような挙動になる。

function Person(name){
    this.name = name;
};

var person1 = new Person('Tom');

// この時点ではperson1は、自分自身もプロトタイプも、sayNameプロパティを持っていない
console.log('sayName' in person1);   // false

// ここで、プロトタイプにsayNameプロパティを作る
Person.prototype.sayName = function(){
    console.log(this.name);
};

console.log('sayName' in person1);   // true
person1.sayName();  // Tom

プロトタイプオブジェクトそのものを新しく定義することも出来る。
その場合、定義し直した後に作成したインスタンスは新しいプロトタイプオブジェクトを参照するが、定義し直す前に作成したインスタンスは、古いプロトタイプオブジェクトは参照し続ける。

function Person(){
};

Person.prototype.sayMessage = function(){
    console.log('これは、最初に作ったsayMessage');
};

var person1 = new Person();
person1.sayMessage();   // これは、最初に作ったsayMessage

// ここで、プロトタイプオブジェクト自体を書き換える
Person.prototype = {
    sayMessage: function(){ console.log('これは、プロトタイプオブジェクト定義し直した際に作ったsayMessage') }
};

var person2 = new Person();
person2.sayMessage();   // これは、プロトタイプオブジェクト定義し直した際に作ったsayMessage

// person1は、書き換えられる前のプロトタイプオブジェクトを参照し続ける
person1.sayMessage();   // これは、最初に作ったsayMessage
console.log(person1.__proto__ === person2.__proto__);   // false