1. ホーム
  2. JavaScript

【JavaScript】class(クラス)の使い方|constructor・メソッド・継承(extends)を解説

Share

同じ形のデータをいくつも扱うプログラムでは、「名前と価格を持つ商品」「体力と攻撃力を持つキャラクター」のように、まとまった性質をひとつの設計図として用意しておくと便利です。JavaScript でこの設計図を書くための構文が、ES2015(ES6)で導入された class(クラス)です。この記事では、class 宣言と constructor による初期化、new でのインスタンス生成から、メソッド・プロパティ・get/set アクセサ・static、そして extendssuper による継承まで、実際に動くコードで順を追って解説します。初心者から中級者の方が対象です。

なぜクラスを使うのか

クラスを使わなくても、オブジェクトリテラルで { name: "りんご", price: 120 } のようにデータを表現することはできます。しかし、同じ構造のオブジェクトを何個も作るたびに同じプロパティを手で書き、関連する処理(値引き計算など)を別の場所に散らばせていると、コードが増えるほど管理が難しくなります。

クラスは、こうした「データ(プロパティ)」と「そのデータを扱う処理(メソッド)」をひとつの設計図にまとめる仕組みです。設計図を1つ書いておけば、そこから同じ形のオブジェクト(インスタンス)を何個でも生成でき、修正も設計図1か所で済みます。なお JavaScript のクラスは、内部的には従来からあるプロトタイプベースの仕組みの上に用意された構文(シンタックスシュガー)です。まったく新しい仕組みが増えたわけではなく、プロトタイプを使った書き方を、より読みやすく書けるようにしたものだと捉えておくとよいでしょう。

class と constructor と new の基本

まずは最小の例です。class のあとにクラス名(慣例で大文字始まり)を書き、{ } の中に constructor という特別なメソッドを用意します。constructornew でインスタンスを作るときに1回だけ自動で呼ばれる初期化用のメソッドで、引数で受け取った値を this に代入してプロパティを用意します。this は「これから作られるインスタンス自身」を指します。

app.js
class Product {
  // new されたときに1回だけ呼ばれる初期化メソッド
  constructor(name, price) {
    this.name = name;   // インスタンスのプロパティ
    this.price = price;
  }
}

// new で設計図からインスタンスを生成する
const apple = new Product("りんご", 120);

console.log(apple.name);  // "りんご"
console.log(apple.price); // 120

new Product("りんご", 120) と書くと、空のインスタンスが作られ、その直後に constructor が実行されて this.namethis.price がセットされます。同じ設計図から const orange = new Product("みかん", 80) のように、別の値を持つインスタンスをいくつでも作れます。constructor を書かなかった場合は、引数を受け取らない空の constructor が自動で用意されます。

メソッドを定義する

クラスの中には、そのインスタンスが持つ処理=メソッドを定義できます。メソッドは function キーワードを付けず、名前() { ... } の形で書きます。メソッドの中からは this を通してそのインスタンスのプロパティにアクセスできます。

app.js
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  // インスタンスメソッド(this で自身のプロパティを使える)
  format() {
    return `${this.name}: ${this.price}円`;
  }

  // 割引後の価格を返すメソッド
  discountedPrice(rate) {
    return Math.round(this.price * (1 - rate));
  }
}

const apple = new Product("りんご", 120);

console.log(apple.format());          // "りんご: 120円"
console.log(apple.discountedPrice(0.2)); // 96

このように定義したメソッドは、すべてのインスタンスで共有されます(内部的にはプロトタイプに置かれます)。インスタンスごとにメソッドの中身がコピーされるわけではないため、たくさんのインスタンスを作ってもメソッドの分だけメモリが増えることはありません。

プロパティとフィールド宣言

プロパティは、これまでのように constructor の中で this.xxx = ... と書いて用意するのが基本です。加えて、クラス本体に直接プロパティを書くフィールド宣言(class fields)という書き方もあります。初期値が固定のプロパティを、constructor を書かずに宣言できます。

app.js
class Counter {
  // フィールド宣言(初期値を持つプロパティ)
  count = 0;

  increment() {
    this.count += 1;
    return this.count;
  }
}

const counter = new Counter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2

// 名前の先頭に # を付けると、外部から触れないプライベートフィールドになる
class BankAccount {
  #balance = 0;

  deposit(amount) {
    this.#balance += amount;
    return this.#balance;
  }
}

const account = new BankAccount();
console.log(account.deposit(1000)); // 1000
// console.log(account.#balance);   // 構文エラー(外部からは参照できない)

# を付けて宣言したフィールドはプライベートフィールドと呼ばれ、クラスの外から読み書きできません。クラス内部だけで使う値を、意図せず書き換えられないように守りたいときに使います。

getter と setter(get / set アクセサ)

メソッドの前に getset を付けると、アクセサプロパティを定義できます。get を付けたメソッドは、メソッド呼び出しの () を書かずにプロパティのように参照でき、set を付けたメソッドは代入したときに呼ばれます。計算で求まる値をプロパティのように見せたり、代入時に値のチェックを挟んだりできます。

app.js
class Temperature {
  constructor(celsius) {
    this._celsius = celsius; // 実データは別名で保持
  }

  // 参照時に呼ばれる(華氏に変換して返す)
  get fahrenheit() {
    return this._celsius * 1.8 + 32;
  }

  // 代入時に呼ばれる(華氏を摂氏に直して保存)
  set fahrenheit(value) {
    this._celsius = (value - 32) / 1.8;
  }
}

const temp = new Temperature(25);
console.log(temp.fahrenheit); // 77   ← () を付けずに参照

temp.fahrenheit = 212;        // set が呼ばれる
console.log(temp._celsius);   // 100

ポイントは、get fahrenheit()temp.fahrenheit() なし)で呼び出す点です。使う側はメソッドか通常のプロパティかを意識せずに扱えます。同じ名前で getset の両方を用意すると、読み取りと書き込みの両方に処理を挟めます。

static メソッドと static プロパティ

static を付けて定義したメソッドやプロパティは、インスタンスではなくクラスそのものに属しますnew で作ったインスタンスからは呼べず、クラス名.メソッド名() の形で呼び出します。インスタンスの状態に依存しない、ユーティリティ的な処理や共通の定数を置くのに向いています。

app.js
class MathUtil {
  static PI = 3.14159; // static プロパティ(クラスに属する定数)

  // static メソッド(this ではなくクラスから呼ぶ)
  static circleArea(radius) {
    return MathUtil.PI * radius * radius;
  }
}

console.log(MathUtil.PI);            // 3.14159
console.log(MathUtil.circleArea(2)); // 12.56636

const util = new MathUtil();
// console.log(util.circleArea(2));  // エラー: インスタンスからは呼べない

インスタンスに属するメンバーと static なメンバーは、呼び出し方が異なります。整理すると次のとおりです。

種類属する先呼び出し方
インスタンスプロパティ各インスタンスinstance.name
インスタンスメソッド各インスタンス(実体はプロトタイプ)instance.format()
アクセサ(get / set)各インスタンスinstance.fahrenheit() なし)
static メソッド / プロパティクラス自身ClassName.circleArea()

extends と super で継承する

既存のクラスをもとに、性質を受け継いだ新しいクラスを作ることを継承といいます。class 子クラス extends 親クラス と書くと、親クラスのプロパティやメソッドをそのまま使える子クラスを定義できます。子クラス側では、追加のプロパティやメソッドを足したり、親のメソッドを上書き(オーバーライド)したりできます。

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

  speak() {
    return `${this.name} が鳴きました`;
  }
}

// Animal を継承した Dog
class Dog extends Animal {
  constructor(name, breed) {
    super(name);       // 親の constructor を呼び、this.name を初期化
    this.breed = breed; // 子で追加するプロパティ
  }

  // 親のメソッドを上書き(オーバーライド)
  speak() {
    // super.親メソッド() で親の処理も呼べる
    return `${super.speak()}(ワン!)`;
  }
}

const dog = new Dog("ポチ", "柴犬");

console.log(dog.name);    // "ポチ"(親から継承したプロパティ)
console.log(dog.breed);   // "柴犬"
console.log(dog.speak()); // "ポチ が鳴きました(ワン!)"

super には2つの使い方があります。constructor の中で関数のように super(引数) と呼ぶと親クラスの constructorを実行し、メソッドの中で super.メソッド名() と書くと親クラスの同名メソッドを呼び出せます。上の例では、子の speak() が親の speak() の結果を利用しつつ、独自の文字列を付け足しています。

継承先で constructor を書くときは super() を先に呼ぶ

継承でつまずきやすいのが super() の扱いです。子クラスに constructor を書く場合、その中で this を使う前に必ず super() を呼ぶ必要があります。親の初期化が終わるまで this が利用可能にならないためで、順番を間違えるとエラーになります。

app.js
class Cat extends Animal {
  constructor(name, color) {
    this.color = color; // NG: super() より前に this を使っている
    super(name);
  }
}

// new Cat("タマ", "白") とすると次のエラーになる
// ReferenceError: Must call super constructor ... before accessing 'this'

正しくは、先に super(name) を呼んで親の初期化を済ませ、そのあとで this.color = color のように子のプロパティを設定します。なお、子クラスで constructorまったく書かなかった場合は、受け取った引数をそのまま親に渡す constructor(...args) { super(...args); } が自動的に用意されるため、super() を意識する必要はありません。super() の呼び忘れが問題になるのは、あくまで子クラスで constructor を自分で書いたときです。

instanceof で型を確認する

あるオブジェクトが、どのクラスから作られたのかを調べたいときは instanceof 演算子を使います。オブジェクト instanceof クラス と書くと、そのオブジェクトが指定したクラス(またはその親クラス)のインスタンスであれば true を返します。継承関係も考慮される点が特徴です。

app.js
const dog = new Dog("ポチ", "柴犬");

console.log(dog instanceof Dog);    // true
console.log(dog instanceof Animal); // true(Dog は Animal を継承している)
console.log(dog instanceof Product); // false

dogDog のインスタンスであると同時に、親である Animal のインスタンスでもあるため、どちらでも true になります。関数に渡されたオブジェクトが期待する種類かどうかを、処理の前にチェックしたいときなどに役立ちます。

まとめ

クラスは、データと処理をまとめた「設計図」を書くための構文です。要点を振り返ります。

  • class で設計図を定義し、constructor で初期化、new でインスタンスを生成する。
  • メソッドは function なしで書き、this で自身のプロパティにアクセスする。
  • プロパティは this.x = ... かフィールド宣言で用意し、# でプライベートにできる。
  • get / set でプロパティのように使えるアクセサを、static でクラスに属するメンバーを定義できる。
  • extends で継承し、super() で親の constructor、super.method() で親メソッドを呼ぶ。
  • 子で constructor を書くときは this より前に super() を呼ぶ。instanceof で型を判定できる。

クラスはプロトタイプベースの仕組みを読みやすく書くための構文である、という前提を押さえておくと、動きの理解がぶれません。まずは小さなクラスを1つ作り、new・メソッド・継承の流れを手元で動かして確かめてみてください。

参考ページ