公有类字段

公有字段是可编辑、可枚举和可配置的属性。因此,不同于私有对应值(private counterpart)的是,它们参与原型的继承。

语法

js
class ClassWithField {
  instanceField;
  instanceFieldWithInitializer = "实例字段";
  static staticField;
  static staticFieldWithInitializer = "静态字段";
}

还有一些额外的语法限制:

  • 静态属性(字段或方法)的名称不能是 prototype
  • 类字段(静态或实例)的名称不能是 constructor

描述

本页面将详细介绍公共实例字段。

公有实例字段存在于类的每个已创建实例中。通过声明公共字段,可以确保该字段始终存在,而且类的定义也更加自文档化(self-documenting)。

公共实例字段会在基类的构造时(构造函数主体运行之前)或子类的 super() 返回后添加到实例中。没有初始化器的字段会被初始化为 undefined。与属性一样,字段名称也可以计算。

js
const PREFIX = "prefix";

class ClassWithField {
  field;
  fieldWithInitializer = "实例字段";
  [`${PREFIX}Field`] = "带前缀字段";
}

const instance = new ClassWithField();
console.log(Object.hasOwn(instance, "field")); // true
console.log(instance.field); // undefined
console.log(instance.fieldWithInitializer); // "实例字段"
console.log(instance.prefixField); // "带前缀字段"

只在类定义时对计算字段名进行一次求值。这意味着每个类总是有一组固定的字段名,两个实例不能通过计算的名称拥有不同的字段名。计算表达式中的 this 指向类声明所处上下文的 this,而引用类名会导致 ReferenceError,因为类尚未初始化。在此表达式中,awaityield 按预期工作。

js
class C {
  [Math.random()] = 1;
}

console.log(new C());
console.log(new C());
// 两个实例拥有相同的字段名称

在字段初始化器中,this 指向正在构建的类实例,而 super 指向基类的 prototype 属性,它包含基类的实例方法,但不包含其实例字段。

js
class Base {
  baseField = "基类字段";
  anotherBaseField = this.baseField;
  baseMethod() {
    return "基类方法输出";
  }
}

class Derived extends Base {
  subField = super.baseMethod();
}

const base = new Base();
const sub = new Derived();

console.log(base.anotherBaseField); // "基类字段"

console.log(sub.subField); // "基类方法输出"

每次创建新实例时,都会对字段初始化表达式进行求值。(因为每个实例的 this 值都不同,所以初始化表达式可以访问特定于实例的属性)。

js
class C {
  obj = {};
}

const instance1 = new C();
const instance2 = new C();
console.log(instance1.obj === instance2.obj); // false

表达式是同步求值的。不能在初始化表达式中使用 awaityield。(将初始化表达式视为被隐式封装在函数中。)

由于类的实例字段是在各自的构造函数运行之前添加的,因此可以在构造函数中访问字段的值。然而,由于派生类的实例字段是在 super() 返回后定义的,因此基类的构造函数无法访问派生类的字段。

js
class Base {
  constructor() {
    console.log("基类的构造函数:", this.field);
  }
}

class Derived extends Base {
  field = 1;
  constructor() {
    super();
    console.log("派生类的控制函数:", this.field);
    this.field = 2;
  }
}

const instance = new Derived();
// 基类的构造函数:undefined
// 派生类的控制函数:1
console.log(instance.field); // 2

字段是逐个添加的。字段初始化器可以引用它上面的字段值,但不能引用它下面的字段值。所有实例方法和静态方法都会事先添加并可以访问,但如果这些方法引用的字段低于正在初始化的字段,那么调用这些方法时可能会出现与预期不符的情况。

js
class C {
  a = 1;
  b = this.c;
  c = this.a + 1;
  d = this.c + 1;
}

const instance = new C();
console.log(instance.d); // 3
console.log(instance.b); // undefined

备注: 这对私有字段更为重要,因为访问未初始化的私有字段会抛出 TypeError,即使该私有字段已在下面声明。(如果未声明私有字段,则会提前抛出 SyntaxError。)

由于类字段是使用 [[DefineOwnProperty]] 语义(本质上是 Object.defineProperty())添加的,因此派生类中的字段声明不会调用基类中的 setter。这种行为不同于在构造函数中使用 this.field = …

js
class Base {
  set field(val) {
    console.log(val);
  }
}

class DerivedWithField extends Base {
  field = 1;
}

const instance = new DerivedWithField(); // No log

class DerivedWithConstructor extends Base {
  constructor() {
    super();
    this.field = 1;
  }
}

const instance2 = new DerivedWithConstructor(); // 打印 1

备注: 在类字段规范最终确定为[[DefineOwnProperty]]语义之前,包括 Babeltsc在内的大多数转译器都将类字段转换为 DerivedWithConstructor 形式,这在类字段规范化之后造成了一些微妙的错误。

示例

使用类字段

类字段不能依赖于构造函数的参数,因此字段初始化器通常会在每个实例中都求出相同的值(除非同一表达式每次求出的值都不同,例如 Date.now() 或对象初始化器)。

js
class Person {
  name = nameArg; // nameArg 在构造函数的作用域外
  constructor(nameArg) {}
}
js
class Person {
  // Person 的所有实例会有相同的 name
  name = "Dragomir";
}

不过,即使声明一个空的类字段也是有好处的,因为这表明了字段的存在,从而允许类型检查程序和人类读者静态分析类的结构。

js
class Person {
  name;
  age;
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

上面的代码看似重复,但考虑一下 this 被动态更改的情况:明确的字段声明清楚地表明了哪些字段一定会出现在实例中。

js
class Person {
  name;
  age;
  constructor(properties) {
    Object.assign(this, properties);
  }
}

由于初始化器是在基类执行完成后求值的,因此你可以访问由基类构造函数创建的属性。

js
class Person {
  name;
  age;
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

class Professor extends Person {
  name = `${this.name} 教授`;
}

console.log(new Professor("Radev", 54).name); // "Radev 教授"

规范

Specification
ECMAScript® 2025 Language Specification
# prod-FieldDefinition

浏览器兼容性

Report problems with this compatibility data on GitHub
desktopmobileserver
Chrome
Edge
Firefox
Opera
Safari
Chrome Android
Firefox for Android
Opera Android
Safari on iOS
Samsung Internet
WebView Android
WebView on iOS
Deno
Node.js
Public class fields

Legend

Tip: you can click/tap on a cell for more information.

Full support
Full support
Partial support
Partial support
Has more compatibility info.

参见