classキーワードによるクラス

《 初回公開:2022/11/17 , 最終更新:2023/10/19 》

この記事の内容の検証はGoogle Chromeを使っておこなっており、他のWebブラウザや同じChromeでもバージョンによっては異なる結果を示すかもしれない。(当サイトの記事は基本的にはWindowsおよびWindowsのChromeブラウザについて記述になります。)

【 目次 】

だいぶ以前に、「不思議の国のJavaScript オブジェクト指向に関するtips - 愚鈍人」という記事で、JavaScriptのオブジェクト指向について書いたが、
その時は、クラスを定義するのにfunctionを使った。

その後classキーワードなど、新たなクラスの定義方法が導入されててきたのでこれについて調べてみる。
従来通りのfunctionを使ったクラスの定義も可能で、実はclassはfunctionのシンタックスシュガーで、classキーワードの背後にはfunctionがあるのだが。

JavaScript オブジェクト クラス インスタンス

クラス構文

クラス構文にはクラス式とクラス宣言の 2 つの定義方法が、 そして、クラスはクラス名を示すnameプロパティを持つ。

クラス宣言

クラス宣言の例

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

関数は巻き上げされるのに対してクラスは巻き上げされない。
巻き上げというのは宣言される前に使う事ができるという意味で。

クラスの定義をする前にクラスを使う事ができないという事で、
以下のコードはエラーになる。

const p = new Rectangle(); // ReferenceError

class Rectangle {}

クラス式

クラス式には名前付きのクラス式と名前なし(無名)のクラス式とがある。

名前付きのクラス式の例

let Rectangle = class Rectangle2 {};
console.log(Rectangle.name);    // 出力: "Rectangle2"

名前なしのクラス式の例

let Rectangle = class {};
console.log(Rectangle.name);    // 出力: "Rectangle"

クラスを再定義することはできない。

class MyCls { }
// 再度、同名のクラスを定義したため次の行でエラーが発生
class MyCls { } // Uncaught SyntaxError: Identifier 'MyCls' has already been declared

クラスのスコープ

以下のコードはクラスがletやconstと同様にブロックスコープである事を示している。

function func() {
  {
    // 定数宣言
    const C = "const";
    let l = "let";

    // クラス定義(クラス宣言)
    class Rectangle { }

    // 名前付きのクラス式
    let Rectangle2 = class { };
    // 名前なし(無名)のクラス式
    let RectangleX = class Rectangle3 { };

    // letやconstはブロックスコープ
    console.log(C);
    console.log(l);

    // クラスもブロックスコープ
    console.log(Rectangle.name);
    console.log(Rectangle2.name);
    console.log(RectangleX.name);


    //console.log(Rectangle3.name); // エラー: Rectangle3でアクセスする事は許されない
  }
  // ブロックの外なので以下のコードは全てエラーに
  console.log(C);
  console.log(l);
  console.log(Rectangle.name);
  console.log(Rectangle2.name);
  console.log(RectangleX.name);
}
func();
// ブロックの外なのでこれもエラー
console.log(Rectangle.name);

フィールド変数(インスタンスフィールド)

パブリックフィールド

フィールド変数はクラス定義の中で変数を指定する事で定義できる。

class Rectangle {
  // パブリックフィールド宣言
  p = 10;
  // p2は値が代入されていないパブリックフィールド
  p2;
}

letやvar,constを使ってのフィールド変数の指定はできないようだ。

フィールド変数をletやvar,constを使って指定する事はできない

class Rectangle {
  // 以下はすべてエラー
  let p3;
  var p4;
  const pc="xxx";
}

上記のコードの変数pやp2はパブリックフィールドと呼ばれる。
パブリックフィールドはクラスのインスタンスからアクセスできる。

// Rectangleクラスのインスタンスを変数rに代入
let r = new Rectangle();
// フィールド変数pをコンソールに出力
console.log(r.p);   // 10

フィールド変数はインスタンス変数で、クラスの変数ではないのでクラスからアクセスできない

console.log(Rectangle.p);   // undefined

フィールド変数はインスタンスに定義されていてprototypeのプロパティでは無い。

console.log(Rectangle.prototype.p); // undefined

クラスのパブリックフィールドとオブジェクトのプロパティ

クラスのインスタンスも従来のオブジェクトと同様に、特定のインスタンスだけで使えるプロパティ(フィールド変数やメソッド)をクラス定義の外部から定義できる。

r.a = "xxx";
console.log(r.a);   // xxx
// 同じクラスのインスタンスであっても異なるインスタンスからはアクセスできない。
let r2 = new Rectangle();

console.log(r2.a);  // undefined

// インスタンスにメソッドも定義できる。
r.f = function () {
  console.log("r.f");
}
r.f();  // "r.f"

従来のオブジェクト同様にクラスオブジェクトのprototypeに変数を定義するとパブリックフィールドのように扱える

// 同じクラスのインスタンスであれば同じ値に
Rectangle.prototype.b = "yyy";
console.log(r.b);   // yyy

console.log(r2.b);  // yyy

// でも変数の値を変更してしまうと異なる値になる
r.b = "aaa";

console.log(r.b);   // aaa
console.log(r2.b);  // yyy
// そしてprototypeの値は元のまま
console.log(Rectangle.prototype.b); // yyy

クラスを従来のJavaScriptのオブジェクトとして扱うことはできるようだが、クラスの概念と混乱しないためにはprototypeに変数を定義するのは避けた方がいいようだ。

プライベートフィールド

変数名の先頭に#の付いているフィールド変数はプライベートとなり、クラス定義の外部からアクセスできない。(隠蔽できる。)
プライベートフィールドは通常のプロパティとは違い、this への追加によって後から作成することができない。
呼び出される前に宣言されていないプライベートフィールドを参照したり、宣言されているフィールドを delete で削除しようとしても構文エラーになる。

class ClassWithPrivateField {
  #privateField;

  constructor() {
    this.#privateField = 42;
    // プライベートフィールドは通常のプロパティとは違い、this への追加によって後から作成することができない
    this.#undeclaredField = 444; // Syntax error
    // 宣言されているフィールドを delete で削除する事はできない
    delete this.#privateField;   // Syntax error
  }
}

const instance = new ClassWithPrivateField()
// クラスの外部からプライベートフィールドにアクセスする事はできない
instance.#privateField === 42;   // Syntax error

コンストラクタ

クラスのインスタンスの初期化をおこなうコンストラクタは以下のようにconstructorキーワードを使って定義する。
Java等の他の言語と異なりコンストラクタはクラスに 1 つしか定義できない。

class Rectangle {
  #height = 0;
  #width;
  constructor(height, width) {
    this.#height = height;
    this.#width = width;
  }
}

上記のコードの中のthisはクラスがインスタンス化された時のインスタンス自身を指す。

this の値は、JavaScriptの実行コンテキストにより異なる。

JavaScript 実行コンテキスト this

インスタンスメソッド

JavaScriptのclassにはC#やJavaのgetter・setterの構文も導入されていて
ゲッター,セッターはメソッドのようなもので、

インスタンスメソッドやインスタンスのゲッター,セッター
さらに、ジェネレーターメソッドも定義できて、

class Foo {
  #seed = 3;

  // ゲッター
  get seed() {
    return this.#seed;
  }

  // セッター
  set seed(seed) {
    this.#seed = seed
  }

  // メソッド
  multiSeed(multi) {
    return this.#seed * multi;
  }

  // ジェネレーターメソッド
  *g() {
    let index = 0;
    while (true) {
      yield index += this.#seed;
    }
  }

}

const foo = new Foo();

// ゲッタ-, セッターの呼び出し
foo.seed = 5;
console.log(foo.seed); // 5

// メソッドの呼び出し
console.log(foo.multiSeed(4)); // 20

// ジェネレーターメソッドの呼び出し
const it = foo.g()
for (let i = 0; i < 5; i++) {
  console.log(it.next().value); // 5, 10, 15, 20, 25
}

プロトタイプメソッド

インスタンスメソッドやインスタンスのゲッター,セッター,ジェネレーターメソッドはクラスのprototypeオブジェクトのメンバーになっている。hasOwnPropertyメソッドでこれを確認。

console.log(Foo.prototype.hasOwnProperty('seed'));      // true
console.log(Foo.prototype.hasOwnProperty('multiSeed')); // true
console.log(Foo.prototype.hasOwnProperty('g'));         // true

メソッドに関してはクラス定義の後に、後からクラスの外でメソッドを追加しても動作する。

// 後からメソッドを追加
Foo.prototype.m = function () {
  return "hello";
};

console.log(foo.m());   // hello

クラスメソッド - 静的メソッドと静的ゲッタ,セッター

静的メソッドや静的ゲッタ,セッター, ジェネレーターはインスタンスではなくクラスによってアクセスする。
ここではこれらをクラスメソッドと呼ぶ事にする。
クラスメソッドを実装するにはstaticキーワードを使用する。
インスタンスメソッドの項のサンプルコードをクラスメソッドに置き換えてみると

class Foo {
  static #seed = 3;

  // ゲッター
  static get seed() {
    return this.#seed;
  }

  // セッター
  static set seed(seed) {
    this.#seed = seed
  }

  // メソッド
  static multiSeed(multi) {
    return this.#seed * multi;
  }

  // ジェネレーターメソッド
  static *g() {
    let index = 0;
    while (true) {
      yield index += this.#seed;
    }
  }

}

// ゲッタ-, セッターの呼び出し
Foo.seed = 5;
console.log(Foo.seed); // 5

// メソッドの呼び出し
console.log(Foo.multiSeed(4)); // 20

// ジェネレーターメソッドの呼び出し
const it = Foo.g()
for (let i = 0; i < 5; i++) {
  console.log(it.next().value); // 5, 10, 15, 20, 25
}

インスタンスメソッドやインスタンスのゲッター,セッター,ジェネレーターメソッドはクラスオブジェクトのメンバーになっていて
フィールド変数やメソッドに関してはクラスのパブリックフィールとしてドクラス定義の後に後からクラスの外でメソッドを追加しても動作する。

// 後からプロパティやメソッドを追加
Foo.m = function () {
  return "hello";
};
Foo.p = "xxx";

console.log(Foo.m());   // hello
console.log(Foo.p); // xxx

クラスの継承 - extends

クラスの(インスタンスの)継承

クラスのサブクラスを作成するには extendsキーワードを使う。
サブクラスにコンストラクターが存在する場合は、"this" を使う前に super() を呼ぶ必要がある。

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

  methodA() {
    console.log(`${this.name}: SuperClsクラスのmethodA`);
  }

  methodB() {
    console.log(`${this.name}: SuperClsクラスのmethodB`);
  }

  methodC() {
    console.log(`${this.name}: SuperClsクラスのmethodC`);
  }

  get age() {
    return 999;
  }
}

class SubCls extends SuperCls {
  constructor(name) {
    super(name); // スーパークラスのコンストラクターを呼び出し、name パラメータを渡す
  }

  // methodAメソッドをオーバーライド
  methodA() {
    console.log(`${this.name}: SubClsクラスのmethodA`);
  }

  // スーパークラスの同名のメソッドを呼び出す
  methodB() {
    super.methodB();
    console.log(`${this.name}: SubClsクラスのmethodB`);
  }
}

let obj = new SubCls('愚鈍人');
// オーバーライドされたメソッドの呼び出し
obj.methodA(); // 愚鈍人: SubClsクラスのmethodA
// スーパークラスで定義されたメソッドの呼び出し
obj.methodB();  // 愚鈍人: SuperClsクラスのmethodB , 愚鈍人: SubClsクラスのmethodB

obj.methodC(); // 愚鈍人: SuperClsクラスのmethodC
// スーパークラスで定義されたゲッターの呼び出し
console.log(obj.age)    // 999

super.methodB()のようにしてサブクラスからスーパークラスのメソッドをコールする事もできる。
ゲッターやセッター,ジェネレーターも継承される。

クラスのクラスの静的メンバー

クラスの静的メンバーも継承される。

class SuperCls {
  static #cvar = "xxx"
  static publicField = "aaa"

  // クラスメソッド
  static cMethod() {
    console.log("SuperClsで定義されたメソッド");
  }

  // ゲッター
  static get cvar() {
    return SuperCls.#cvar;
  }
  // ジェネレーターメソッド
  static *g() {
    let index = 0;
    while (true) {
    yield index += 3;
    }
  }
}

class SubCls extends SuperCls {
static #cvar = "yyy"

// ゲッターのオーバーライド
static get cvar() {
    console.log(super.cvar);
    return SubCls.#cvar;
}
// 新規メソッドの追加
static newMethod() {
    console.log("SubClsで追加されたメソッド");
}
}

SubCls.cMethod()    // SuperClsで定義されたメソッド
SubCls.newMethod()  // SubClsで追加されたメソッド

// SubClsのpublicFieldの初期値はSuperClsの値と同じ
console.log(SubCls.publicField);    // aaa
// SubClsのpublicFieldの値を変更
SubCls.publicField = "bbb";
console.log(SubCls.publicField);
// SubClsのpublicFieldの値を変更してもSuperClsのpublicFieldの値は変化しない
console.log(SuperCls.publicField);

// SuperClsのゲッターを呼び出し
console.log(SuperCls.cvar);
// SubClsのゲッターを呼び出し
console.log(SubCls.cvar);

// ジェネレーターメソッドも継承されている
const it = SubCls.g();
for (let i = 0; i < 5; i++) {
    console.log(it.next().value);   // 3, 6, 9, 12, 15
}
ページのトップへ戻る