はじめに (対象読者・この記事でわかること)

この記事は、JavaScriptでオブジェクト指向プログラミングを行うエンジニア、特に ES6 の class 構文に慣れたが、内部実装や従来のプロトタイプ継承との違いが曖昧な方を対象としています。
読むことで、以下が理解できるようになります。

  • classextends が内部的にどのようにプロトタイプチェーンを構築しているか
  • 旧来のプロトタイプ継承と extends の挙動・初期化順序の違い
  • パフォーマンスやデバッグ時の利点・欠点を踏まえた、ケースバイケースの継承選択基準

執筆のきっかけは、チーム内で「class とプロトタイプのどちらを使うべきか」という議論が頻繁に起き、結論が出せないまま実装が分散してしまったことです。そこで、実際のコードと仕様を比較しながら整理しました。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • JavaScript の基本文法(変数、関数、オブジェクトリテラル)
  • ES6 以降の class 構文の基本的な書き方
  • ブラウザのデベロッパーツールでコンソール出力ができること

ES6 class の extends とプロトタイプ継承の概念的違い

class は構文糖衣であり、実際の継承はプロトタイプチェーンを操作して実現されています。extends が宣言されたときに行われる主な処理は次の通りです。

  1. [[Prototype]] の設定
    SubClass.__proto__SuperClass になる(静的継承)。これにより static メンバーが継承されます。
  2. prototype オブジェクトのリンク
    SubClass.prototype.__proto__SuperClass.prototype になる(インスタンス継承)。インスタンスはこのチェーンを辿ってプロパティを検索します。
  3. super キーワードの内部実装
    メソッド内部で super.method() が呼ばれると、現在の [[Prototype]](すなわち SuperClass.prototype)からメソッドを取得し、正しい this コンテキストで実行します。

一方、古典的なプロトタイプ継承は開発者が自力で上記 1・2 を行います。代表的なのは次のパターンです。

Js
function 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 での実装

Js
class 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:プロトタイプ継承で同等機能を実装

Js
function 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.nameundefined に。
  • 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 メンバーが必要な場合)

実装上のベストプラクティス

  1. class を使う場合は super() の位置に注意
    - ECMAScript が強制するため、呼び忘れはコンパイルエラーになります。
  2. プロトタイプ継承で constructor をリセット
    - Object.create 後に prototype.constructorUser に残るので、必ず Admin.prototype.constructor = Admin; を行う。
  3. 静的メンバーが必要なら手動で継承
    - Admin.staticMethod = function(){} のように個別に定義するか、Object.setPrototypeOf(Admin, User); で静的チェーンを構築する。

まとめ

本記事では、JavaScript の class における extends と従来のプロトタイプ継承の内部構造・挙動・実装方法を比較し、以下のポイントを整理しました。

  • extends は構文糖衣で、内部的に [[Prototype]]prototype.__proto__ を自動設定し、super キーワードで安全に親メンバーへアクセスできる。
  • プロトタイプ継承は手動設定が必要で柔軟性は高いが、初期化漏れや静的継承の未設定といった落とし穴がある。
  • パフォーマンス差はエンジン最適化に依存し、実務では可読性・保守性を重視して class を選択するケースが増えている。

これにより、読者はシナリオに応じて class を使うべきか、手動のプロトタイプ継承が適切か」 を判断できるようになります。今後は、ミックスミックス継承や Object.setPrototypeOf を利用した高度なパターン、さらに TypeScript での型安全継承についても記事化予定です。

参考資料