クラス

JS

クラスとは

ES6で導入されたクラスについて。

クラスとはオブジェクトの設計図で、コンストラクタ関数をクラス表記で書けるようにしたもの。

コンストラクタ関数はオブジェクトを生成する関数で、newをつけると「インスタンスを作る」ことができます。

(よってインスタンス化してできるのは、オブジェクトになります。)

コンストラクタ関数はthisを使ってnewで作られるインスタンスに値を設定します。アロー関数はnewを使ってもthisを持たないので、コンストラクタにはなれません。

クラスを使うと、コードのメンテナンスがしやすくなり、再利用性が上がるというメリットがあります。

コンストラクタ関数

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

Animal.prototype.speak = function() {
    console.log(this.name + "が鳴きます");
};

const dog = new Animal("ポチ");//インスタンス化
dog.speak(); // → ポチが鳴きます
console.log(dog);
// 結果:
{
  name: "ポチ",
  speak: function() { ... }
}//オブジェクト

prototypeとは、オブジェクトが継承するための親オブジェクトです。

prototypeに定義しておけば、すべてのインスタンスが同じ関数を共有できて効率的です。

function Animal(name) {
  this.name = name;
  this.speak = function() {
    console.log(this.name + "が鳴きます");
  };
}

const a1 = new Animal("ポチ");
const a2 = new Animal("タマ");

console.log(a1.speak === a2.speak); // false → 別々の関数!

↑これだと各インスタンスに独自のspeak関数があり、メモリ的に非効率(インスタンスが増えるたびに関数が複製される)

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

Animal.prototype.speak = function() {
  console.log(this.name + "が鳴きます");
};

const a1 = new Animal("ポチ");
const a2 = new Animal("タマ");

console.log(a1.speak === a2.speak); // true → 同じ関数!

話がそれましたが、上のコンストラクタ関数をクラスを使って書くとこうなります。

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

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

親クラス(スーパークラス)、子クラス

// 親クラスの定義
class Animal {
    constructor(name) {
    this.name = name;
  }

  // 親クラスのメソッド
  sound() {
    console.log("Animal makes a sound");
  }
}

// 子クラスの定義(extentdsを使う)
class Dog extends Animal {
    constructor(name, breed) {
    super(name); // 親クラスのコンストラクタを呼び出すにはsuper()を使う
    this.breed = breed;
  }

  // 子クラスのメソッド
  sound() {
    console.log("Woof!");//親クラスのメソッドをオーバーライド
  }

  // 子クラスの独自のメソッド
  wagTail() {
    console.log("Dog wags its tail");
  }
}

// 親クラスのインスタンス作成
const animal = new Animal("Generic Animal");
console.log(animal.name); // "Generic Animal"
animal.sound(); // "Animal makes a sound"

// 子クラスのインスタンス作成
const dog = new Dog("Buddy", "Golden Retriever");
console.log(dog.name); // "Buddy"
console.log(dog.breed); // "Golden Retriever"
dog.sound(); // "Woof!"
dog.wagTail(); // "Dog wags its tail"

ゲッター、セッター

ゲッター、セッターについて。

class Circle {
  constructor(radius) {
    this._radius = radius;
  }

  // ゲッター
  get radius() {
    return this._radius;
  }

  // セッター
  set radius(newRadius) {
    if (newRadius > 0) {
      this._radius = newRadius;
    } else {
      console.error("Radius must be a positive number.");
    }
  }
}

const circle = new Circle(5);

// ゲッターを使用してプロパティにアクセスし、プロパティの値を取得します
console.log(circle.radius); // 出力: 5

// セッターを使用してプロパティに新しい値を設定します
circle.radius = 10; // 正常に動作します

// ゲッターを使用してプロパティの値を取得します
console.log(circle.radius); // 出力: 10

// セッターを使用してプロパティに負の値を設定しようとしますが、セッターが定義された条件に反しているためエラーが発生します
circle.radius = -5; // エラー: Radius must be a positive number.

// プロパティは変更されず、以前の値が維持されています
console.log(circle.radius); // 出力: 10

ゲッターだけを設定すると、プロパティの値を変更不可にできます。

class Circle {
  constructor(radius) {
    this._radius = radius;
  }

  get radius() {
    return this._radius;
  }

  // このようにセッターを提供しないことで、半径を読み取り専用にする
}

const circle = new Circle(5);
console.log(circle.radius); // プロパティの値を取得する
circle.radius = 10; // エラー: ゲッターのみが定義されているため、プロパティの値を変更できません

一方セッターは、条件を満たさない値がセットされないようにすることができます。

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

// セッター
set width(newWidth) {
if (newWidth > 0) {
this._width = newWidth;
} else {
console.error("Width must be a positive number.");
}
}
}

const rectangle = new Rectangle(5, 10);

rectangle.width = -5; // Width must be a positive number.

クロージャ

クロージャとは、関数がスコープ外で呼び出された時に、元のスコープ内の変数を覚えている機能です。

function Animal(name) {
  let _name = name; // プライベート変数

  this.getName = function() {
    return _name;
  };

  this.setName = function(newName) {
    _name = newName;
  };
}

const dog = new Animal('Rover');
console.log(dog.getName()); // 出力: Rover
dog.setName('Spot');
console.log(dog.getName()); // 出力: Spot
console.log(dog._name); // 出力: undefined (外部からはアクセスできない)

クラスを使う場合は以下のようになります。

class Animal {
  constructor(name) {
    let _name = name; // プライベート変数

    this.getName = function() {
      return _name;
    };

    this.setName = function(newName) {
      _name = newName;
    };
  }
}

const cat = new Animal('Whiskers');
console.log(cat.getName()); // 出力: Whiskers
cat.setName('Tom');
console.log(cat.getName()); // 出力: Tom
console.log(cat._name); // 出力: undefined (外部からはアクセスできない)

最初のクラスの例と何が違うかというと、

// 親クラスの定義
class Animal {
    constructor(name) {
    this.name = name;
  }

この this.name = name; が let _name = name; になっている箇所が違います。

this.name = name; としている例はクラスのインスタンスに直接追加されたパブリックなプロパティで、カプセル化は行われておらず、外部から name に自由にアクセスでき、変更も可能です。

プライベートフィールド

クラス構文ではプライベートフィールドとして # を使う方法が標準化されています。(ES13)

class Animal {
  #name; // プライベートフィールド

  constructor(name) {
    this.#name = name;
  }

  getName() {
    return this.#name;
  }

  setName(newName) {
    this.#name = newName;
  }
}

const bird = new Animal('Tweety');
console.log(bird.getName()); // 出力: Tweety
bird.setName('Polly');
console.log(bird.getName()); // 出力: Polly
console.log(bird.#name); // エラー: プライベートフィールドに直接アクセスできない

プライベートフィールドは変数だけではなく、関数にも使えます。

class MyClass {
  #secretValue = 42;

  #secretMethod() {
    console.log("これは秘密のメソッドです。値は", this.#secretValue);
  }

  publicMethod() {
    console.log("これは公開メソッドです");
    this.#secretMethod(); // ← クラス内から呼び出し可能!
  }
}

const obj = new MyClass();

obj.publicMethod();       // OK → "これは公開メソッドです" → "これは秘密のメソッドです。値は 42"
obj.#secretMethod();      // エラー!外からは呼べない
obj.#secretValue;         // エラー!外からは読めない

プライベートフィールドにすることで、カプセル化ができます。

カプセル化とは、

内部の状態や処理を外部から隠して、必要な操作だけを公開すること。

よって、上の例ではbird.#nameは直接アクセスできないようになります。

カプセル化を実現するためにクロージャが使われます。

関数 + クロージャでカプセル化

ES5以前は、これを関数+クロージャで実現しています。

function createSecretHolder(secret) {
  return {
    getSecret: function() {
      return secret; // ← クロージャでsecretを保持
    }
  };
}

const holder = createSecretHolder("ひみつ");
console.log(holder.getSecret()); // → "ひみつ"
console.log(holder.secret); // → undefined(カプセル化成功!)

secret は外からアクセスできない(カプセル化)。

でも getSecret()secret にアクセスできる(クロージャ)

WeakMap

ES6以降使えるWeakMapについて。

const _private = new WeakMap();// 外からアクセス不可のストレージ

function SecretHolder(secret) {
  _private.set(this, { secret });// this(インスタンス)をキーに格納
}

SecretHolder.prototype.getSecret = function() {
  return _private.get(this).secret;// インスタンスに紐づいた値を取得
};

const holder = new SecretHolder("ひみつ");
console.log(holder.getSecret()); // → "ひみつ"
console.log(holder.secret);      // → undefined

WeakMapを使うと、thisをキーにしてインスタンスごとの秘密情報を保存できます。

(prototype メソッドでも private データにアクセス可能!)

ではプライベートフィールドとの違いはなんでしょうか?

WeakMapとプライベートフィールドの違い

比較項目#name(プライベートフィールド)WeakMap
宣言方法class内で # を使うclass外で WeakMap を作る
プライベート性本当に完全な private(外部からアクセス不可)外部から WeakMap 自体を使えば見れる(でも隠しやすい)
実行速度最適化されていて速いやや遅い(外部構造を使うから)
メモリ管理自動WeakMap の特徴で、keyが消えたら中身も消える(GC)
使える場所クラスの中のみクラス外でも使える(関数でもOK)
実装の自由度JavaScriptに組み込まれた構文柔軟だけどちょっとトリッキー
対応ブラウザES2022以降ならOK(IE未対応)古くから使える(IE11でも使えた)
// WeakMapバージョン
const secrets = new WeakMap();
class WUser {
  constructor(name) {
    secrets.set(this, { name });
  }
  getName() {
    return secrets.get(this).name;
  }
}

// #nameバージョン
class PUser {
  #name;
  constructor(name) {
    this.#name = name;
  }
  getName() {
    return this.#name;
  }
}

結果はどちらもgetName()で名前を返しますが、

WUserの場合、secrets.get(u)と書けば外部から見える可能性がある一方、

PUserは、#nameには絶対アクセスできないという違いがあります。

static

staticはクラスのインスタンスではなく、クラス自体に属するメソッドやプロパティを定義するときに使います。

class User {
  static count = 0;

  constructor(name) {
    this.name = name;
    User.count++;
  }

  static getUserCount() {
    return User.count;
  }
}

new User("Alice");
new User("Bob");

console.log(User.getUserCount()); // → 2

モジュールシステム

ES6以降ブロックスコープとモジュールシステムが入りました。

// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// app.js
import { add, subtract } from './math.js';

console.log(add(2, 3)); // 5
console.log(subtract(7, 4)); // 3