目次
パブリックインスタンスフィールド
基本構文
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); // 2static count = 0;は クラス本体に作成されるプロパティ(User.count)- インスタンスからは見えない(
u1.countはundefined)
イメージ:
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);ベースクラスでは以下の順で処理される:
new A()でインスタンスが生成される- フィールド初期化子(
x = 1;,y = this.x + 1;)が 上から順に 実行される - その後で
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();継承クラスでは:
new Derived()呼び出しDerivedの constructor が動き始めるが、super()前にはthisを触れないsuper()でBaseの constructor が呼ばれ、thisが初期化される- そのあとに Derived のフィールド初期化子(
value = 1)が実行される - 最後に
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:引数に応じたカスタマイズ
コメント