はじめに

ES6(ECMAScript 2015)で導入された「クラス構文」は、JavaScriptでオブジェクト指向プログラミングを行うための強力な機能です。従来のプロトタイプベースの記法と比べて、より直感的で読みやすいコードを書くことができます。

本記事では、以下の内容を初心者向けにわかりやすく解説します。

  • クラスとは何か、なぜ使うのか
  • クラスの定義方法とインスタンスの生成
  • コンストラクターの役割と使い方
  • メソッドの定義とgetter/setter
  • 静的メソッドと静的プロパティ
  • 継承(extends)とsuperキーワード
  • 実践的な活用パターン

クラスとは

クラスの基本概念

クラスとは、オブジェクトを作成するための「設計図」や「テンプレート」です。クラスを使うことで、同じ構造を持つオブジェクトを効率的に作成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// クラスを使わない場合:同じ構造のオブジェクトを何度も定義
const user1 = {
  name: "田中",
  age: 25,
  greet() {
    console.log(`こんにちは、${this.name}です`);
  }
};

const user2 = {
  name: "佐藤",
  age: 30,
  greet() {
    console.log(`こんにちは、${this.name}です`);
  }
};

// クラスを使う場合:設計図から効率的にオブジェクトを生成
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`こんにちは、${this.name}です`);
  }
}

const user3 = new User("田中", 25);
const user4 = new User("佐藤", 30);

クラスを使うメリット

クラスを使うことで、以下のメリットが得られます。

メリット 説明
コードの再利用 同じ構造のオブジェクトを何度も定義する必要がない
可読性の向上 オブジェクトの構造が明確になり、コードが読みやすくなる
保守性の向上 変更が必要な場合、クラス定義を修正するだけで済む
継承のサポート 既存のクラスを拡張して新しいクラスを作成できる

クラスの定義方法

JavaScriptでクラスを定義する方法は主に2つあります。

クラス宣言

最も一般的な方法で、classキーワードを使ってクラスを宣言します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

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

const rect = new Rectangle(10, 5);
console.log(rect.getArea()); // 50

クラス式

変数にクラスを代入する方法です。無名クラスまたは名前付きクラスを使用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 無名クラス式
const Rectangle = class {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
};

// 名前付きクラス式
const Square = class SquareClass {
  constructor(side) {
    this.side = side;
  }
};

const rect = new Rectangle(10, 5);
const square = new Square(5);

クラス宣言とクラス式の違い

関数と同様に、クラス宣言とクラス式には以下の違いがあります。

特徴 クラス宣言 クラス式
巻き上げ されない(一時的デッドゾーン) されない
名前 必須 省略可能
使用場面 一般的なクラス定義 動的なクラス生成、即時実行

コンストラクター

コンストラクターの役割

constructorは、クラスからインスタンスを生成する際に自動的に呼び出される特別なメソッドです。オブジェクトの初期化処理を行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Book {
  constructor(title, author, price) {
    // thisはこれから作成されるインスタンスを指す
    this.title = title;
    this.author = author;
    this.price = price;
    this.createdAt = new Date();
  }
}

const book = new Book("JavaScript入門", "山田太郎", 2980);
console.log(book.title);     // JavaScript入門
console.log(book.author);    // 山田太郎
console.log(book.price);     // 2980
console.log(book.createdAt); // 現在の日時

コンストラクターの省略

コンストラクターを定義しない場合、空のコンストラクターが自動的に追加されます。

1
2
3
4
5
6
class EmptyClass {
  // constructor() {} が暗黙的に定義される
}

const obj = new EmptyClass();
console.log(obj); // EmptyClass {}

デフォルト引数の活用

コンストラクターでもデフォルト引数を使用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Config {
  constructor(
    host = "localhost",
    port = 3000,
    debug = false
  ) {
    this.host = host;
    this.port = port;
    this.debug = debug;
  }
}

const defaultConfig = new Config();
console.log(defaultConfig.host); // localhost
console.log(defaultConfig.port); // 3000

const customConfig = new Config("example.com", 8080, true);
console.log(customConfig.host); // example.com
console.log(customConfig.port); // 8080

メソッドの定義

インスタンスメソッド

クラス内で定義したメソッドは、すべてのインスタンスで共有されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Calculator {
  constructor(value = 0) {
    this.value = value;
  }

  add(num) {
    this.value += num;
    return this; // メソッドチェーン用
  }

  subtract(num) {
    this.value -= num;
    return this;
  }

  multiply(num) {
    this.value *= num;
    return this;
  }

  getResult() {
    return this.value;
  }
}

const calc = new Calculator(10);
const result = calc.add(5).multiply(2).subtract(10).getResult();
console.log(result); // 20

getter と setter

getsetキーワードを使って、プロパティへのアクセスをカスタマイズできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Temperature {
  constructor(celsius) {
    this._celsius = celsius;
  }

  // getter: プロパティを取得する際に呼ばれる
  get celsius() {
    return this._celsius;
  }

  get fahrenheit() {
    return this._celsius * 9 / 5 + 32;
  }

  // setter: プロパティに値を設定する際に呼ばれる
  set celsius(value) {
    if (value < -273.15) {
      throw new Error("絶対零度を下回ることはできません");
    }
    this._celsius = value;
  }

  set fahrenheit(value) {
    this._celsius = (value - 32) * 5 / 9;
  }
}

const temp = new Temperature(25);
console.log(temp.celsius);    // 25
console.log(temp.fahrenheit); // 77

temp.fahrenheit = 100;
console.log(temp.celsius);    // 約37.78

getterとsetterを使うメリットは以下の通りです。

  • 計算プロパティの実装(上記のfahrenheitのように)
  • 値の検証や変換の追加
  • プロパティへのアクセスの制御

静的メソッドと静的プロパティ

静的メソッド

staticキーワードを使って定義したメソッドは、インスタンスではなくクラス自体に属します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MathUtils {
  static PI = 3.14159;

  static add(a, b) {
    return a + b;
  }

  static multiply(a, b) {
    return a * b;
  }

  static circleArea(radius) {
    return MathUtils.PI * radius * radius;
  }
}

// インスタンスを作成せずに直接呼び出す
console.log(MathUtils.add(5, 3));        // 8
console.log(MathUtils.multiply(4, 7));   // 28
console.log(MathUtils.circleArea(5));    // 78.53975
console.log(MathUtils.PI);               // 3.14159

// インスタンスからは呼び出せない
const utils = new MathUtils();
// utils.add(1, 2); // TypeError: utils.add is not a function

静的メソッドの活用パターン

静的メソッドは、以下のような場面で活用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  // ファクトリーメソッド:オブジェクト生成の別パターンを提供
  static fromJSON(json) {
    const data = JSON.parse(json);
    return new User(data.name, data.email);
  }

  // バリデーション:入力値の検証
  static isValidEmail(email) {
    return email.includes("@") && email.includes(".");
  }

  // ユーティリティ:インスタンスに依存しない処理
  static compareByName(user1, user2) {
    return user1.name.localeCompare(user2.name);
  }
}

// ファクトリーメソッドの使用例
const jsonData = '{"name": "田中", "email": "tanaka@example.com"}';
const user = User.fromJSON(jsonData);
console.log(user.name); // 田中

// バリデーションの使用例
console.log(User.isValidEmail("test@example.com")); // true
console.log(User.isValidEmail("invalid-email"));     // false

クラスの継承

extendsキーワード

extendsキーワードを使って、既存のクラスを拡張した新しいクラスを作成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 親クラス(スーパークラス)
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name}が鳴きます`);
  }

  move() {
    console.log(`${this.name}が移動します`);
  }
}

// 子クラス(サブクラス)
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 親クラスのコンストラクターを呼び出す
    this.breed = breed;
  }

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

  // 子クラス固有のメソッド
  fetch() {
    console.log(`${this.name}がボールを取ってきます`);
  }
}

const dog = new Dog("ポチ", "柴犬");
dog.speak();  // ポチがワンワンと吠えます
dog.move();   // ポチが移動します(親クラスのメソッドを継承)
dog.fetch();  // ポチがボールを取ってきます
console.log(dog.breed); // 柴犬

継承における重要なポイントは以下の通りです。

classDiagram
    Animal <|-- Dog
    Animal <|-- Cat
    Animal : +name
    Animal : +speak()
    Animal : +move()
    Dog : +breed
    Dog : +speak()
    Dog : +fetch()
    Cat : +color
    Cat : +speak()
    Cat : +climb()

superキーワード

superキーワードは、親クラスのコンストラクターやメソッドを呼び出すために使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Vehicle {
  constructor(brand, year) {
    this.brand = brand;
    this.year = year;
  }

  getInfo() {
    return `${this.year}年製 ${this.brand}`;
  }

  start() {
    console.log("エンジンをかけます");
  }
}

class Car extends Vehicle {
  constructor(brand, year, doors) {
    // 親クラスのコンストラクターを呼び出す(必須)
    super(brand, year);
    this.doors = doors;
  }

  // 親クラスのメソッドを拡張
  getInfo() {
    // 親クラスのメソッドを呼び出してから追加情報を付加
    return `${super.getInfo()} - ${this.doors}ドア`;
  }

  start() {
    super.start(); // 親クラスのstart()を呼び出す
    console.log("シートベルトを締めてください");
  }
}

const car = new Car("トヨタ", 2024, 4);
console.log(car.getInfo()); // 2024年製 トヨタ - 4ドア
car.start();
// エンジンをかけます
// シートベルトを締めてください

superを使う際の注意点

子クラスでコンストラクターを定義する場合、以下のルールを守る必要があります。

  1. thisを使用する前にsuper()を呼び出す必要がある
  2. super()を呼び出さないとエラーになる
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Parent {
  constructor(value) {
    this.value = value;
  }
}

class Child extends Parent {
  constructor(value, extra) {
    // super()の前にthisを使うとエラー
    // this.extra = extra; // ReferenceError

    super(value); // 最初にsuper()を呼び出す
    this.extra = extra; // その後でthisを使用可能
  }
}

実践的な活用例

ECサイトの商品管理システム

クラスと継承を活用した実践的な例を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 基本の商品クラス
class Product {
  static nextId = 1;

  constructor(name, price) {
    this.id = Product.nextId++;
    this.name = name;
    this.price = price;
    this.createdAt = new Date();
  }

  getDisplayPrice() {
    return ${this.price.toLocaleString()}`;
  }

  getInfo() {
    return `[${this.id}] ${this.name} - ${this.getDisplayPrice()}`;
  }
}

// 書籍クラス(Productを継承)
class Book extends Product {
  constructor(name, price, author, isbn) {
    super(name, price);
    this.author = author;
    this.isbn = isbn;
    this.category = "書籍";
  }

  getInfo() {
    return `${super.getInfo()} / 著者: ${this.author}`;
  }
}

// 電子機器クラス(Productを継承)
class Electronics extends Product {
  constructor(name, price, brand, warrantyYears) {
    super(name, price);
    this.brand = brand;
    this.warrantyYears = warrantyYears;
    this.category = "電子機器";
  }

  getInfo() {
    return `${super.getInfo()} / ${this.brand} / 保証${this.warrantyYears}年`;
  }

  getWarrantyEndDate() {
    const endDate = new Date(this.createdAt);
    endDate.setFullYear(endDate.getFullYear() + this.warrantyYears);
    return endDate;
  }
}

// 使用例
const book = new Book("JavaScript入門", 2980, "山田太郎", "978-4-xxx-xxxxx-x");
const laptop = new Electronics("ノートPC", 98000, "メーカーA", 3);

console.log(book.getInfo());
// [1] JavaScript入門 - ¥2,980 / 著者: 山田太郎

console.log(laptop.getInfo());
// [2] ノートPC - ¥98,000 / メーカーA / 保証3年

console.log(laptop.getWarrantyEndDate());
// 3年後の日付

ショッピングカートの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class CartItem {
  constructor(product, quantity = 1) {
    this.product = product;
    this.quantity = quantity;
  }

  getSubtotal() {
    return this.product.price * this.quantity;
  }

  getDisplaySubtotal() {
    return ${this.getSubtotal().toLocaleString()}`;
  }
}

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

  addItem(product, quantity = 1) {
    // 既存の商品があれば数量を追加
    const existingItem = this.items.find(
      item => item.product.id === product.id
    );

    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.items.push(new CartItem(product, quantity));
    }
  }

  removeItem(productId) {
    this.items = this.items.filter(
      item => item.product.id !== productId
    );
  }

  getTotal() {
    return this.items.reduce(
      (sum, item) => sum + item.getSubtotal(),
      0
    );
  }

  getDisplayTotal() {
    return ${this.getTotal().toLocaleString()}`;
  }

  getItemCount() {
    return this.items.reduce(
      (count, item) => count + item.quantity,
      0
    );
  }

  showCart() {
    console.log("===== ショッピングカート =====");
    this.items.forEach(item => {
      console.log(
        `${item.product.name} x ${item.quantity} = ${item.getDisplaySubtotal()}`
      );
    });
    console.log(`合計: ${this.getDisplayTotal()} (${this.getItemCount()}点)`);
  }
}

// 使用例
const cart = new ShoppingCart();
const book1 = new Book("JavaScript入門", 2980, "山田太郎", "xxx");
const book2 = new Book("Python入門", 3200, "佐藤花子", "yyy");

cart.addItem(book1, 2);
cart.addItem(book2, 1);

cart.showCart();
// ===== ショッピングカート =====
// JavaScript入門 x 2 = ¥5,960
// Python入門 x 1 = ¥3,200
// 合計: ¥9,160 (3点)

クラスとプロトタイプの関係

ES6のクラス構文は、JavaScriptの既存のプロトタイプベースの継承を、より分かりやすい構文で書けるようにした「シンタックスシュガー(糖衣構文)」です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log(`こんにちは、${this.name}です`);
  }
}

// 上記のクラスは、内部的には以下のような処理と同等
function PersonFunction(name) {
  this.name = name;
}

PersonFunction.prototype.greet = function() {
  console.log(`こんにちは、${this.name}です`);
};

// 両方とも同じように動作する
const person1 = new Person("田中");
const person2 = new PersonFunction("佐藤");

person1.greet(); // こんにちは、田中です
person2.greet(); // こんにちは、佐藤です

// クラスも実際には関数
console.log(typeof Person); // function

クラス構文を使うメリットは以下の通りです。

  • より直感的で読みやすい構文
  • extendsによる継承の簡潔な記述
  • superによる親クラスへの明確なアクセス
  • 静的メソッドの定義が容易

まとめ

本記事では、JavaScriptのクラス構文について基礎から実践的な使い方まで解説しました。

機能 説明
class クラスを定義するキーワード
constructor インスタンス生成時の初期化処理
メソッド クラス内で定義する関数
get/set プロパティへのアクセスをカスタマイズ
static クラス自体に属するメソッドやプロパティ
extends 既存のクラスを継承して新しいクラスを作成
super 親クラスのコンストラクターやメソッドを呼び出す

クラス構文を使いこなすことで、コードの再利用性、可読性、保守性が向上します。最初は基本的なクラス定義から始めて、徐々に継承やメソッドのオーバーライドなど、より高度な機能を活用していきましょう。

参考リンク