「JavaScriptの基礎 - 継承とプロトタイプ」の版間の差分

提供: MochiuWiki : SUSE, EC, PCB

169行目: 169行目:
<br>
<br>
# オブジェクト自身のプロパティを探索する。
# オブジェクト自身のプロパティを探索する。
# 見つからない場合、<code>[[Prototype]]</code> (プロトタイプ) のプロパティを探索する。
# 見つからない場合、<code><nowiki>[[Prototype]]</nowiki></code> (プロトタイプ) のプロパティを探索する。
# 見つからない場合、プロトタイプのプロトタイプを探索する。
# 見つからない場合、プロトタイプのプロトタイプを探索する。
# <code>null</code> に到達しても見つからない場合、<u>undefined</u> を返す。
# <code>null</code> に到達しても見つからない場合、<u>undefined</u> を返す。
203行目: 203行目:
  </syntaxhighlight>
  </syntaxhighlight>
<br>
<br>
==== classとプロトタイプの関係 ====
==== classとプロトタイプの関係 ====
<code>class</code> 構文はシンタックスシュガーであり、内部的にはプロトタイプベースの継承を使用している。<br>
<code>class</code> 構文はシンタックスシュガーであり、内部的にはプロトタイプベースの継承を使用している。<br>

2026年2月20日 (金) 04:42時点における版

概要

JavaScriptはプロトタイプベースのオブジェクト指向言語であり、全ての継承はプロトタイプチェーンという仕組みによって実現されている。
ES2015で導入された class 構文の extendssuper により、他の言語に近い直感的なクラス継承を記述できるが、
これはシンタックスシュガーである。(内部では依然としてプロトタイプベースの機構が動作している)

プロトタイプチェーンとは、オブジェクトが持つ内部プロパティ [[Prototype]] を辿ることで、プロパティやメソッドを継承元から順に探索する仕組みである。
プロパティは、自身のオブジェクト -> プロトタイプ -> プロトタイプのプロトタイプ -> ... -> null の順に探索され、見つからない場合は undefined を返す。

型チェックには instanceoftypeof の2つの演算子を使い分ける。
typeof はプリミティブ型の判定に用い、instanceof はプロトタイプチェーンを辿ってオブジェクトの型 (コンストラクタ) を検査する。

なお、typeof null === "object" となる点は言語仕様上の既知の問題であり、注意が必要である。

ReactのErrorBoundaryは、クラス継承が今なお必須とされる代表的な実用例である。
getDerivedStateFromErrorcomponentDidCatch はクラスコンポーネント専用のライフサイクルメソッドであり、現時点ではHooksに相当する代替手段が存在しない。

クラスの宣言、コンストラクタ、メソッド、フィールド、静的メンバーについては、JavaScriptの基礎 - クラスのページを参照すること。


クラスの継承

extends キーワードを使用することにより、既存のクラスを継承した新しいクラスを定義できる。
子クラスは親クラスのプロパティとメソッドを全て引き継ぎ、独自のメンバーを追加または上書きすることができる。

extendsキーワード

class Child extends Parent {} と記述することにより、ChildParent を継承する。

子クラスのインスタンスは、親クラスで定義された全てのプロパティとメソッドを利用できる。

extends を使用した継承の基本例を以下に示す。

 class Animal {
    constructor(name) {
       this.name = name;
    }
 
    speak() {
       console.log(this.name + " が鳴いた");
    }
 }
 
 // DogはAnimalを継承する
 class Dog extends Animal {
    constructor(name, breed) {
       super(name);        // 親クラスのコンストラクタを呼び出す
       this.breed = breed; // Dog固有のプロパティを追加する
    }
 
    // Dog 固有のメソッドを追加する
    fetch() {
       console.log(this.name + " がボールを取ってきた");
    }
 }
 
 const dog = new Dog("ポチ", "柴犬");
 dog.speak();             // "ポチ が鳴いた" (親クラスのメソッドを使用できる)
 dog.fetch();             // "ポチ がボールを取ってきた"
 console.log(dog.name);   // "ポチ"
 console.log(dog.breed);  // "柴犬"


superキーワード

super キーワードには、コンストラクタ内とメソッド内の2つの用途がある。

  • コンストラクタ内の super()
    親クラスのコンストラクタを呼び出す。
    this にアクセスするより前に呼び出す必要があり、省略すると ReferenceError が発生する。

  • メソッド内の super.method()
    親クラスのメソッドを呼び出す。
    子クラスのメソッド内で親の処理を実行してから拡張する場合に使用する。


super()super.method() の両方を示す例を以下に示す。

 class Vehicle {
    constructor(make, speed) {
       this.make  = make;
       this.speed = speed;
    }
 
    describe() {
       return this.make + " (最高速度: " + this.speed + " km/h)";
    }
 }
 
 class ElectricCar extends Vehicle {
    constructor(make, speed, range) {
       super(make, speed);  // 親コンストラクタを呼び出してから this にアクセスする
       this.range = range;  // ElectricCar 固有のプロパティ
    }
 
    describe() {
       // super.describe() で親のメソッドを呼び出してから拡張する
       return super.describe() + " / 航続距離: " + this.range + " km";
    }
 }
 
 const ev = new ElectricCar("BEV", 200, 500);
 console.log(ev.describe());
 // "BEV (最高速度: 200 km/h) / 航続距離: 500 km"


メソッドのオーバーライド

子クラスで親クラスと同名のメソッドを定義すると、親のメソッドがオーバーライド (上書き) される。
super.method() を呼び出してから処理を追加することにより、親の実装を再利用しつつ拡張することができる。

オーバーライドの例を以下に示す。

 class Logger {
    log(message) {
       console.log(message);
    }
 }
 
 // Loggerを継承してタイムスタンプ付きのログに拡張する
 class TimestampedLogger extends Logger {
    log(message) {
       // 親のlogを呼び出してから拡張する
       const timestamp = new Date().toISOString();
       super.log("[" + timestamp + "] " + message);
    }
 }
 
 const logger = new TimestampedLogger();
 logger.log("アプリケーションが起動しました");
 // "[2026-02-19T00:00:00.000Z] アプリケーションが起動しました"



プロトタイプチェーン

プロトタイプチェーンは、JavaScriptのプロトタイプベースの継承の核心となる仕組みである。
class 構文が導入された後も、継承の内部メカニズムはプロトタイプチェーンによって実現されている。

この仕組みを理解することで、JavaScriptのオブジェクトモデルをより深く把握できる。

プロトタイプの基本概念

全てのJavaScriptオブジェクトは、内部プロパティ [[Prototype]] を持つ。
この内部プロパティは、Object.getPrototypeOf() を使用して取得できる。

非推奨の __proto__ プロパティも同様の機能を持つが、コードでは Object.getPrototypeOf() の使用を推奨する。

プロトタイプチェーンの終端は null であり、Object.prototype のプロトタイプが null となる。

Object.getPrototypeOf() でチェーンを辿る例を以下に示す。

 class Animal {
    speak() {
       console.log("鳴く");
    }
 }
 
 class Dog extends Animal {}
 
 const dog = new Dog();
 
 // プロトタイプチェーンを確認する
 console.log(Object.getPrototypeOf(dog) === Dog.prototype);                  // true
 console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype);     // true
 console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype);  // true
 console.log(Object.getPrototypeOf(Object.prototype));                       // null (チェーンの終端)


プロトタイプチェーンの仕組み

オブジェクトのプロパティやメソッドにアクセスすると、JavaScriptエンジンは以下の順序で探索を行う。

  1. オブジェクト自身のプロパティを探索する。
  2. 見つからない場合、[[Prototype]] (プロトタイプ) のプロパティを探索する。
  3. 見つからない場合、プロトタイプのプロトタイプを探索する。
  4. null に到達しても見つからない場合、undefined を返す。


子のプロパティが親の同名プロパティを隠す現象をプロパティのシャドウイングと呼ぶ。

多段継承でのプロパティ探索の実演を以下に示す。

 class A {
    hello() { return "Aのhello"; }
    common() { return "Aのcommon"; }
 }
 
 class B extends A {
    hello() { return "Bのhello"; }  // Aのhelloをシャドウイングする
 }
 
 class C extends B {
    // helloもcommonも定義しない
 }
 
 const c = new C();
 
 // helloはCにない → Bにある → Bのhelloを使用する
 console.log(c.hello());   // "Bのhello"
 
 // commonはCにない → Bにない → Aにある → Aのcommonを使用する
 console.log(c.common());  // "Aのcommon"
 
 // unknownは、CにもBにもAにもObject.prototypeにもない
 console.log(c.unknown);   // undefined


classとプロトタイプの関係

class 構文はシンタックスシュガーであり、内部的にはプロトタイプベースの継承を使用している。

class 構文とプロトタイプベースの等価なコードを以下に示す。

 // class 構文
 class Animal {
    constructor(name) {
       this.name = name;
    }
 
    speak() {
       console.log(this.name + " が鳴く");
    }
 }
 
 class Dog extends Animal {
    constructor(name, breed) {
       super(name);
       this.breed = breed;
    }
 }
 
 // 上記と等価なプロトタイプベースのコード
 function AnimalFn(name) {
    this.name = name;
 }
 AnimalFn.prototype.speak = function() {
    console.log(this.name + " が鳴く");
 };
 
 function DogFn(name, breed) {
    AnimalFn.call(this, name);  // super(name) に相当する
    this.breed = breed;
 }
 // プロトタイプチェーンを設定する (extendsに相当する)
 Object.setPrototypeOf(DogFn.prototype, AnimalFn.prototype);


下表に、class 構文とプロトタイプベースの対応関係を示す。

class構文とプロトタイプの対応表
class構文 プロトタイプベースの等価コード 説明
class Animal {} function Animal() {} クラス宣言はコンストラクタ関数に対応する
constructor(name) { this.name = name; } function Animal(name) { this.name = name; } コンストラクタ本体はそのまま関数本体になる
speak() { ... } Animal.prototype.speak = function() { ... }; インスタンスメソッドはprototypeに追加される
static create() { ... } Animal.create = function() { ... }; 静的メソッドはコンストラクタ関数自体のプロパティになる
class Dog extends Animal Object.setPrototypeOf(Dog.prototype, Animal.prototype); extendsはプロトタイプチェーンを設定する
super(name) Animal.call(this, name); super()は親コンストラクタをthisに対して呼び出す
super.speak() Animal.prototype.speak.call(this); super.method()は親のprototypeのメソッドを呼び出す



型チェック

JavaScriptでオブジェクトの型を検査するには、instanceoftypeof の2つの演算子を使い分ける。
それぞれ用途が異なるため、適切に選択することが重要である。

instanceof 演算子

instanceof 演算子は、オブジェクトがあるコンストラクタ (クラス) のインスタンスであるかどうかを、プロトタイプチェーンを辿って検査する。
子クラスのインスタンスは親クラスの instanceof でも true を返すため、継承チェーン全体を検査できる。

instanceof の挙動を示す例を以下に示す。

 class Animal {}
 class Dog extends Animal {}
 class Cat extends Animal {}
 
 const dog = new Dog();
 
 console.log(dog instanceof Dog);     // true  (Dogのインスタンスである)
 console.log(dog instanceof Animal);  // true  (プロトタイプチェーンを辿ってAnimalも見つかる)
 console.log(dog instanceof Cat);     // false (Catのインスタンスではない)
 console.log(dog instanceof Object);  // true  (全てのオブジェクトはObjectのインスタンスである)


typeof との違い

typeof はプリミティブ型の判定に使用し、instanceof はオブジェクトの型 (コンストラクタ) の判定に使用する。
typeof null === "object" となる点は言語仕様上の既知の問題 (バグ) であり、nullチェックには === null を使用する必要がある。

下表に、代表的な値に対する typeofinstanceof Object の結果を示す。

typeof vs instanceof 比較表
typeof の結果 instanceof Object
null "object" (既知の問題) false
undefined "undefined" false
"文字列" "string" false
42 "number" false
true "boolean" false
{} "object" true
[] "object" true
function() {} "function" true
new Date() "object" true
new Dog() "object" true


型チェックの使い分けの例を以下に示す。

 function processValue(value) {
    // null チェック: typeof では "object" になるため === null を使用する
    if (value === null) {
       console.log("null です");
       return;
    }
 
    // プリミティブ型の判定には typeof を使用する
    if (typeof value === "string") {
       console.log("文字列: " + value.toUpperCase());
       return;
    }
 
    if (typeof value === "number") {
       console.log("数値: " + value.toFixed(2));
       return;
    }
 
    // オブジェクトの型の判定には instanceof を使用する
    if (value instanceof Date) {
       console.log("日付: " + value.toISOString());
       return;
    }
 
    if (Array.isArray(value)) {
       // 配列チェックには Array.isArray() が確実である
       console.log("配列 (要素数: " + value.length + ")");
       return;
    }
 
    console.log("オブジェクト");
 }
 
 processValue(null);         // "null です"
 processValue("hello");      // "文字列: HELLO"
 processValue(3.14159);      // "数値: 3.14"
 processValue(new Date());   // "日付: 2026-02-19T..."
 processValue([1, 2, 3]);    // "配列 (要素数: 3)"
 processValue({ x: 1 });     // "オブジェクト"



ReactのErrorBoundaryとクラス継承

Reactでは関数コンポーネントとHooksが主流となっているが、ErrorBoundaryは現在もクラスコンポーネントで実装しなければならない機能の代表例である。

これは、エラーキャッチに必要なライフサイクルメソッドがHooksで代替できないためであり、クラス継承が実際の開発において今なお不可欠であることを示している。

ErrorBoundaryとは

ErrorBoundaryは、子コンポーネントツリーのレンダリングエラーをキャッチしてアプリケーション全体のクラッシュを防ぎ、フォールバックUIを表示するコンポーネントである。

  • キャッチできるエラー
    レンダリング中に発生したエラー
    ライフサイクルメソッド内で発生したエラー
    コンストラクタ内で発生したエラー

  • キャッチできないエラー
    イベントハンドラ内で発生したエラー (try / catchで対処する)
    非同期コード内で発生したエラー (Promiseのrejection等)


クラス継承が必要な理由

ErrorBoundaryには以下の2つのライフサイクルメソッドが必要であり、いずれもクラスコンポーネント専用である。

各メソッドの動作
メソッド 動作
static getDerivedStateFromError(error) エラー発生時に状態を更新してフォールバックUIをレンダリングするために使用する。
静的メソッドであり、static キーワードを付けて定義する。
componentDidCatch(error, errorInfo) エラーの詳細情報をログに記録するために使用する。
エラーのスタックトレース等の情報 (errorInfo.componentStack) を参照できる。


React Hooksには、getDerivedStateFromErrorcomponentDidCatch に相当する機能が存在しない。
そのため、ErrorBoundaryは現在もクラスコンポーネントで実装する必要がある。

ErrorBoundaryの実装例

  • ErrorBoundaryの実装例
     import React from "react";
     
     class ErrorBoundary extends React.Component {
        constructor(props) {
           super(props);
           this.state = {
              hasError : false,
              error    : null,
           };
        }
     
        // エラー発生時にstateを更新してフォールバックUIをレンダリングする
        static getDerivedStateFromError(error) {
           return {
              hasError : true,
              error    : error,
           };
        }
     
        // エラーの詳細情報をログに記録する
        componentDidCatch(error, errorInfo) {
           console.error("ErrorBoundary がエラーをキャッチしました:", error);
           console.error("コンポーネントスタック:", errorInfo.componentStack);
     
           // 外部エラートラッキングサービスへの送信例
           // reportError(error, errorInfo);
        }
     
        // エラー状態をリセットしてコンテンツを再表示する
        handleReset = () => {
           this.setState({ hasError: false, error: null });
        };
     
        render() {
           if (this.state.hasError) {
              return (
                 <div>
                    <h2>予期しないエラーが発生しました</h2>
                    <p>{this.state.error?.message}</p>
                    <button onClick={this.handleReset}>再試行</button>
                 </div>
              );
           }
     
           // エラーがない場合は子コンポーネントをそのまま表示する
           return this.props.children;
        }
     }
     
     export default ErrorBoundary;
    

  • ErrorBoundaryの使用例
     import React from "react";
     import ErrorBoundary from "./ErrorBoundary";
     import UserProfile from "./UserProfile";
     import DataTable from "./DataTable";
     
     function App() {
        return (
           <div>
              {/* アプリケーション全体をラップする */}
              <ErrorBoundary>
                 <UserProfile userId={1} />
              </ErrorBoundary>
     
              {/* 特定のセクションだけをラップすることもできる */}
              <ErrorBoundary>
                 <DataTable dataSource="api/records" />
              </ErrorBoundary>
           </div>
        );
     }
     
     export default App;
    



関連情報