JavaScript継承

2025-07-28

はじめに

前章までにクラス、プロパティ、メソッドの基本を学びました。この章では、オブジェクト指向プログラミングの重要な概念である「継承」について詳しく解説します。継承を理解することで、コードの再利用性を高め、より効率的なプログラム設計が可能になります。

継承の基本概念

継承(Inheritance)とは、既存のクラスを基にして新しいクラスを作成する仕組みです。親クラス(スーパークラス)の特性を子クラス(サブクラス)が引き継ぐことで、以下のメリットが得られます。

  1. コードの再利用:既存のコードを再利用できる
  2. 階層的な分類:現実世界の関係性を自然に表現できる
  3. 拡張性:親クラスの機能を拡張できる
  4. 一貫性:共通のインターフェースを維持できる

基本的な継承の例

// 親クラス(スーパークラス)
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name}が音を出します`);
  }
}

// 子クラス(サブクラス) - extendsで継承
class Dog extends Animal {
  constructor(name) {
    super(name); // 親クラスのコンストラクタを呼び出し
  }

  // メソッドのオーバーライド
  speak() {
    console.log(`${this.name}が吠えます:ワンワン!`);
  }
}

const myDog = new Dog('ポチ');
myDog.speak(); // "ポチが吠えます:ワンワン!"

継承の詳細な仕組み

extendsキーワード

extendsキーワードを使ってクラスを継承します:

class ChildClass extends ParentClass {
  // クラス定義
}

superキーワードの役割

superには2つの使用方法があります:

  1. コンストラクタ内でのsuper():親クラスのコンストラクタを呼び出す
  • 子クラスのコンストラクタでは、thisを使う前にsuper()を呼び出す必要がある
  1. メソッド内でのsuper:親クラスのメソッドを呼び出す
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name}が音を出します`);
  }
}

class Cat extends Animal {
  constructor(name, color) {
    super(name); // 親クラスのコンストラクタ呼び出し
    this.color = color;
  }

  speak() {
    super.speak(); // 親クラスのspeakメソッド呼び出し
    console.log(`${this.name}が鳴きます:ニャー!`);
  }

  describe() {
    return `${this.color}色の猫、${this.name}`;
  }
}

const myCat = new Cat('タマ', '茶');
myCat.speak();
/*
"タマが音を出します"
"タマが鳴きます:ニャー!"
*/
console.log(myCat.describe()); // "茶色の猫、タマ"

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

子クラスで同じ名前のメソッドを定義すると、**親クラスのメソッドを上書き(オーバーライド)**できます。

class Animal {
  speak() {
    console.log("Animal is making a sound");
  }
}

class Dog extends Animal {
  // オーバーライド
  speak() {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog();
dog.speak(); // "Woof! Woof!"

このように、Dogクラスでspeakを再定義すると、親のspeakは呼ばれなくなります。

では次に子クラスから親クラスのメソッドを呼び出す(super)方法です。

オーバーライドしたメソッドの中から、親クラスのメソッドを呼びたい場合はsuperを使います。

class Animal {
  speak() {
    console.log("Animal is making a sound");
  }
}

class Dog extends Animal {
  speak() {
    super.speak(); // 親クラスのメソッドを呼ぶ
    console.log("Woof! Woof!");
  }
}

const dog = new Dog();
dog.speak();
// 出力:
// Animal is making a sound
// Woof! Woof!

継承の実践的な使用例

次に実際の継承使用例を紹介します。

例1:図形クラスの継承

Shape クラスは「図形」の親クラスで、色 (color) を持ち、面積を求める getArea() メソッドと図形の説明文を返す describe() メソッドを定義します。getArea() は具体的な計算式を持たず、「サブクラスで必ず実装するべき」という意図でエラーを投げます。

// 親クラス:図形
class Shape {
  constructor(color) {
    this.color = color;
  }

  getArea() {
    throw new Error('このメソッドはサブクラスで実装する必要があります');
  }

  describe() {
    return `この図形の色は${this.color}で、面積は${this.getArea()}です`;
  }
}

// 子クラス:円
class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }

  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

// 子クラス:四角形
class Rectangle extends Shape {
  constructor(color, width, height) {
    super(color);
    this.width = width;
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

// 使用例
const redCircle = new Circle('赤', 5);
console.log(redCircle.describe()); // "この図形の色は赤で、面積は78.53981633974483です"

const blueRectangle = new Rectangle('青', 4, 6);
console.log(blueRectangle.describe()); // "この図形の色は青で、面積は24です"

次に、Circle クラスは Shape を継承し、半径 (radius) を使って円の面積を計算する getArea() を実装します。
同様に、Rectangle クラスは Shape を継承し、幅 (width) と高さ (height) から四角形の面積を計算します。

最後に、CircleRectangle のインスタンスを作成し、describe() を呼び出すことで、それぞれの色と面積を含む説明文がコンソールに出力されます。

例2:従業員管理システム

このコードは、JavaScriptのクラス継承を用いて従業員を表す親クラスと、その役割ごとの子クラスを定義し、役割ごとの機能や動作を表現したものです。親クラスであるEmployeeは従業員の名前、ID、部署を管理し、共通の動作として仕事をするworkメソッドと詳細情報を文字列で返すgetDetailsメソッドを持ちます。

// 親クラス:従業員
class Employee {
  constructor(name, id, department) {
    this.name = name;
    this.id = id;
    this.department = department;
  }

  work() {
    console.log(`${this.name}は仕事をしています`);
  }

  getDetails() {
    return `名前: ${this.name}, ID: ${this.id}, 部署: ${this.department}`;
  }
}

// 子クラス:マネージャー
class Manager extends Employee {
  constructor(name, id, department, teamSize) {
    super(name, id, department);
    this.teamSize = teamSize;
  }

  conductMeeting() {
    console.log(`${this.name}は${this.teamSize}人のチームで会議を主催しています`);
  }

  // メソッドのオーバーライド
  work() {
    console.log(`${this.name}はマネジメント業務を行っています`);
  }

  // 親クラスのメソッドを拡張
  getDetails() {
    return `${super.getDetails()}, チームサイズ: ${this.teamSize}`;
  }
}

// 子クラス:開発者
class Developer extends Employee {
  constructor(name, id, department, programmingLanguage) {
    super(name, id, department);
    this.programmingLanguage = programmingLanguage;
  }

  writeCode() {
    console.log(`${this.name}は${this.programmingLanguage}でコードを書いています`);
  }

  work() {
    console.log(`${this.name}は開発作業を行っています`);
  }
}

// 使用例
const manager = new Manager('山田太郎', 'M001', 'IT', 5);
manager.work(); // "山田太郎はマネジメント業務を行っています"
manager.conductMeeting(); // "山田太郎は5人のチームで会議を主催しています"
console.log(manager.getDetails()); // "名前: 山田太郎, ID: M001, 部署: IT, チームサイズ: 5"

const developer = new Developer('佐藤花子', 'D001', 'IT', 'JavaScript');
developer.work(); // "佐藤花子は開発作業を行っています"
developer.writeCode(); // "佐藤花子はJavaScriptでコードを書いています"
console.log(developer.getDetails()); // "名前: 佐藤花子, ID: D001, 部署: IT"

ManagerクラスはEmployeeを継承してチームサイズを追加し、会議を主催するconductMeetingメソッドを持ち、さらに親クラスのworkメソッドをマネジメント業務向けに書き換え、getDetailsメソッドを拡張してチームサイズも表示するようにしています。

DeveloperクラスはEmployeeを継承してプログラミング言語を追加し、コードを書くwriteCodeメソッドを持ち、workメソッドを開発作業向けに変更しています。最後に、ManagerとDeveloperのインスタンスを作成し、それぞれの役割に応じた動作や詳細表示が正しく動く様子をコンソール出力で確認しています。

継承の高度なテクニック

プロトタイプチェーンの理解

JavaScriptの継承はプロトタイプチェーンによって実現されています。

これは、あるオブジェクトのプロパティやメソッドを探すときに、そのオブジェクト自身になければ__proto__(内部的には[[Prototype]])をたどって親オブジェクトを探し、さらにその親のプロトタイプへ…と連続的にたどっていく仕組みです。

サブクラスのインスタンスはまず自分のプロパティを見て、なければ親クラス(スーパークラス)のprototype、さらにObject.prototypeまで順に検索します。このつながりがチェーン状になっているため「プロトタイプチェーン」と呼ばれます。

class Parent {
  parentMethod() {
    console.log('親メソッド');
  }
}

class Child extends Parent {
  childMethod() {
    console.log('子メソッド');
  }
}

class Unrelated {
  otherMethod() {
    console.log('無関係クラス');
  }
}

const child = new Child();

// --- instanceof の例 ---
console.log(child instanceof Child);      // true: child は Child のインスタンス
console.log(child instanceof Parent);     // true: Child は Parent を継承している
console.log(child instanceof Object);     // true: すべてのクラスは Object を継承している
console.log(child instanceof Unrelated);  // false: 継承関係がない

// --- プロトタイプチェーンの例 ---
console.log(Object.getPrototypeOf(child) === Child.prototype);        
// true: child の直接のプロトタイプは Child.prototype

console.log(Object.getPrototypeOf(Child.prototype) === Parent.prototype); 
// true: Child.prototype のプロトタイプは Parent.prototype

console.log(Object.getPrototypeOf(Parent.prototype) === Object.prototype); 
// true: Parent.prototype のプロトタイプは Object.prototype

// falseになるパターン
console.log(Object.getPrototypeOf(child) === Parent.prototype); 
// false: child の直接のプロトタイプは Child.prototype であり Parent.prototype ではない

console.log(Object.getPrototypeOf(Child.prototype) === Object.prototype); 
// false: Child.prototype のプロトタイプは Parent.prototype

console.log(Object.getPrototypeOf(Unrelated.prototype) === Parent.prototype); 
// false: Unrelated.prototype は Parent.prototype とは関係がない

静的メソッドの継承

静的メソッドも継承されます。

class Parent {
  static parentStaticMethod() {
    console.log('親の静的メソッド');
  }
}

class Child extends Parent {
  static childStaticMethod() {
    console.log('子の静的メソッド');
  }
}

Child.parentStaticMethod(); // "親の静的メソッド"
Child.childStaticMethod(); // "子の静的メソッド"

プライベートメンバーと継承

プライベートフィールド(#で始まるプロパティ)は継承されません:

class Parent {
  #parentSecret = '親のプライベート';

  getParentSecret() {
    return this.#parentSecret;
  }
}

class Child extends Parent {
  #childSecret = '子のプライベート';

  getChildSecret() {
    return this.#childSecret;
  }

  // 親のプライベートに直接アクセスしようとする例
  tryAccessParentSecret() {
    return this.#parentSecret; // エラーになる!
  }
}

const child = new Child();

console.log(child.getParentSecret()); // "親のプライベート"
console.log(child.getChildSecret());  // "子のプライベート"

console.log(child.tryAccessParentSecret()); 
// SyntaxError: Private field '#parentSecret' must be declared in an enclosing class

ミックスイン(Mixins)

JavaScriptでは多重継承はサポートされていませんが、ミックスインパターンで似たようなことが実現できます:

あるオブジェクトやクラスに、他のオブジェクトから機能(メソッドやプロパティ)を“混ぜ込む”設計手法です。

JavaScriptでは多重継承ができないため、継承ではなくオブジェクトのコピー(Object.assignなど)を使って機能を追加します。これにより、異なるクラス間で共通の機能を簡単に共有できます。

// --- ミックスイン関数の定義 ---
// Serializable: オブジェクトを JSON 文字列に変換する機能を追加する
const Serializable = (Base) => class extends Base {
  serialize() {
    // this(インスタンス)を JSON 文字列に変換
    return JSON.stringify(this);
  }
};

// Loggable: オブジェクトを整形してコンソールに出力する機能を追加する
const Loggable = (Base) => class extends Base {
  log() {
    // this を見やすい形(インデント付き)でログ出力
    console.log(JSON.stringify(this, null, 2));
  }
};

// --- ベースクラス ---
// User クラスは name プロパティだけを持つ、シンプルなクラス
class User {
  constructor(name) {
    this.name = name;
  }
}

// --- ミックスインの適用 ---
// ここで User に「Serializable」と「Loggable」の機能を追加
// Serializable(User) → User を拡張して serialize() を持つクラスを作る
// Loggable(Serializable(User)) → さらに log() も追加したクラスを作る
const LoggableSerializableUser = Loggable(Serializable(User));

// --- 実際に使ってみる ---
// インスタンスを作成
const user = new LoggableSerializableUser('田中');

// serialize(): インスタンスを JSON 文字列に変換
console.log(user.serialize()); 
// 出力: "{"name":"田中"}"

// log(): インスタンスをきれいに表示(デバッグ用に便利)
user.log();
/*
出力:
{
  "name": "田中"
}
*/

継承のベストプラクティス

1. 「is-a」関係を確認

継承を使用する前に、子クラスと親クラスの間に「is-a」関係があることを確認してください。

例えば、「Dog is an Animal」という関係が成り立つので、DogがAnimalを継承するのは適切です。

2. 深すぎる継承階層を避ける

継承階層が深くなりすぎると、コードの理解と保守が難しくなります。一般的に3レベル以上は避けるべきです。

3. コンポジションを検討

継承よりもオブジェクトの組み合わせ(コンポジション)の方が適している場合があります:

// 継承を使う場合(不適切な例)
class Car extends Engine {} // 「Car is an Engine」は不自然

// コンポジションを使う場合(適切な例)
class Car {
  constructor(engine) {
    this.engine = engine; // Car has an Engine
  }
}

4. 抽象基底クラス

JavaScriptには JavaやC#のような「抽象クラス」「抽象メソッド」を言語仕様として定義する仕組みはありません。ですが共通のインターフェースを強制したい場合、抽象基底クラスのパターンが使えます。

コンストラクタでのインスタンス生成禁止することで「このクラスは直接使わず、必ず継承して使ってね」という規定を設けています。

class AbstractShape {
  constructor() {
    if (new.target === AbstractShape) {
      throw new Error('AbstractShapeは抽象クラスで、インスタンス化できません');
    }
  }

  getArea() {
    throw new Error('getAreaメソッドは実装する必要があります');
  }
}

class Circle extends AbstractShape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  getArea() {
    return Math.PI * this.radius ** 2;
  }
}

// const shape = new AbstractShape(); // エラー
const circle = new Circle(5);
console.log(circle.getArea()); // 78.53981633974483

そのため、このコードでやっている「抽象的な規定」は、開発者が自分でルールを作って実装しているにすぎません。

よくある間違いと対処法

1. super()の呼び忘れ

子クラスのコンストラクタでsuper()を呼び出すのを忘れるとエラーになります:

class Parent {
  constructor(name) {
    this.name = name;
  }
}

class Child extends Parent {
  constructor(name, age) {
    // super(name); を忘れるとエラー
    this.age = age;
  }
}

// const child = new Child('太郎', 10); // ReferenceError

2. 不適切なメソッドオーバーライド

親クラスのメソッドをオーバーライドする際に、意図せず重要な機能を削除してしまう:

class Database {
  save(data) {
    this.validate(data);
    this.logSaveOperation(data);
    // 実際の保存処理...
  }

  validate(data) { /* ... */ }
  logSaveOperation(data) { /* ... */ }
}

class NoLogDatabase extends Database {
  save(data) {
    // ログ記録を忘れてしまった
    this.validate(data);
    // 実際の保存処理...
  }
}

解決策:superを使って親クラスの機能を活用する

class BetterNoLogDatabase extends Database {
  save(data) {
    this.validate(data);
    // ログ記録をスキップ
    // 実際の保存処理...
  }
}

3. 継承の乱用

全ての関係に継承を使おうとすると、複雑で柔軟性のない設計になります:

// 悪い例:不自然な継承階層
class AdminUser extends LoginForm extends DatabaseConnection extends HTTPRequest {}

ブラウザAPIでの継承の例

DOM(Document Object Model)でも継承が使われています:

// DOMの継承階層の例
class EventTarget {
  // 基底クラス
  addEventListener() { /* ... */ }
  removeEventListener() { /* ... */ }
  dispatchEvent() { /* ... */ }
}

class Node extends EventTarget {
  // ノード関連のメソッド...
}

class Element extends Node {
  // 要素関連のメソッド...
}

class HTMLElement extends Element {
  // HTML要素共通のメソッド...
}

class HTMLButtonElement extends HTMLElement {
  // ボタン固有のメソッド...
}

// 実際の使用例
const button = document.querySelector('button');
console.log(button instanceof HTMLButtonElement); // true
console.log(button instanceof HTMLElement); // true
console.log(button instanceof Element); // true
console.log(button instanceof Node); // true
console.log(button instanceof EventTarget); // true

ES5での継承(クラス構文以前)

クラス構文が導入される前のES5では、プロトタイプを使って継承を実現していました:

// 親コンストラクタ
function Animal(name) {
  this.name = name;
}

// 親のメソッド
Animal.prototype.speak = function() {
  console.log(this.name + ' makes a noise.');
};

// 子コンストラクタ
function Dog(name) {
  Animal.call(this, name); // 親のコンストラクタを呼び出す
}

// プロトタイプチェーンを設定
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 子のメソッド
Dog.prototype.speak = function() {
  console.log(this.name + ' barks.');
};

var dog = new Dog('ポチ');
dog.speak(); // "ポチ barks."

まとめ

この章では、JavaScriptの継承について詳細に学びました。

継承を適切に使用することで、コードの再利用性と拡張性を大幅に向上させることができます。しかし、過度な継承はコードを複雑にするため、状況に応じてコンポジション(オブジェクトの組み合わせ)も検討することが重要です。


演習問題

初級問題

問題1: 次のコードの出力結果は何ですか?

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a noise.`;
  }
}

class Dog extends Animal {
  speak() {
    return `${this.name} barks.`;
  }
}

const dog = new Dog('Rex');
console.log(dog.speak());

問題2: extendsキーワードの役割を説明してください。

問題3: 次のコードの出力結果は何ですか?

class Vehicle {
  constructor(type) {
    this.type = type;
  }
  info() {
    return `This is a ${this.type}.`;
  }
}

class Car extends Vehicle {
  constructor(type, brand) {
    super(type);
    this.brand = brand;
  }
}

const myCar = new Car('car', 'Toyota');
console.log(myCar.info());

中級問題

問題4: 次のコードの出力結果は何ですか?また、その理由を説明してください。

class A {
  method() {
    return 'A';
  }
}

class B extends A {
  method() {
    return super.method() + 'B';
  }
}

class C extends B {
  method() {
    return super.method() + 'C';
  }
}

const instance = new C();
console.log(instance.method());

問題5: 次のコードを修正して、正しく”Cat meows.”と出力されるようにしてください。

class Animal {
  constructor(name) {
    this.name = name;
  }
  sound() {
    return `${this.name} makes a noise.`;
  }
}

class Cat extends Animal {
  sound() {
    // ここを修正
  }
}

const cat = new Cat('Cat');
console.log(cat.sound());

問題6: コンストラクタ関数を使った継承の例を書いてください。親コンストラクタはPerson、子コンストラクタはEmployeeとします。

問題7: 次のコードの出力結果は何ですか?

class Parent {
  static staticMethod() {
    return 'Parent static';
  }

  instanceMethod() {
    return 'Parent instance';
  }
}

class Child extends Parent {
  static staticMethod() {
    return super.staticMethod() + ' -> Child static';
  }

  instanceMethod() {
    return super.instanceMethod() + ' -> Child instance';
  }
}

console.log(Child.staticMethod());
const child = new Child();
console.log(child.instanceMethod());

問題8: instanceof演算子の役割と、プロトタイプチェーンにおけるその振る舞いを説明してください。

問題9: 次のコードの出力結果は何ですか?

class Shape {
  constructor(color) {
    this.color = color;
  }
  getInfo() {
    return `Color: ${this.color}`;
  }
}

class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }
  getInfo() {
    return `${super.getInfo()}, Radius: ${this.radius}`;
  }
}

const circle = new Circle('red', 5);
console.log(circle.getInfo());

上級問題

問題10: ミックスイン(Mixin)パターンを使って、複数のクラスから機能を継承する方法をコード例で示してください。

問題11: 次のコードの出力結果は何ですか?また、その理由を説明してください。

class Base {
  constructor() {
    this.baseProp = 'base';
  }
}

class Derived extends Base {
  constructor() {
    console.log(this.baseProp);
    super();
  }
}

try {
  new Derived();
} catch (e) {
  console.log(e.message);
}

問題12: Object.create()を使ったプロトタイプ継承の例を、コンストラクタ関数を使わずに実装してください。


解答例

初級問題の解答

解答1:

Rex barks.

解答2:
extendsキーワードはクラスを別のクラスの子クラスとして定義するために使用します。これにより、親クラスのプロパティとメソッドを継承できます。

解答3:

This is a car.

中級問題の解答

解答4:

ABC

理由: 各クラスが親クラスのメソッドを呼び出し(super.method())、自身の文字を追加しているため。

解答5:

class Cat extends Animal {
  sound() {
    return `${this.name} meows.`;
  }
}

解答6:

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`;
};

function Employee(name, position) {
  Person.call(this, name);
  this.position = position;
}

Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;

const emp = new Employee('John', 'Developer');
console.log(emp.greet());

解答7:

Parent static -> Child static
Parent instance -> Child instance

解答8:
instanceof演算子は、オブジェクトが特定のコンストラクタのインスタンスかどうかをチェックします。プロトタイプチェーンをたどって検査するため、親コンストラクタに対してもtrueを返します。

解答9:

Color: red, Radius: 5

上級問題の解答

解答10:

const WalkMixin = {
  walk() {
    return `${this.name} is walking.`;
  }
};

const SwimMixin = {
  swim() {
    return `${this.name} is swimming.`;
  }
};

class Animal {
  constructor(name) {
    this.name = name;
  }
}

Object.assign(Animal.prototype, WalkMixin);

class Fish extends Animal {}
Object.assign(Fish.prototype, SwimMixin);

const nemo = new Fish('Nemo');
console.log(nemo.swim());
console.log(nemo.walk()); // エラー、FishにはWalkMixinを適用していない

解答11:

Must call super constructor in derived class before accessing 'this' or returning from derived constructor

理由: 派生クラスのコンストラクタでsuper()を呼び出す前にthisにアクセスしようとしたため。

解答12:

const animal = {
  init(name) {
    this.name = name;
    return this;
  },
  speak() {
    return `${this.name} makes a noise.`;
  }
};

const dog = Object.create(animal).init('Rex');
dog.bark = function() {
  return `${this.name} barks.`;
};

console.log(dog.speak());
console.log(dog.bark());