はじめに (対象読者・この記事でわかること)
この記事は、JavaScriptでオブジェクト指向プログラミングを行うエンジニア、特に ES6 の class 構文に慣れたが、内部実装や従来のプロトタイプ継承との違いが曖昧な方を対象としています。
読むことで、以下が理解できるようになります。
classのextendsが内部的にどのようにプロトタイプチェーンを構築しているか- 旧来のプロトタイプ継承と
extendsの挙動・初期化順序の違い - パフォーマンスやデバッグ時の利点・欠点を踏まえた、ケースバイケースの継承選択基準
執筆のきっかけは、チーム内で「class とプロトタイプのどちらを使うべきか」という議論が頻繁に起き、結論が出せないまま実装が分散してしまったことです。そこで、実際のコードと仕様を比較しながら整理しました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- JavaScript の基本文法(変数、関数、オブジェクトリテラル)
- ES6 以降の
class構文の基本的な書き方 - ブラウザのデベロッパーツールでコンソール出力ができること
ES6 class の extends とプロトタイプ継承の概念的違い
class は構文糖衣であり、実際の継承はプロトタイプチェーンを操作して実現されています。extends が宣言されたときに行われる主な処理は次の通りです。
- [[Prototype]] の設定
SubClass.__proto__がSuperClassになる(静的継承)。これによりstaticメンバーが継承されます。 - prototype オブジェクトのリンク
SubClass.prototype.__proto__がSuperClass.prototypeになる(インスタンス継承)。インスタンスはこのチェーンを辿ってプロパティを検索します。 superキーワードの内部実装
メソッド内部でsuper.method()が呼ばれると、現在の[[Prototype]](すなわちSuperClass.prototype)からメソッドを取得し、正しいthisコンテキストで実行します。
一方、古典的なプロトタイプ継承は開発者が自力で上記 1・2 を行います。代表的なのは次のパターンです。
Jsfunction Super(name) { this.name = name; } Super.prototype.greet = function() { console.log(`Hello, ${this.name}`); }; function Sub(name, age) { Super.call(this, name); // コンストラクタ呼び出し this.age = age; } Sub.prototype = Object.create(Super.prototype); // プロトタイプ継承 Sub.prototype.constructor = Sub;
この手法は Object.create でチェーンを作り、Super.call でコンストラクタを明示的に実行します。class が自動で行う処理を手動で書く感覚です。
主な違いのまとめ
| 項目 | class + extends |
従来のプロトタイプ継承 |
|---|---|---|
| 構文の可読性 | 高(宣言的) | 低(手続き的) |
super のサポート |
あり(自動バインド) | なし(手動で呼び出す) |
| 静的継承 | SubClass.__proto__ が自動設定 |
手動で Object.setPrototypeOf 必要 |
| 初期化順序 | super() が必須で、必ず先頭に |
任意の順序で Super.call を配置 |
| デバッグ情報 | コンストラクタ名が保持されやすい | 関数名が匿名になることがある |
| パフォーマンス | エンジン最適化が期待できる | 同等だが手書きミスで非最適化になる可能性 |
実践:extends とプロトタイプ継承を同一機能で実装して比較
以下では、「ユーザー情報を保持し、メッセージを出力する」というシンプルなユースケースを、class と従来の関数ベースで実装します。コードと実行結果、そして内部で起こるプロトタイプチェーンの変化を可視化します。
ステップ1:class での実装
Jsclass User { constructor(name) { this.name = name; } greet() { console.log(`Hi, ${this.name}!`); } } class Admin extends User { constructor(name, role) { super(name); // 必ず先頭で呼ぶ this.role = role; } greet() { super.greet(); // 親クラスの greet を呼び出す console.log(`Role: ${this.role}`); } } // テスト const a = new Admin('Alice', 'super'); a.greet(); // => Hi, Alice! // => Role: super // プロトタイプチェーンの確認 console.log(Object.getPrototypeOf(Admin) === User); // true (静的継承) console.log(Object.getPrototypeOf(a) === Admin.prototype); // true console.log(Object.getPrototypeOf(Admin.prototype) === User.prototype); // true (インスタンス継承)
ポイント解説
super(name)が必須で、thisが正しく初期化されるまで他のプロパティにアクセスできません。super.greet()によって親メソッドが呼び出され、thisはサブクラスのインスタンスを指したままです。Object.getPrototypeOfでチェーンを確認すると、extendsが自動で__proto__とprototype.__proto__を設定していることが分かります。
ステップ2:プロトタイプ継承で同等機能を実装
Jsfunction User(name) { this.name = name; } User.prototype.greet = function() { console.log(`Hi, ${this.name}!`); }; function Admin(name, role) { // 親コンストラクタを明示的に呼び出す User.call(this, name); this.role = role; } // プロトタイプチェーンを構築 Admin.prototype = Object.create(User.prototype); Admin.prototype.constructor = Admin; // メソッドオーバーライド Admin.prototype.greet = function() { // 親メソッドを手動で呼び出す User.prototype.greet.call(this); console.log(`Role: ${this.role}`); }; // テスト var b = new Admin('Bob', 'operator'); b.greet(); // => Hi, Bob! // => Role: operator // プロトタイプチェーンの確認 console.log(Object.getPrototypeOf(Admin) === Function.prototype); // true (静的継承は手動未設定) console.log(Object.getPrototypeOf(b) === Admin.prototype); // true console.log(Object.getPrototypeOf(Admin.prototype) === User.prototype); // true
ポイント解説
User.call(this, name)でコンストラクタの初期化を自分で行う必要があります。呼び忘れるとthis.nameがundefinedに。Admin.prototype = Object.create(User.prototype)がインスタンス継承の核です。Object.createが新しいオブジェクトを生成し、その[[Prototype]]をUser.prototypeに設定します。- 静的継承(
Admin.__proto__)は自動で行われないため、staticメンバーが必要な場合はObject.setPrototypeOf(Admin, User)などを追加しなければなりません。
ハマった点やエラー解決
| 発生した問題 | 原因 | 解決策 |
|---|---|---|
ReferenceError: Must call super constructor before accessing 'this' |
class のサブクラスで super() を呼び忘れた |
コンストラクタの最初行に必ず super(arguments) を記述 |
TypeError: User.prototype.greet is not a function |
プロトタイプ継承で User.prototype が上書きされていた |
Admin.prototype = Object.create(User.prototype); の後に Admin.prototype.constructor = Admin; を忘れずに設定 |
Admin.__proto__ が Function.prototype のまま |
静的継承が設定されていない | Object.setPrototypeOf(Admin, User); を追加(static メンバーが必要な場合) |
実装上のベストプラクティス
classを使う場合はsuper()の位置に注意
- ECMAScript が強制するため、呼び忘れはコンパイルエラーになります。- プロトタイプ継承で
constructorをリセット
-Object.create後にprototype.constructorがUserに残るので、必ずAdmin.prototype.constructor = Admin;を行う。 - 静的メンバーが必要なら手動で継承
-Admin.staticMethod = function(){}のように個別に定義するか、Object.setPrototypeOf(Admin, User);で静的チェーンを構築する。
まとめ
本記事では、JavaScript の class における extends と従来のプロトタイプ継承の内部構造・挙動・実装方法を比較し、以下のポイントを整理しました。
extendsは構文糖衣で、内部的に[[Prototype]]とprototype.__proto__を自動設定し、superキーワードで安全に親メンバーへアクセスできる。- プロトタイプ継承は手動設定が必要で柔軟性は高いが、初期化漏れや静的継承の未設定といった落とし穴がある。
- パフォーマンス差はエンジン最適化に依存し、実務では可読性・保守性を重視して
classを選択するケースが増えている。
これにより、読者はシナリオに応じて 「class を使うべきか、手動のプロトタイプ継承が適切か」 を判断できるようになります。今後は、ミックスミックス継承や Object.setPrototypeOf を利用した高度なパターン、さらに TypeScript での型安全継承についても記事化予定です。
参考資料
- MDN Web Docs – Classes
- ECMAScript 2015 Specification – Class Definition
- 斎藤 康毅著『JavaScript本格入門』技術評論社 (2023)
- Kyle Simpson, You Don't Know JS – this & Object Prototypes (O'Reilly, 2022)