JavaScriptのクラス継承を使ったコードの再利用方法10選

JavaScriptのクラス継承を徹底解説JS
この記事は約27分で読めます。

※本記事のコンテンツは、利用目的を問わずご活用いただけます。実務経験10000時間以上のエンジニアが監修しており、基礎知識があれば初心者にも理解していただけるように、常に解説内容のわかりやすさや記事の品質に注力しております。不具合・分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。(理解できない部分などの個別相談も無償で承っております)
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)


●クラス継承とは?

皆さん、JavaScriptのクラス継承について、どのくらいご存知でしょうか?

クラス継承は、オブジェクト指向プログラミングの重要な概念の1つで、コードの再利用性と保守性を大幅に向上させることができます。

○クラス継承のメリット

クラス継承を使うと、既存のクラスを拡張して新しいクラスを作成できます。

これにより、コードの重複を減らし、プログラムの構造をより明確にすることができるんです。

継承を活用することで、コードの修正や機能の追加がしやすくなり、開発の効率化につながります。

○クラス継承の基本構文

JavaScriptでクラス継承を実装するには、extendsキーワードを使います。

例えば、次のようにParentクラスを継承したChildクラスを定義できます。

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

  sayHello() {
    console.log(`Hello, I'm ${this.name}.`);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);
    this.age = age;
  }

  introduce() {
    console.log(`I'm ${this.name}, and I'm ${this.age} years old.`);
  }
}

ここで注目すべきは、Childクラスのコンストラクター内でsuper()を呼び出している点です。

これにより、親クラスのコンストラクターを呼び出し、nameプロパティを初期化しています。

○サンプルコード1:親クラスと子クラスの定義

それでは、実際に親クラスと子クラスを定義して、クラス継承の動作を確認してみましょう。

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

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name);
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

const animal = new Animal("Generic Animal");
const dog = new Dog("Buddy");

animal.speak(); // "Generic Animal makes a noise."
dog.speak();    // "Buddy barks."

この例では、Animalクラスを継承したDogクラスを定義しています。

Dogクラスでは、speak()メソッドをオーバーライドして、犬特有の鳴き声を出力するようにしています。

実行結果から、Animalクラスのインスタンスは”Generic Animal makes a noise.”と出力され、Dogクラスのインスタンスは”Buddy barks.”と出力されることがわかります。

●クラス継承を使ったコードの再利用方法

クラス継承の基本的な使い方について理解が深まったところで、今度はクラス継承を活用したコードの再利用方法について探っていきましょう。

コードの再利用は、開発の効率化と保守性の向上に大きく貢献します。

では、具体的にどのようにクラス継承を使ってコードを再利用できるのでしょうか?

○サンプルコード2:メソッドのオーバーライド

クラス継承を使ったコードの再利用方法の1つに、メソッドのオーバーライドがあります。

オーバーライドとは、子クラスで親クラスのメソッドを再定義することを指します。

先ほどの例でも、Dogクラスでspeak()メソッドをオーバーライドしましたね。

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

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name);
  }

  speak() {
    console.log(`${this.name} meows.`);
  }
}

const animal = new Animal("Generic Animal");
const cat = new Cat("Whiskers");

animal.speak(); // "Generic Animal makes a noise."
cat.speak();    // "Whiskers meows."

この例では、Catクラスでspeak()メソッドをオーバーライドし、猫特有の鳴き声を出力するようにしています。

こうすることで、Animalクラスの機能を再利用しつつ、Catクラス独自の振る舞いを追加できるのです。

○サンプルコード3:コンストラクターの継承

クラス継承を使うと、親クラスのコンストラクターを子クラスで再利用することもできます。

先ほどの例でも、super()を使って親クラスのコンストラクターを呼び出していましたね。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  introduce() {
    console.log(`I'm ${this.name}, and I'm ${this.age} years old.`);
  }
}

class Student extends Person {
  constructor(name, age, major) {
    super(name, age);
    this.major = major;
  }

  introduceMajor() {
    console.log(`I'm majoring in ${this.major}.`);
  }
}

const person = new Person("Alice", 25);
const student = new Student("Bob", 20, "Computer Science");

person.introduce();  // "I'm Alice, and I'm 25 years old."
student.introduce(); // "I'm Bob, and I'm 20 years old."
student.introduceMajor(); // "I'm majoring in Computer Science."

この例では、Studentクラスのコンストラクターでsuper()を使ってPersonクラスのコンストラクターを呼び出し、nameageプロパティを初期化しています。

さらに、Studentクラス独自のmajorプロパティを追加しています。

こうすることで、Personクラスの機能を再利用しつつ、Studentクラス独自の機能を追加できます。

Studentクラスのインスタンスは、Personクラスのメソッドであるintroduce()と、Studentクラス独自のメソッドであるintroduceMajor()の両方を使えるようになるのです。

○サンプルコード4:静的メソッドの継承

クラス継承を使うと、静的メソッドも継承できます。

静的メソッドとは、クラスレベルのメソッドで、インスタンスを作成せずに直接呼び出すことができます。

class MathUtils {
  static square(x) {
    return x * x;
  }
}

class AdvancedMathUtils extends MathUtils {
  static cube(x) {
    return x * x * x;
  }
}

console.log(MathUtils.square(3));       // 9
console.log(AdvancedMathUtils.square(3)); // 9
console.log(AdvancedMathUtils.cube(3));   // 27

この例では、AdvancedMathUtilsクラスがMathUtilsクラスを継承しています。

AdvancedMathUtilsクラスでは、cube()という新しい静的メソッドを追加しつつ、MathUtilsクラスのsquare()静的メソッドも使えるようになっています。

静的メソッドの継承を活用することで、関連するユーティリティ関数をグループ化し、コードの構造を改善できます。

また、親クラスの静的メソッドを再利用しつつ、子クラスで新しい静的メソッドを追加できるのも大きなメリットです。

○サンプルコード5:ゲッターとセッターの継承

クラス継承を使うと、ゲッターとセッターも継承できます。

ゲッターとセッターは、プロパティの値を読み取ったり設定したりするための特殊なメソッドです。

class Rectangle {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }

  get area() {
    return this._width * this._height;
  }

  set width(value) {
    this._width = value;
  }

  set height(value) {
    this._height = value;
  }
}

class Square extends Rectangle {
  constructor(size) {
    super(size, size);
  }

  set size(value) {
    this._width = value;
    this._height = value;
  }
}

const rectangle = new Rectangle(5, 3);
console.log(rectangle.area); // 15
rectangle.width = 10;
console.log(rectangle.area); // 30

const square = new Square(4);
console.log(square.area); // 16
square.size = 6;
console.log(square.area); // 36

この例では、SquareクラスがRectangleクラスを継承しています。

Rectangleクラスには、areaゲッターとwidthセッター、heightセッターが定義されています。

Squareクラスでは、これらのゲッターとセッターを継承しつつ、sizeセッターを追加しています。

sizeセッターを使うことで、正方形の幅と高さを一度に設定できます。

こうすることで、SquareクラスはRectangleクラスの機能を再利用しつつ、正方形特有の振る舞いを追加できるのです。

ゲッターとセッターの継承を活用することで、プロパティのアクセス方法を統一し、コードの可読性と保守性を向上できます。

また、親クラスのゲッターとセッターを再利用しつつ、子クラスで新しいゲッターとセッターを追加できるのも大きな利点ですね。

●クラス継承の応用例

クラス継承を使ったコードの再利用方法について理解が深まったところで、今度はクラス継承のさらなる応用例について見ていきましょう。

JavaScriptのクラス継承は、多重継承やミックスイン、抽象クラスやインターフェースなど、より高度なテクニックにも対応しています。

この応用例を学ぶことで、クラス継承の可能性が大きく広がります。早速、具体的なサンプルコードを交えて解説していきます。

○サンプルコード6:多重継承の実現

JavaScriptは単一継承のみをサポートしていますが、ミックスインを使うことで多重継承のような機能を実現できます。

ミックスインとは、クラスの機能を他のクラスに取り込む手法のことです。

class Flyable {
  fly() {
    console.log("I can fly!");
  }
}

class Swimmable {
  swim() {
    console.log("I can swim!");
  }
}

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

Object.assign(Duck.prototype, Flyable.prototype, Swimmable.prototype);

const duck = new Duck("Donald");
duck.fly();  // "I can fly!"
duck.swim(); // "I can swim!"

この例では、FlyableSwimmableというミックスイン用のクラスを定義しています。

それぞれのクラスには、fly()swim()というメソッドが含まれています。

Duckクラスは、nameプロパティを持つシンプルなクラスです。

Object.assign()を使って、FlyableSwimmableのプロトタイプをDuckのプロトタイプにコピーすることで、Duckクラスにこれらのメソッドを追加しています。

結果として、Duckクラスのインスタンスであるduckは、fly()swim()の両方のメソッドを呼び出すことができるようになります。

このように、ミックスインを使うことで、複数のクラスの機能を組み合わせることができるのです。

○サンプルコード7:ミックスインの活用

ミックスインは、コードの再利用性を高めるのに非常に有効です。

特に、複数のクラスで共通の機能を共有したい場合に便利です。

const Serializable = {
  serialize() {
    return JSON.stringify(this);
  }
};

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class Post {
  constructor(title, content) {
    this.title = title;
    this.content = content;
  }
}

Object.assign(User.prototype, Serializable);
Object.assign(Post.prototype, Serializable);

const user = new User("Alice", "alice@example.com");
const post = new Post("My First Post", "Hello, world!");

console.log(user.serialize()); // '{"name":"Alice","email":"alice@example.com"}'
console.log(post.serialize()); // '{"title":"My First Post","content":"Hello, world!"}'

この例では、Serializableというミックスインを定義しています。

serialize()メソッドは、オブジェクトをJSON文字列にシリアライズする機能を提供します。

UserPostという2つのクラスがあり、それぞれ異なるプロパティを持っています。

Object.assign()を使って、Serializableのメソッドをこれらのクラスのプロトタイプに追加することで、両方のクラスでシリアライズ機能を利用できるようになります。

結果として、UserPostのインスタンスであるuserpostは、ともにserialize()メソッドを呼び出すことができます。

このように、ミックスインを活用することで、コードの重複を減らし、再利用性を高められるのです。

○サンプルコード8:抽象クラスの実装

JavaScriptには抽象クラスのための専用の構文はありませんが、クラス継承を使って抽象クラスの概念を実現できます。

抽象クラスとは、インスタンス化できないベースクラスのことです。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error("Cannot instantiate abstract class.");
    }
  }

  area() {
    throw new Error("Method 'area' must be implemented.");
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

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

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

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

// const shape = new Shape(); // Error: Cannot instantiate abstract class.
const rectangle = new Rectangle(5, 3);
const circle = new Circle(4);

console.log(rectangle.area()); // 15
console.log(circle.area());    // 50.26548245743669

この例では、Shapeという抽象クラスを定義しています。

Shapeクラスのコンストラクターでは、new.targetを使って直接のインスタンス化を防いでいます。

また、area()メソッドは実装されておらず、サブクラスでオーバーライドする必要があります。

RectangleCircleクラスは、Shapeクラスを継承し、それぞれarea()メソッドを実装しています。

これらのクラスはインスタンス化できますが、Shapeクラス自体はインスタンス化できません。

このように、抽象クラスを使うことで、共通の機能を提供しつつ、具体的な実装をサブクラスに委ねることができます。

これにより、コードの構造を改善し、保守性を高められるのです。

○サンプルコード9:インターフェースの実装

JavaScriptにはインターフェースのための専用の構文はありませんが、クラス継承とダックタイピングを組み合わせることでインターフェースの概念を実現できます。

class Drawable {
  draw() {
    throw new Error("Method 'draw' must be implemented.");
  }
}

class Rectangle extends Drawable {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  draw() {
    console.log(`Drawing a rectangle with width ${this.width} and height ${this.height}.`);
  }
}

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

  draw() {
    console.log(`Drawing a circle with radius ${this.radius}.`);
  }
}

function drawShape(shape) {
  if (shape instanceof Drawable) {
    shape.draw();
  } else {
    console.log("Invalid shape object.");
  }
}

const rectangle = new Rectangle(5, 3);
const circle = new Circle(4);

drawShape(rectangle); // "Drawing a rectangle with width 5 and height 3."
drawShape(circle);    // "Drawing a circle with radius 4."
drawShape({});        // "Invalid shape object."

この例では、Drawableというインターフェース的なクラスを定義しています。

Drawableクラスには、draw()メソッドが含まれていますが、実装は提供されていません。

RectangleCircleクラスは、Drawableクラスを継承し、それぞれdraw()メソッドを実装しています。

drawShape()関数は、shapeパラメーターがDrawableのインスタンスであるかどうかをinstanceof演算子でチェックし、適切なdraw()メソッドを呼び出します。

これにより、Drawableインターフェースを満たすオブジェクトのみが受け入れられるようになります。

このように、インターフェース的なクラスを使うことで、オブジェクトが特定のメソッドを持つことを保証し、コードの型安全性を高められます。

また、異なる実装を持つオブジェクトを同じように扱うことができるため、コードの柔軟性も向上するのです。

○サンプルコード10:ジェネリッククラスの継承

JavaScriptにはジェネリクスのための専用の構文はありませんが、クラス継承とダックタイピングを組み合わせることで、ジェネリッククラスのような機能を実現できます。

class Stack {
  constructor() {
    this.items = [];
  }

  push(item) {
    this.items.push(item);
  }

  pop() {
    return this.items.pop();
  }

  peek() {
    return this.items[this.items.length - 1];
  }

  isEmpty() {
    return this.items.length === 0;
  }
}

class NumberStack extends Stack {
  push(item) {
    if (typeof item === "number") {
      super.push(item);
    } else {
      throw new Error("Only numbers can be pushed onto NumberStack.");
    }
  }
}

const stack = new Stack();
stack.push(1);
stack.push("two");
stack.push({ three: 3 });
console.log(stack.pop()); // { three: 3 }
console.log(stack.pop()); // "two"
console.log(stack.pop()); // 1

const numberStack = new NumberStack();
numberStack.push(1);
numberStack.push(2);
// numberStack.push("three"); // Error: Only numbers can be pushed onto NumberStack.
console.log(numberStack.pop()); // 2
console.log(numberStack.pop()); // 1

この例では、Stackという汎用的なスタッククラスを定義しています。

Stackクラスは、任意の型の要素を保持できます。

NumberStackクラスは、Stackクラスを継承し、push()メソッドをオーバーライドしています。

NumberStackpush()メソッドでは、itemが数値型であるかどうかをチェックし、数値型の場合のみsuper.push()を呼び出して要素を追加します。

これにより、NumberStackは数値型の要素のみを受け入れるようになります。

●クラス継承を使う際の注意点

クラス継承は、コードの再利用性と保守性を向上させるための強力な機能ですが、使い方を誤ると複雑さが増してしまう可能性もあります。

そこで、クラス継承を使う際の注意点について、プロトタイプチェーンの理解、継承階層の設計、カプセル化の維持など、重要なポイントを見ていきましょう。

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

JavaScriptのクラス継承は、プロトタイプチェーンに基づいています。

プロトタイプチェーンとは、オブジェクトがプロパティやメソッドを探索する際に、自身のプロトタイプオブジェクトを順番に辿っていく仕組みのことです。

クラス継承を使う際は、このプロトタイプチェーンの動作を理解しておくことが重要です。

プロトタイプチェーンを意識することで、予期せぬ動作を避け、コードの意図を明確に伝えられるようになります。

例えば、次のようなコードを考えてみましょう。

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

  sayHello() {
    console.log(`Hello, I'm ${this.name}!`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  sayHello() {
    console.log(`Woof! I'm ${this.name}, a ${this.breed}.`);
  }
}

const animal = new Animal("Generic Animal");
const dog = new Dog("Buddy", "Labrador Retriever");

animal.sayHello(); // "Hello, I'm Generic Animal!"
dog.sayHello();    // "Woof! I'm Buddy, a Labrador Retriever."

この例では、DogクラスがAnimalクラスを継承しています。

Dogクラスでは、sayHello()メソッドをオーバーライドしています。

dog.sayHello()を呼び出すと、まずDogクラスのsayHello()メソッドが探索されます。

DogクラスにsayHello()メソッドが定義されているため、そのメソッドが実行されます。

一方、animal.sayHello()を呼び出すと、AnimalクラスのsayHello()メソッドが実行されます。

これは、AnimalクラスのプロトタイプチェーンにはDogクラスが含まれていないためです。

このように、プロトタイプチェーンを理解することで、メソッドの呼び出しがどのように解決されるのかを把握できます。

これにより、コードの動作を正確に予測し、バグを防ぐことができるのです。

○継承階層の設計

クラス継承を使う際は、継承階層の設計にも注意が必要です。

継承階層が深すぎたり、関係性が複雑すぎたりすると、コードの理解と保守が難しくなってしまいます。

継承階層を設計する際は、次の点に留意しましょう。

  • 単一責任の原則に従い、各クラスが単一の責任を持つようにする
  • 継承階層が深くなりすぎないように、必要最小限の階層に留める
  • 親クラスと子クラスの関係が明確で、直感的に理解できるようにする
  • インターフェースや抽象クラスを活用し、共通の機能を抽象化する

例えば、次のような継承階層を考えてみましょう。

class Shape {
  constructor(color) {
    this.color = color;
  }

  getArea() {
    throw new Error("Method 'getArea' must be implemented.");
  }
}

class Rectangle extends Shape {
  constructor(color, width, height) {
    super(color);
    this.width = width;
    this.height = height;
  }

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

class Circle extends Shape {
  constructor(color, radius) {
    super(color);
    this.radius = radius;
  }

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

const rectangle = new Rectangle("red", 5, 3);
const circle = new Circle("blue", 4);

console.log(rectangle.getArea()); // 15
console.log(circle.getArea());    // 50.26548245743669

この例では、Shapeクラスを親クラスとし、RectangleCircleクラスがそれを継承しています。

Shapeクラスは色を表すcolorプロパティを持ち、getArea()メソッドを抽象メソッドとして定義しています。

RectangleCircleクラスは、それぞれ幅と高さ、または半径を表すプロパティを追加し、getArea()メソッドを実装しています。

この継承階層は、明確で理解しやすいものになっています。

各クラスが単一の責任を持ち、親クラスと子クラスの関係が直感的に理解できます。

また、getArea()メソッドを抽象化することで、共通の機能を親クラスに集約しています。

このように、継承階層を適切に設計することで、コードの可読性と保守性を高められます。

また、将来的な拡張にも対応しやすくなるのです。

○カプセル化の維持

クラス継承を使う際は、カプセル化の維持にも気をつける必要があります。

カプセル化とは、オブジェクトの内部状態を隠蔽し、外部からのアクセスを制限することを指します。

JavaScriptには、privateやprotectedなどのアクセス修飾子がありませんが、コーディングの慣習として、アンダースコア(_)で始まる名前を使ってプライベートなプロパティやメソッドを表すことがあります。

クラス継承を使う際は、このようなプライベートなプロパティやメソッドを適切に保護し、カプセル化を維持することが重要です。

不必要に内部状態を公開すると、予期せぬ変更が加えられ、コードの安全性が損なわれる可能性があります。

例えば、次のようなコードを考えてみましょう。

class BankAccount {
  constructor(balance) {
    this._balance = balance;
  }

  getBalance() {
    return this._balance;
  }

  deposit(amount) {
    this._balance += amount;
  }

  withdraw(amount) {
    if (this._balance >= amount) {
      this._balance -= amount;
    } else {
      console.log("Insufficient funds.");
    }
  }
}

class SavingsAccount extends BankAccount {
  constructor(balance, interestRate) {
    super(balance);
    this._interestRate = interestRate;
  }

  addInterest() {
    const interest = this._balance * this._interestRate;
    this.deposit(interest);
  }
}

const account = new SavingsAccount(1000, 0.05);
console.log(account.getBalance()); // 1000
account.addInterest();
console.log(account.getBalance()); // 1050

この例では、BankAccountクラスとSavingsAccountクラスを定義しています。

BankAccountクラスは、残高を表す_balanceプロパティを持ち、残高を取得、預金、引き出しするメソッドを提供します。

SavingsAccountクラスは、BankAccountクラスを継承し、利子率を表す_interestRateプロパティを追加しています。

また、addInterest()メソッドを定義し、利子を計算して残高に加算します。

ここで重要なのは、_balance_interestRateがアンダースコアで始まる名前になっていることです。

これは、これらのプロパティがクラスの内部でのみ使用されるべきであり、外部から直接アクセスすべきではないことを示しています。

まとめ

JavaScriptのクラス継承について、基本的な使い方から応用例、注意点まで詳しく見てきました。

クラス継承を活用することで、コードの再利用性と保守性を大幅に向上させられることがわかりましたね。

オブジェクト指向プログラミングの概念を取り入れ、より効率的で質の高いJavaScriptコードを書けるようになるでしょう。

JavaScriptのクラス継承を使いこなせるようになれば、コードの再利用性と保守性が向上し、開発の効率化にもつながります。

また、オブジェクト指向プログラミングの概念を深く理解することで、より高度なJavaScriptプログラミングができるようになるでしょう。

クラス継承は、JavaScriptの強力な機能の1つです。

ぜひ、サンプルコードを実際に手を動かして試してみてください。

クラス継承をあなたのプロジェクトに活用し、より良いコードを書いていきましょう。