[JavaScript講座] クラスフィールドとプライベートフィールド

当ページのリンクには広告が含まれています。
目次

パブリックインスタンスフィールド

基本構文

class Counter {
  count = 0; // パブリックインスタンスフィールド

  constructor(label) {
    this.label = label; // これまで通りの書き方も当然OK
  }

  increment() {
    this.count++;
    console.log(this.label, this.count);
  }
}

const c1 = new Counter("A");
const c2 = new Counter("B");

c1.increment(); // A 1
c2.increment(); // B 1

ポイント:

  • count = 0; は 各インスタンスごと に初期値 0 をセットする
  • 実際には「constructor の中で this.count = 0; する」のとほぼ等価

何が嬉しいか

  • 初期値が宣言の近くに書ける → クラス定義が読みやすい
  • constructor が「引数処理やバリデーション」に集中できる

比較:

// 従来
class User {
  constructor(name) {
    this.name = name;
    this.role = "user";
    this.isActive = true;
    this.createdAt = new Date();
  }
}

// フィールド宣言を使う
class User2 {
  role = "user";
  isActive = true;
  createdAt = new Date();

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

後者の方が「どんな状態を持つクラスか」がパッと見えて整理しやすいです。

静的フィールド(static フィールド)

構文と挙動

class User {
  static count = 0; // クラス全体で共有する値

  constructor(name) {
    this.name = name;
    User.count++;   // インスタンス生成ごとにカウント
  }
}

const u1 = new User("Taro");
const u2 = new User("Hanako");

console.log(User.count); // 2
  • static count = 0; は クラス本体に作成されるプロパティ(User.count
  • インスタンスからは見えない(u1.countundefined

イメージ:

User  ----> { count: 2, prototype: { ... } }
  ↑
  ├─ constructor of u1
  └─ constructor of u2

よくある用途

  • インスタンス数のカウント
  • キャッシュや設定値など、「インスタンスでなくクラスに紐づく情報」
  • ファクトリ的な static メソッドとセットで使う
class IdGenerator {
  static lastId = 0;

  static next() {
    this.lastId++;
    return this.lastId;
  }
}

console.log(IdGenerator.next()); // 1
console.log(IdGenerator.next()); // 2

プライベートフィールドとプライベートメソッド

# 付きフィールドの基本

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

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

  getName() {
    return this.#name;
  }
}

const u = new User("Taro");
console.log(u.getName()); // "Taro"

// console.log(u.#name);  // 構文エラー(SyntaxError)
console.log(u.name);      // undefined(public の name は存在しない)

ポイント:

  • #name は クラスの外から一切アクセスできない
  • そもそも u.#name は 構文として無効(実行時エラーではなくパース時エラー)

プライベートメソッド

class User {
  #name;

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

  #format() {               // プライベートメソッド
    return `User(${this.#name})`;
  }

  debugPrint() {
    console.log(this.#format());
  }
}

const u = new User("Taro");
u.debugPrint(); // User(Taro)

// u.#format(); // 構文エラー
  • 実装詳細を完全に隠蔽できる
  • 外から呼べるのは public メソッドだけ

プライベート static フィールド / メソッド

class Config {
  static #secretKey = "xxxx";

  static #getSecret() {
    return this.#secretKey;
  }

  static debug() {
    console.log("secret:", this.#getSecret());
  }
}

Config.debug();
// Config.#secretKey; // 構文エラー
  • 「クラス全体で共有されるが、外には公開しない」情報を持ちたいときに便利

クラス内 getter / setter

基本構文

class User {
  #name;

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

  get name() {             // プロパティっぽく読み取れる
    return this.#name.toUpperCase();
  }

  set name(value) {        // 代入時のロジック
    if (typeof value !== "string") {
      throw new TypeError("name must be string");
    }
    this.#name = value;
  }
}

const u = new User("Taro");
console.log(u.name); // "TARO"  (getter 経由)

u.name = "Hanako";        // setter 経由
console.log(u.name); // "HANAKO"
  • get name() / set name() と書くと、u.name / u.name = ... で自然に使える
  • 裏で アクセサプロパティ が定義されるイメージ(Object.defineProperty 相当)

どこに定義されるか

クラスで書いた getter / setter は prototype 上 に定義されます:

console.log(Object.getOwnPropertyDescriptor(
  Object.getPrototypeOf(u), "name"
));
// { get: f, set: f, enumerable: false, configurable: true } みたいな感じ
  • 各インスタンスで共有される(関数オブジェクトは1つ)
  • 内部でプライベートフィールド #name を触るのが典型パターン

フィールド初期化タイミングと constructor の役割分担

ここが少しややこしいところなので、要点だけ整理します。

ベースクラスの場合(extends していない class)

class A {
  x = 1;
  y = this.x + 1;

  constructor() {
    console.log("in constructor:", this.x, this.y);
  }
}

const a = new A();
console.log("after:", a.x, a.y);

ベースクラスでは以下の順で処理される:

  1. new A() でインスタンスが生成される
  2. フィールド初期化子(x = 1;, y = this.x + 1;)が 上から順に 実行される
  3. その後で constructor 本体が実行される

継承クラス(extends)の場合の流れ

class Base {
  base = "base";
  constructor() {
    console.log("Base constructor");
  }
}

class Derived extends Base {
  value = 1;

  constructor() {
    super();                // ここを呼ぶまで this は使えない
    console.log(this.value);
  }
}

const d = new Derived();

継承クラスでは:

  1. new Derived() 呼び出し
  2. Derived の constructor が動き始めるが、super() 前には this を触れない
  3. super()Base の constructor が呼ばれ、this が初期化される
  4. そのあとに Derived のフィールド初期化子(value = 1)が実行される
  5. 最後に constructor 本体残りの処理(console.log(this.value)

重要なポイント:

  • extends しているクラスの constructor 内では、
    super() より前に this にアクセスするとエラー
  • フィールド初期化子は super() のあと に評価される(派生クラスの場合)

役割分担の感覚

  • フィールド宣言(x = ...
    • 「デフォルト値・構造」を定義する場所
    • クラスの「メンバー一覧」を宣言するイメージ
  • constructor
    • 引数から初期値を上書きしたり
    • 依存オブジェクトを受け取ったり
    • バリデーションやセットアップ処理を書く場所

例:

class User {
  role = "user";
  isActive = true;

  constructor(name, options = {}) {
    this.name = name;

    if (options.role) {
      this.role = options.role;
    }
    if (options.isActive === false) {
      this.isActive = false;
    }
  }
}
  • フィールド宣言:素の初期状態
  • constructor:引数に応じたカスタマイズ

<<前へ(コンストラクタ関数と class 構文)

>>次へ(プロパティ属性とディスクリプタ)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次