ゴール:「クラスが new されたとき“何がどの順番で動くか”をイメージできるようになる」
クラス設計がうまくいくかどうかは、
「初期化の順番」をちゃんと理解しているかにかなり左右されます。
- フィールド初期化子(
x = 1) - コンストラクタの処理
- 継承しているときの
super() - static とインスタンス
これらがどの順番で動くのかを知らないと、
「なんでここ undefined なんだ…?」
「この時点ではまだ初期化されてないのか…」
みたいな“モヤッとバグ”にハマりやすくなります。
ここでは、TypeScript(=中身は JavaScript)のクラスが
「new された瞬間に何が起きているか」を、
初心者向けに順番でかみ砕いていきます。
まずはシンプルなクラスの初期化の流れ
フィールド初期化子とコンストラクタの関係
一番シンプルなクラスから見てみます。
class User {
id = 0;
name = "no name";
constructor(id: number, name: string) {
console.log("constructor start");
this.id = id;
this.name = name;
console.log("constructor end");
}
}
const u = new User(1, "Taro");
console.log(u.id, u.name);
TypeScriptこのとき、実際の「初期化の順番」はこうなります。
new User(1, "Taro")が呼ばれる- インスタンスが「空っぽの箱」として作られる
- フィールド初期化子
id = 0;とname = "no name";が実行される - そのあとでコンストラクタ本体が実行される(
constructorの中身)
つまり、フィールド初期化子は「コンストラクタの前」に走ります。
だから、上の例では最終的に id と name はコンストラクタで上書きされます。
ここで大事なのは、
「フィールド初期化子は“デフォルト値”であり、
コンストラクタで上書きされることが前提」
という感覚です。
フィールド初期化子を使わない場合との違い
同じクラスを、フィールド初期化子なしで書くとこうなります。
class User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
TypeScriptこの場合は、
- インスタンスが作られる(プロパティはまだ
undefined) - コンストラクタ本体が実行される
this.id = id;などで初期化される
という流れになります。
strict モードでは「コンストラクタでちゃんと全部初期化してね」と怒られるのは、
このタイミングの話です。
継承が絡むときの初期化順序
親クラスと子クラスがある場合の流れ
継承が入ると、順番が少し複雑になります。
class Base {
baseValue = "base field";
constructor() {
console.log("Base constructor");
}
}
class Child extends Base {
childValue = "child field";
constructor() {
console.log("Child constructor start");
super();
console.log("Child constructor middle");
console.log(this.baseValue, this.childValue);
console.log("Child constructor end");
}
}
const c = new Child();
TypeScriptこのときの流れを、ざっくり言葉で追うとこうなります。
new Child()が呼ばれる- まず「Base 部分」の初期化が行われる
2-1. Base のフィールド初期化子(baseValue = "base field")が実行される
2-2. Base のコンストラクタ本体が実行される - 次に「Child 部分」の初期化が行われる
3-1. Child のフィールド初期化子(childValue = "child field")が実行される
3-2. Child のコンストラクタ本体の残りが実行される
ここで重要なのは、
「子クラスのコンストラクタの先頭で super() を呼ばないと this が使えない」
というルールです。
class Child extends Base {
childValue = "child field";
constructor() {
// console.log(this.childValue); // ここで this を触るとエラー
super();
console.log(this.childValue); // ここなら OK
}
}
TypeScriptsuper() が呼ばれる前は、
「まだ親クラスの初期化が終わっていない」状態なので、this に触ることが禁止されています。
「継承しているクラスのコンストラクタでは、
必ず super() を一番最初に呼ぶ」
というのは、この初期化順序のルールから来ています。
子クラスのフィールド初期化子は「super() のあと」に走る
もう一つ、よくハマるポイントがあります。
class Base {
baseValue = "base";
constructor() {
console.log("Base:", this.baseValue);
}
}
class Child extends Base {
childValue = "child";
constructor() {
super();
console.log("Child:", this.childValue);
}
}
const c = new Child();
TypeScriptこのときのログは、だいたいこんなイメージになります。
Base: base
Child: child
つまり、
- Base のフィールド初期化子 → Base のコンストラクタ
- そのあとで Child のフィールド初期化子 → Child のコンストラクタ本体
という順番です。
「子クラスのフィールド初期化子は super() のあとに実行される」
というのを覚えておくと、
「このタイミングではまだ childValue が初期化されてないのか…」
みたいな混乱を避けやすくなります。
static の初期化順序:インスタンスとは別世界
static フィールドと static メソッドは「クラス読み込み時」に初期化される
static はインスタンスとは別のタイミングで初期化されます。
class Example {
static value = Example.init();
static init(): number {
console.log("static init");
return 42;
}
constructor() {
console.log("constructor");
}
}
console.log("before new");
const e = new Example();
console.log("after new");
TypeScriptこのときのログのイメージは、
static init
before new
constructor
after new
のようになります(実際の順番は環境によって多少違うこともありますが、
「static はクラスが評価されたときに一度だけ」というのがポイント)。
つまり、
- static フィールドの初期化子
- static メソッドの定義
は、「クラス定義が読み込まれたタイミング」で一度だけ実行される
(インスタンスを何回 new しても、static の初期化は一度きり)
という性質を持っています。
インスタンスの初期化順序とは完全に別枠なので、
頭の中で「static の世界」と「インスタンスの世界」を分けて考えると整理しやすいです。
初期化順序を意識した安全な書き方
フィールド初期化子の中で「他のフィールド」に依存しすぎない
例えば、こんなコードは危険です。
class Bad {
a = this.initA();
b = this.initB();
private initA(): number {
return 1;
}
private initB(): number {
return this.a + 1;
}
}
TypeScriptこの場合はまだマシですが、
継承やオーバーライドが絡むと「この時点で本当に初期化済みか?」が
一気に分かりづらくなります。
特に継承関係で、
「親クラスのフィールド初期化子の中から、
オーバーライドされたメソッドを呼んでしまう」
みたいなパターンはかなり危険です。
初心者のうちは、
- フィールド初期化子は「単純な値」か「コンストラクタ引数だけ」を使う
- 複雑な初期化ロジックはコンストラクタ本体に寄せる
くらいのルールにしておくと、安全側に倒せます。
コンストラクタの中で「何がもう初期化済みか」を意識する
継承ありのクラスで、子クラスのコンストラクタを書くときは、
super()の前:何も触れない(this禁止)super()の直後:親クラスのフィールドは初期化済み、子クラスのフィールド初期化子はまだ- 子クラスのフィールド初期化子のあと:親も子もフィールド初期化子は完了
という3段階をイメージしておくと、
「このタイミングで何に触っていいか」が分かりやすくなります。
実務では、
「コンストラクタの中であまりごちゃごちゃやらず、
必要なら専用の初期化メソッドを呼ぶ」
というスタイルにすることも多いです。
まとめ:クラスの初期化順序を自分の言葉で整理すると
最後に、あなた自身の言葉でこう整理してみてください。
クラスが new されたとき、
インスタンスについては、
- インスタンスが作られる
- 親クラスのフィールド初期化子 → 親コンストラクタ
- 子クラスのフィールド初期化子 → 子コンストラクタ本体
という順番で動く。
static については、
- クラス定義が評価されたタイミングで、一度だけ初期化される
- インスタンスの生成回数とは関係ない
という別枠の世界がある。
そして、コードを書くときは、
- フィールド初期化子は「デフォルト値」としてシンプルに使う
- 継承クラスのコンストラクタでは、必ず
super()を最初に呼ぶ - 「このタイミングで本当に初期化済みか?」を意識して設計する
今書いているクラスを1つ選んで、
「このフィールドはどのタイミングで値が入るんだっけ?」
「親子クラスがあるなら、どっちが先に初期化されるんだっけ?」
と、実際に console.log を仕込んで確かめてみてください。
一度「目で見て」順番を体感すると、
クラスの初期化順序は一気に腹落ちして、
そのあとクラス設計がかなり楽になります。
