Contents
プロトタイプとプロトタイプチェーンの基本概念
JavaScript の継承は「オブジェクトが別のオブジェクトを参照して機能を取得する」仕組みです。このセクションでは、MDN が定義した プロトタイプ と プロトタイプチェーン の概要を整理し、実務でコードを書く前に押さえておくべきポイントを解説します。概念を正しく理解すれば、意図しない継承やバグの原因特定が格段に楽になります。
プロトタイプとは何か
プロトタイプは、各オブジェクトが内部的に保持している [[Prototype]] 参照(別名 proto)です。MDN の「継承とプロトタイプチェーン」によると、オブジェクトがプロパティを検索するときはまず自身を調べ、見つからなければ [[Prototype]] が指すオブジェクトに委譲します。
[[Prototype]]はエンジン内部のリンクであり、直接書き換えることはできません。- 代替手段として
Object.getPrototypeOf()(MDN リファレンス)やデバッグ目的の__proto__アクセサ(MDN の説明)が利用できます。
ポイント:プロトタイプは「オブジェクトのテンプレート」であり、インスタンス自身が保持しない共有ロジックやデフォルトメソッドを格納します。
プロトタイプチェーンの探索アルゴリズム
あるオブジェクト obj がプロパティ p を取得しようとしたとき、エンジンは次の順序で検索を行います。以下の手順は MDN の「プロパティの探索」でも同様に説明されています。
obj自身にpが存在するか確認- 存在しなければ
Object.getPrototypeOf(obj)(=obj.[[Prototype]])へ移動して再チェック - さらに上位のプロトタイプをたどり、
nullに到達するまで繰り返す
この連鎖が プロトタイプチェーン と呼ばれます。検索が null に至った時点で「プロパティは存在しない」ことが確定します。
|
1 2 |
obj ──► Vehicle.prototype ──► Object.prototype ──► null |
ポイント:チェーンの途中で同名プロパティが見つかれば、下位オブジェクト側のものは「隠蔽」されます。デバッグ時にどこで隠蔽されたかを追うことがバグ解決の鍵です。
ES5 での継承パターン:コンストラクタ関数と Object.create
実務コードではまだ ES5 スタイルが残っているケースも多く、コンストラクタ関数 と Object.create を組み合わせた継承手順を把握しておくことは重要です。このセクションでは、ES5 で安全に継承を実装する方法と注意点を具体例とともに示します。
コンストラクタ関数と prototype の設定方法
まずはベースとなるコンストラクタ関数とその prototype にメソッドを定義する基本形です。以下のコードは MDN の「関数としてのクラス」に相当します。
|
1 2 3 4 5 6 7 8 9 10 11 |
// ── ベースコンストラクタ ─────────────────────── function Vehicle(name) { // インスタンス固有プロパティはここで初期化 this.name = name; } // prototype に共有メソッドを配置(インスタンス間でメモリが節約される) Vehicle.prototype.getName = function () { return this.name; }; |
new Vehicle('car')が呼び出されると、エンジンは自動的に新しいオブジェクトを生成し、その[[Prototype]]をVehicle.prototypeに設定します。- 重要:メソッドはすべて
Vehicle.prototype上に置くことで、インスタンスごとの重複定義を防げます。
Object.create を用いた安全な継承実装例
次に子コンストラクタ Car が Vehicle からプロパティとメソッドを受け継ぐ例です。Object.create は「空のオブジェクトを作り、指定したオブジェクトを prototype に設定」する安全な手段として MDN でも推奨されています(MDN の Object.create)。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// ── 子コンストラクタ ─────────────────────── function Car(name, doors) { // 親コンストラクタを呼び出して基底プロパティを初期化 Vehicle.call(this, name); this.doors = doors; // Car 固有プロパティ } // 継承設定(Object.create が安全な手段) Car.prototype = Object.create(Vehicle.prototype); // constructor プロパティは手動で修正する必要がある Car.prototype.constructor = Car; // メソッドのオーバーライド例 Car.prototype.getName = function () { // 親メソッドを呼び出しつつ文字列を拡張 return 'Car: ' + Vehicle.prototype.getName.call(this); }; |
ポイント解説
Object.create(Vehicle.prototype)によりCar.prototypeが生成され、内部的に[[Prototype]]がVehicle.prototypeへリンクします。- 継承後は
Car.prototype.constructorがデフォルトでVehicleを指すため、必ず手動でCarに戻さなければinstanceof Carの判定が正しく行われません。 - 完成した継承チェーンは
carInstance → Car.prototype → Vehicle.prototype → Object.prototype → nullとなります。
ES6+ の class 構文と内部的プロトタイプリンク
ES2015(ES6)以降、class 文法が導入され継承記述が大幅に簡略化されました。しかし内部では依然として プロトタイプチェーン が使われている点を理解すれば、旧コードとの比較やトラブルシュートが容易になります。この章では class と extends の実装詳細と、MDN が提供する公式解説(クラスの継承)を踏まえて説明します。
class 文の基本形と自動生成される prototype
|
1 2 3 4 5 6 7 8 9 10 |
class Vehicle { constructor(name) { this.name = name; // インスタンス固有プロパティ } getName() { // メソッドは自動的に prototype に配置 return this.name; } } |
classは実体として 関数オブジェクト が生成され、Vehicle.prototypeにメソッドが自動的に登録されます。new Vehicle('bike')の結果得られるインスタンスは内部で[[Prototype]] === Vehicle.prototypeを保持します(ES5 と同様の挙動)。
extends が行う二重のプロトタイプ設定
|
1 2 3 4 5 6 7 8 9 10 11 |
class Car extends Vehicle { constructor(name, doors) { super(name); // 親コンストラクタ呼び出し this.doors = doors; } getName() { return 'Car: ' + super.getName(); // super は親 prototype のメソッドにアクセス } } |
extends が内部で実行している処理は、概念的には次の ES5 ヘルパー関数と同等です(MDN の内部実装例を参考)。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function _inherits(subClass, superClass) { // インスタンス側の継承 subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; // コンストラクタ関数同士の継承(static プロパティ用) if (Object.setPrototypeOf) { Object.setPrototypeOf(subClass, superClass); } else { subClass.__proto__ = superClass; // 非推奨だが互換性確保のため } } _inherits(Car, Vehicle); |
- インスタンス側:
Car.prototype.[[Prototype]] → Vehicle.prototypeが作られ、new Car()の結果は[[Prototype]]がCar.prototypeに向く。 - コンストラクタ側:
Car.[[Prototype]] → Vehicleが設定され、staticメンバーやinstanceof判定に影響します。
実務で役立つサンプル:Vehicle / Car と Animal / Dog
実際のプロジェクトでは「ベースクラス+派生クラス」のパターンが頻出します。ここでは ES5 と ES6 の両方で Vehicle → Car、さらに別例として Animal → Dog を示し、オーバーライドやシャドウイングの注意点を併せて解説します。
ES5 版の完全実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// ---------- Vehicle ---------- function Vehicle(name) { this.name = name; } Vehicle.prototype.move = function (km) { console.log(this.name + ' moves ' + km + 'km'); }; // ---------- Car ---------- function Car(name, doors) { Vehicle.call(this, name); // 親コンストラクタ呼び出し this.doors = doors; } Car.prototype = Object.create(Vehicle.prototype); Car.prototype.constructor = Car; // メソッドのオーバーライド Car.prototype.move = function (km) { Vehicle.prototype.move.call(this, km); // 親メソッドを再利用 console.log('Doors: ' + this.doors); }; var sedan = new Car('Sedan', 4); sedan.move(120); // => Sedan moves 120km // => Doors: 4 |
ES6 版の完全実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Vehicle { constructor(name) { this.name = name; } move(km) { console.log(`${this.name} moves ${km}km`); } } class Car extends Vehicle { constructor(name, doors) { super(name); this.doors = doors; } // オーバーライド move(km) { super.move(km); // 親メソッド呼び出し console.log(`Doors: ${this.doors}`); } } const coupe = new Car('Coupe', 2); coupe.move(80); // => Coupe moves 80km // => Doors: 2 |
オーバーライドとシャドウイングの注意点
- オーバーライドは
super.method()(ES6)またはParent.prototype.method.call(this, …)(ES5)で親実装を呼び出すことで、コード重複を防ぎつつ機能拡張が可能です。 - シャドウイングはインスタンス側に同名プロパティがあると、prototype 上の値が「隠蔽」されます。以下は典型的な例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// prototype にデフォルト速度を設定(全インスタンスで共有) Vehicle.prototype.defaultSpeed = 60; class Bike extends Vehicle { constructor(name) { super(name); this.defaultSpeed = 30; // ← インスタンス側に同名プロパティができ、prototype の値は見えなくなる } } const bike = new Bike('RoadBike'); console.log(bike.defaultSpeed); // 30(インスタンス側) console.log(Vehicle.prototype.defaultSpeed); // 60(共有値) |
ポイント:シャドウイングは意図的に使用すれば便利ですが、デバッグ時に「どこで上書きされたか」を見失いやすいので、プロパティ名は一意に保つか、コメントで明示しておくと安全です。
典型的な落とし穴とデバッグテクニック
実務で遭遇しやすいバグのパターンと、Chrome DevTools を活用したプロトタイプチェーンの可視化手順をまとめました。以下の対策を習慣化すれば、予期せぬ挙動に迅速に対応できます。
共有参照型プロパティによるバグ
|
1 2 3 4 5 6 7 8 |
function Team() {} Team.prototype.members = []; // ❌ 全インスタンスが同じ配列を参照 const a = new Team(); const b = new Team(); a.members.push('Alice'); console.log(b.members); // ['Alice'] と予期せぬ共有が発生 |
回避策はコンストラクタ内部で毎回新しいオブジェクトを作ることです。
|
1 2 3 4 |
function Team() { this.members = []; // ✅ 各インスタンスが独立した配列を保持 } |
ES6 のクラスでも同様に書けます。
|
1 2 3 4 5 6 |
class Team { constructor() { this.members = []; } } |
constructor プロパティの修正忘れ
Object.create で継承した直後、prototype.constructor が親コンストラクタを指したままだと instanceof 判定が狂います。
|
1 2 3 4 5 6 7 8 |
Car.prototype = Object.create(Vehicle.prototype); // 修正しないと false console.log(new Car('X') instanceof Car); // false // 正しい修正 Car.prototype.constructor = Car; console.log(new Car('X') instanceof Car); // true |
ES6 の class は自動で修正されますが、手作業で prototype を操作した場合は必ず確認してください。
Chrome DevTools でプロトタイプチェーンを可視化する方法
-
コンソールでオブジェクトを出力
js
console.dir(carInstance);
→ オブジェクトのツリーが展開でき、[[Prototype]]が最上位に表示されます。 -
Object.getPrototypeOfを直接呼び出す
js
console.log(Object.getPrototypeOf(carInstance));
これで現在のプロトタイプオブジェクトがコンソールに表示され、チェーンを手動でたどれます。 -
DevTools の「[[Prototype]]」ビュー
- コンソールで変数(例:
$0)を選択し、右側ペインの「プロトタイプ」タブを開く。 -
[[Prototype]]をクリックすると、その上位オブジェクトが展開され、最終的にnullまで辿れます。 -
ショートカット:
$0.__proto__と入力すれば、現在選択中の要素のプロトタイプを即座に取得できます。
ポイント:デバッグ時は「どこまで継承されているか」を視覚的に把握することが、共有参照型やオーバーライドミスの早期発見につながります。
まとめと次のアクション
- プロトタイプはオブジェクト間でロジックを共有する基盤であり、
[[Prototype]]と__proto__の違いを正しく認識しましょう。 - ES5ではコンストラクタ関数と
Object.createを組み合わせ、prototype.constructorの修正を忘れないことが継承の基本です。 - ES6+の
class extendsは内部的に同じプロトタイプリンクを生成しますが、等価な ES5 コードイメージを持つとデバッグが楽になります。 - 実務例(Vehicle → Car / Animal → Dog)はオーバーライドとシャドウイングの挙動確認に最適です。自プロジェクトのモデルに合わせて書き換えてみましょう。
- 典型的な落とし穴(共有参照型プロパティ、
constructor修正忘れ)はコンストラクタ内で初期化し、チェーン全体を DevTools で可視化する習慣で防げます。
次のステップ
- 本稿のコードを ブラウザのコンソールまたは CodePen に貼り付けて動作確認。
- 自分のドメインモデル(例:
User → Admin、Shape → Rectangle)で同様の継承構造を実装し、プロトタイプチェーンが期待通りに機能するか検証。 - デバッグ時は必ず
Object.getPrototypeOfと DevTools のプロトタイプビュー を併用し、チェーンの各段階を目視で確認する習慣をつける。
これらを実践すれば、プロトタイプ継承に関する理解が確固たるものとなり、コードの保守性とバグ耐性が格段に向上します。 Happy coding!