公共类字段

公共字段是可写、可枚举和可配置的属性。因此,与私有对应物不同,它们参与原型继承。

¥Public fields are writable, enumerable, and configurable properties. As such, unlike their private counterparts, they participate in prototype inheritance.

语法

¥Syntax

js
class ClassWithField {
  instanceField;
  instanceFieldWithInitializer = "instance field";
  static staticField;
  static staticFieldWithInitializer = "static field";
}

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

¥There are some additional syntax restrictions:

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

描述

¥Description

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

¥This page introduces public instance fields in detail.

公共实例字段存在于类的每个创建的实例上。通过声明公共字段,你可以确保该字段始终存在,并且类定义更加自文档化。

¥Public instance fields exist on every created instance of a class. By declaring a public field, you can ensure the field is always present, and the class definition is more self-documenting.

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

¥Public instance fields are added to the instance either at construction time in the base class (before the constructor body runs), or just after super() returns in a subclass. Fields without initializers are initialized to undefined. Like properties, field names may be computed.

js
const PREFIX = "prefix";

class ClassWithField {
  field;
  fieldWithInitializer = "instance field";
  [`${PREFIX}Field`] = "prefixed field";
}

const instance = new ClassWithField();
console.log(Object.hasOwn(instance, "field")); // true
console.log(instance.field); // undefined
console.log(instance.fieldWithInitializer); // "instance field"
console.log(instance.prefixField); // "prefixed field"

计算字段名称仅在 类定义时间 处计算一次。这意味着每个类始终具有一组固定的字段名称,并且两个实例不能通过计算名称具有不同的字段名称。计算表达式中的 this 值是围绕类定义的 this,并且引用类名称的是 ReferenceError,因为该类尚未初始化。awaityield 在此表达式中按预期工作。

¥Computed field names are only evaluated once, at class definition time. This means that each class always has a fixed set of field names, and two instances cannot have different field names via computed names. The this value in the computed expression is the this surrounding the class definition, and referring to the class's name is a ReferenceError because the class is not initialized yet. await and yield work as expected in this expression.

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

console.log(new C());
console.log(new C());
// Both instances have the same field name

在字段初始值设定项中,this 指的是正在构造的类实例,super 指的是基类的 prototype 属性,该属性包含基类的实例方法,但不包含其实例字段。

¥In the field initializer, this refers to the class instance under construction, and super refers to the prototype property of the base class, which contains the base class's instance methods, but not its instance fields.

js
class Base {
  baseField = "base field";
  anotherBaseField = this.baseField;
  baseMethod() {
    return "base method output";
  }
}

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

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

console.log(base.anotherBaseField); // "base field"

console.log(sub.subField); // "base method output"

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

¥The field initializer expression is evaluated each time a new instance is created. (Because the this value is different for each instance, the initializer expression can access instance-specific properties.)

js
class C {
  obj = {};
}

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

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

¥The expression is evaluated synchronously. You cannot use await or yield in the initializer expression. (Think of the initializer expression as being implicitly wrapped in a function.)

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

¥Because instance fields of a class are added before the respective constructor runs, you can access the fields' values within the constructor. However, because instance fields of a derived class are defined after super() returns, the base class's constructor does not have access to the derived class's fields.

js
class Base {
  constructor() {
    console.log("Base constructor:", this.field);
  }
}

class Derived extends Base {
  field = 1;
  constructor() {
    super();
    console.log("Derived constructor:", this.field);
    this.field = 2;
  }
}

const instance = new Derived();
// Base constructor: undefined
// Derived constructor: 1
console.log(instance.field); // 2

字段是逐一添加的。字段初始值设定项可以引用其上方的字段值,但不能引用其下方的字段值。所有实例和静态方法都是预先添加的并且可以访问,但如果它们引用正在初始化的字段下方的字段,则调用它们可能不会按预期运行。

¥Fields are added one-by-one. Field initializers can refer to field values above it, but not below it. All instance and static methods are added beforehand and can be accessed, although calling them may not behave as expected if they refer to fields below the one being initialized.

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。)

¥Note: This is more important with private fields, because accessing a non-initialized private field throws a TypeError, even if the private field is declared below. (If the private field is not declared, it would be an early SyntaxError.)

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

¥Because class fields are added using the [[DefineOwnProperty]] semantic (which is essentially Object.defineProperty()), field declarations in derived classes do not invoke setters in the base class. This behavior differs from using this.field = … in the constructor.

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(); // Logs 1

注意:在类字段规范最终确定为 [[DefineOwnProperty]] 语义之前,大多数转译器(包括 Babeltsc)都将类字段转换为 DerivedWithConstructor 形式,这在类字段标准化后引起了微妙的错误。

¥Note: Before the class fields specification was finalized with the [[DefineOwnProperty]] semantic, most transpilers, including Babel and tsc, transformed class fields to the DerivedWithConstructor form, which has caused subtle bugs after class fields were standardized.

示例

¥Examples

使用类字段

¥Using class fields

类字段不能依赖于构造函数的参数,因此字段初始值设定项通常对每个实例计算为相同的值(除非同一表达式每次可以计算为不同的值,例如 Date.now() 或对象初始值设定项)。

¥Class fields cannot depend on arguments of the constructor, so field initializers usually evaluate to the same value for each instance (unless the same expression can evaluate to different values each time, such as Date.now() or object initializers).

js
class Person {
  name = nameArg; // nameArg is out of scope of the constructor
  constructor(nameArg) {}
}
js
class Person {
  // All instances of Person will have the same name
  name = "Dragomir";
}

然而,即使声明一个空的类字段也是有益的,因为它表明该字段的存在,这允许类型检查器以及人类读者静态分析类的形状。

¥However, even declaring an empty class field is beneficial, because it indicates the existence of the field, which allows type checkers as well as human readers to statically analyze the shape of the class.

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

上面的代码看似重复,但考虑一下 this 动态变异的情况:显式字段声明清楚地表明哪些字段肯定会出现在实例上。

¥The code above seems repetitive, but consider the case where this is dynamically mutated: the explicit field declaration makes it clear which fields will definitely be present on the instance.

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

由于初始值设定项是在基类执行后计算的,因此你可以访问由基类构造函数创建的属性。

¥Because initializers are evaluated after the base class has executed, you can access properties created by the base class constructor.

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

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

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

规范

Specification
ECMAScript Language Specification
# prod-FieldDefinition

¥Specifications

浏览器兼容性

BCD tables only load in the browser

¥Browser compatibility

也可以看看

¥See also