JavaScript上級 #3 プロトタイプチェーン
JavaScript のオブジェクトモデルは他のオブジェクト指向言語とは違います。クラスベース(class-based)ではなく プロトタイプベース(prototype-based) です。ES2015 の class 構文はプロトタイプの上に被せられた糖衣構文(syntactic sugar)です。この記事ではその内側を覗きます。
すべてのオブジェクトは別のオブジェクトを参照する #
JavaScript のすべてのオブジェクトは [[Prototype]] という内部スロットを持ちます。これが別のオブジェクトを指します。
const obj = { a: 1 };
Object.getPrototypeOf(obj); // Object.prototype
空のオブジェクト {} のプロトタイプは Object.prototype というビルトインオブジェクト。そこには toString、hasOwnProperty のようなメソッドが入っています。
const obj = { a: 1 };
obj.toString(); // '[object Object]'
// ↑ obj に直接ないが Object.prototype にある
obj.toString を探すとき — JavaScript はまず obj 自身を見て、なければプロトタイプチェーンを上に辿っていって探します。Object.prototype.toString まで到達して見つかります。
これをプロトタイプチェーンルックアップと呼びます。JavaScript のほぼすべてのメソッド呼び出しがこの動作の上で起こります。
関数の prototype プロパティ
#
関数はすべて prototype というプロパティを持ちます。
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log(`こんにちは, ${this.name}`);
};
const u = new User('カーティス');
u.greet(); // こんにちは, カーティス
new User('カーティス') が実行されると:
- 新しい空のオブジェクトを作る
- そのオブジェクトの
[[Prototype]]をUser.prototypeに設定 Userの本体を実行しながらthisにプロパティを追加- オブジェクトを返す
なので u.greet を呼ぶと — u にはないけれどプロトタイプチェーンを辿って User.prototype.greet を見つけて呼び出します。
クラス = プロトタイプの糖衣 #
ES2015 の class 構文で同じコードを書き直すと:
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`こんにちは, ${this.name}`);
}
}
const u = new User('カーティス');
console.log(u); // User { name: 'カーティス' }
console.log(u.greet); // [Function: greet]
内部的には正確に同じことが起こります。
class User {
greet() { /* ... */ }
}
// 上は実質的に次と同じ
function User() {}
User.prototype.greet = function() { /* ... */ };class が登場してから直接 prototype にメソッドを付けるコードはほとんどなくなりましたが、この構造は変わっていません。
プロトタイプチェーン — 深く入っていく #
継承関係はチェーンが長くなることで表現されます。
class Animal {
speak() { console.log('音'); }
}
class Dog extends Animal {
bark() { console.log('ワンワン'); }
}
const d = new Dog();
d.bark(); // 'd' → 'Dog.prototype' で発見
d.speak(); // 'd' → 'Dog.prototype' (なし) → 'Animal.prototype' で発見
チェーン:
d
↓ [[Prototype]]
Dog.prototype (bark)
↓ [[Prototype]]
Animal.prototype (speak)
↓ [[Prototype]]
Object.prototype (toString, hasOwnProperty)
↓
nullプロパティを探すとき、このチェーンを上に辿りながら検索します。最後まで見つからなければ undefined。
チェーンの終わりは null
#
Object.getPrototypeOf(Object.prototype); // null
Object.prototype のプロトタイプは null です。ここでチェーンが終わります。
自分のプロパティ vs 継承されたプロパティ #
class User {
constructor(name) {
this.name = name;
}
greet() {}
}
const u = new User('カーティス');
Object.hasOwn(u, 'name'); // true — インスタンス自身のプロパティ
Object.hasOwn(u, 'greet'); // false — プロトタイプにある
'greet' in u; // true ('in' はチェーンまで検査)
Object.hasOwn は自分のプロパティだけを検査。in 演算子はチェーンまで辿ります。両者の違いを意識しなければならない場面が時々あります。
古いコードでは obj.hasOwnProperty('key') がよく見られますが、これは obj 自身に hasOwnProperty が定義されていると壊れる可能性があります。ES2022 の Object.hasOwn が安全な答えです。
プロトタイプを直接扱う道具 #
1) Object.create — プロトタイプを明示して作る
#
const animalProto = {
speak() { console.log('音'); },
};
const dog = Object.create(animalProto);
dog.bark = function() { console.log('ワンワン'); };
dog.speak(); // 音 (プロトタイプから)
dog.bark(); // ワンワン (自身のプロパティ)
クラスを使わずプロトタイプを直接指定してオブジェクトを作る方法です。古い資料にはよく出てきますが、モダンなコードではほぼ class が標準です。
2) Object.create(null) — プロトタイプのないオブジェクト
#
const dict = Object.create(null);
dict.toString; // undefined — Object.prototype もない
const normal = {};
normal.toString; // [Function: toString] — ビルトインが見える
キーと値の辞書としてオブジェクトを使うとき — toString のようなビルトインキーの衝突を防ぐには Object.create(null) が安全です。モダン JavaScript では Map がより良い答えですが、ライブラリ内部のコードで時々出会います。
プロトタイプの修正 — 推奨されない #
Array.prototype.first = function() {
return this[0];
};
[1, 2, 3].first(); // 1
ビルトインのプロトタイプにメソッドを追加するパターン(いわゆる monkey patching)は非常に強力ですがほぼ常に事故を起こします。他のコード(ライブラリ、標準の新規メソッド)と衝突しやすいです。
自分のクラスの prototype には普通にメソッドを追加できますし、それは正常です。ビルトインを触るのは避けてください。
__proto__ — 古い動作
#
古い JavaScript には __proto__ という非公式のプロパティがありました。ES2015 で標準化されましたが使用は推奨されません。
const obj = {
__proto__: animalProto, // 使用しないでください
};
// 推奨
const obj2 = Object.create(animalProto);Object.getPrototypeOf / Object.setPrototypeOf / Object.create がモダンな答えです。
instanceof — プロトタイプチェーン検査
#
class Animal {}
class Dog extends Animal {}
const d = new Dog();
d instanceof Dog; // true
d instanceof Animal; // true (チェーンに沿って親も)
d instanceof Object; // true
x instanceof C は C.prototype が x のプロトタイプチェーンのどこかにあるかを検査します。チェーンルックアップのもう一つの応用です。
関数もオブジェクト — 自分のプロパティを持てる #
JavaScript では関数もオブジェクトです。プロパティを直接付けられます。
function counter() {
counter.count = (counter.count ?? 0) + 1;
return counter.count;
}
counter(); // 1
counter(); // 2
counter(); // 3
このようなパターンはあまり使われませんが、JavaScript の関数が第一級オブジェクト(first-class object)であるという意味が見える場面です。
具体的な例 — 配列のメソッドチェーン #
const arr = [1, 2, 3];
Object.hasOwn(arr, 'map'); // false
Object.hasOwn(arr, 'length'); // true
arr.__proto__ === Array.prototype; // true
Array.prototype.__proto__ === Object.prototype; // true
チェーン:
[1, 2, 3] (length, 0, 1, 2)
↓
Array.prototype (map, filter, reduce, ...)
↓
Object.prototype (toString, ...)
↓
nullarr.map を呼ぶと — arr 自身にはないので Array.prototype で見つけて実行します。すべての配列が同じ Array.prototype を共有しているので、メソッドが一度だけメモリ上に存在します。
まとめ #
この記事で整理した内容:
- JavaScript はプロトタイプベース — クラス構文は糖衣
- すべてのオブジェクトは
[[Prototype]]で別のオブジェクトを指す - プロパティを探すときはプロトタイプチェーンを上に辿る
- 関数の
prototypeプロパティがnew呼び出し時にインスタンスの[[Prototype]]に設定される Object.hasOwnで自分のプロパティを検査(inはチェーンまで)Object.create(...)でプロトタイプを明示して作成- ビルトイン prototype の修正は推奨 X
instanceofはプロトタイプチェーン検査
次の記事(#4 イベントループとタスク)では JavaScript の非同期の実際の動作 — イベントループ、マイクロタスクとマクロタスクの違い、そして Promise.then と setTimeout がなぜ違う順序で実行されるかを扱います。