Skip to main content

基类和派生类初始化顺序

JavaScript 类继承的初始化顺序解析

在 JavaScript 中,类继承的初始化顺序与其他面向对象语言(如 Java、C++)既有相似之处,也有独特特性。理解这些规则对于编写健壮的继承结构至关重要。

一、核心初始化规则

1. 基类构造函数优先执行

  • 创建派生类实例时,JavaScript 强制要求先调用基类构造函数(通过 super()),再执行派生类构造函数。
  • 若派生类未显式定义构造函数,JavaScript 会自动生成一个并调用 super()

2. 类字段初始化顺序

  • 类字段(Class Fields)在构造函数开始执行前初始化。
  • 基类字段先初始化,再初始化派生类字段。

3. 静态成员初始化

  • 静态字段和静态块在类定义被加载时初始化,且只执行一次。
  • 静态成员初始化早于任何实例创建。

二、执行流程示例

代码示例

class Base {
static baseStaticField = console.log('Base static field'); // 1

baseField = this.initBaseField();

constructor() {
console.log('Base constructor'); // 5
}

initBaseField() {
console.log('Base field initialization'); // 4
return 1;
}
}

class Derived extends Base {
static derivedStaticField = console.log('Derived static field'); // 2

derivedField = this.initDerivedField();

constructor() {
super(); // 必须先调用基类构造函数
console.log('Derived constructor'); // 7
}

initDerivedField() {
console.log('Derived field initialization'); // 6
return 2;
}
}

console.log('Creating instance...'); // 3
new Derived();

执行结果

Base static field
Derived static field
Creating instance...
Base field initialization
Base constructor
Derived field initialization
Derived constructor

执行流程解析

  1. 静态成员初始化

    • 类定义加载时,依次初始化 BaseDerived 的静态字段。
  2. 实例创建

    • 调用 Derived 构造函数。
    • 自动调用 super() 跳转到 Base 构造函数。
    • 执行 Base 字段初始化(baseField)。
    • 执行 Base 构造函数体。
    • 返回 Derived 构造函数,执行 Derived 字段初始化(derivedField)。
    • 执行 Derived 构造函数体。

三、关键细节与陷阱

1. 必须先调用 super()

  • 派生类构造函数中,若未显式调用 super(),访问 this 会报错:
    class Derived extends Base {
    constructor() {
    this.value = 1; // 错误!必须先调用 super()
    super();
    }
    }

2. 类字段 vs 构造函数赋值

  • 类字段:在 super() 调用前初始化(属于实例初始化的一部分)。
  • 构造函数赋值:在 super() 调用后执行。

示例对比

class Base {
constructor() {
this.init(); // 可能触发问题!
}

init() {
console.log('Base init');
}
}

class Derived extends Base {
value = 10; // 类字段在 super() 前初始化

constructor() {
super();
this.value = 20; // 构造函数赋值在 super() 后
}

init() {
console.log('Derived init, value:', this.value); // 输出?
}
}

new Derived(); // 输出:Derived init, value: undefined

陷阱分析

  • Base 构造函数调用 init() 时,Derivedvalue 尚未初始化(虽然类字段已定义,但赋值发生在 super() 之后)。

四、静态初始化块(Static Blocks)

ES2022 引入的静态块允许更复杂的静态初始化逻辑:

class MyClass {
static {
console.log('Static block executed');
// 可以访问私有静态字段
this.#privateStaticField = 42;
}

static #privateStaticField;

static getPrivateValue() {
return this.#privateStaticField;
}
}

console.log(MyClass.getPrivateValue()); // 输出:42

执行时机
静态块在类加载时按顺序执行,且早于任何静态字段初始化。

五、最佳实践

1. 避免在基类构造函数中依赖派生类状态

  • 不要在基类构造函数中调用可能被重写的方法,因为此时派生类可能尚未完全初始化。

2. 明确初始化顺序

  • 若需在派生类中初始化资源,使用专门的初始化方法而非构造函数:
    class Base {
    constructor() {
    // 仅执行基类必要初始化
    }

    init() {
    // 可选的初始化方法,由派生类显式调用
    }
    }

    class Derived extends Base {
    constructor() {
    super();
    this.init(); // 确保基类和派生类都已初始化
    }

    init() {
    super.init(); // 调用基类初始化
    // 执行派生类初始化逻辑
    }
    }

3. 使用类字段替代构造函数赋值

  • 类字段语法更简洁,且明确在 super() 前初始化:
    class Derived extends Base {
    value = 10; // 优于在构造函数中赋值

    constructor() {
    super();
    // 构造函数仅处理需要在 super() 后执行的逻辑
    }
    }

六、总结

JavaScript 类继承的初始化顺序可概括为:

  1. 静态成员(基类 → 派生类)在类加载时初始化。
  2. 实例创建时:
    • 基类字段初始化 → 基类构造函数 → 派生类字段初始化 → 派生类构造函数。
  3. 关键约束
    • 派生类构造函数必须先调用 super()
    • 避免在基类构造函数中依赖派生类状态。