For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/year/2022/proposal-class-fields.md.
  • 简体中文
  • Class fields, Private methods, and Static class features S4

    中文标题:类字段、私有方法与静态类特性

    提案概览
    提案速览

    该提案为 JavaScript 引入类字段声明、私有方法和静态类特性。它允许在类主体中直接声明字段,并可选提供初始化器,同时通过以 # 为前缀的名称支持私有状态。其设计涵盖字段创建语义、初始化顺序以及封装边界。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    JavaScript 的类字段声明

    Daniel Ehrenberg, Jeff Morrison

    第 4 阶段

    引导示例:使用类的自定义元素

    要定义一个点击时递增的计数器组件,你可以使用 ES2015 定义以下内容:

    class Counter extends HTMLElement {
      clicked() {
        this.x++;
        window.requestAnimationFrame(this.render.bind(this));
      }
    
      constructor() {
        super();
        this.onclick = this.clicked.bind(this);
        this.x = 0;
      }
    
      connectedCallback() { this.render(); }
    
      render() {
        this.textContent = this.x.toString();
      }
    }
    window.customElements.define('num-counter', Counter);

    字段声明

    使用 ESnext 字段声明提案,上面的示例可以写成

    class Counter extends HTMLElement {
      x = 0;
    
      clicked() {
        this.x++;
        window.requestAnimationFrame(this.render.bind(this));
      }
    
      constructor() {
        super();
        this.onclick = this.clicked.bind(this);
      }
    
      connectedCallback() { this.render(); }
    
      render() {
        this.textContent = this.x.toString();
      }
    }
    window.customElements.define('num-counter', Counter);

    在上面的示例中,你可以看到使用语法 x = 0 声明的字段。你也可以声明一个没有初始化器的字段,如 x。通过预先声明字段,类定义变得更具自文档性;实例经历的状态转换更少,因为声明的字段始终存在。

    私有字段

    上面的示例暴露了一些实现细节,这些细节可能最好保持内部。使用 ESnext 私有字段和方法,定义可以细化为:

    class Counter extends HTMLElement {
      #x = 0;
    
      clicked() {
        this.#x++;
        window.requestAnimationFrame(this.render.bind(this));
      }
    
      constructor() {
        super();
        this.onclick = this.clicked.bind(this);
      }
    
      connectedCallback() { this.render(); }
    
      render() {
        this.textContent = this.#x.toString();
      }
    }
    window.customElements.define('num-counter', Counter);

    要使字段私有,只需给它们一个以 # 开头的名字。

    通过定义类外部不可见的内容,ESnext 提供了更强的封装,确保类的使用者不会因为依赖可能随版本变化而变化的内部实现而意外犯错。

    注意,ESnext 仅提供在字段声明中预先声明的私有字段;私有字段不能像普通属性那样,之后通过给它们赋值来临时创建。

    主要设计要点

    使用 Object.defineProperty 创建的公有字段

    公有字段声明使用 Object.defineProperty 的内部机制(在 TC39 术语中我们称之为 [[Define]] 语义)在实例上定义字段,而不是使用 this.field = value;(称为 [[Set]] 语义)。以下是一个影响示例:

    class A {
      set x(value) { console.log(value); }
    }
    class B extends A {
      x = 1;
    }

    采用所采纳的语义时,new B() 将产生一个具有属性 x 且值为 1 的对象,并且不会向控制台写入任何内容。使用替代的 [[Set]] 语义时,1 会被写入控制台,而尝试访问该属性将导致 TypeError(因为缺少 getter)。

    在 [[Set]] 和 [[Define]] 之间的选择是一个设计决策,对比了不同种类的行为预期:预期字段将作为数据属性创建,无论超类包含什么,与预期 setter 会被调用。经过冗长讨论,TC39 最终选择了 [[Define]] 语义,认为保留第一种预期很重要。

    将公有字段语义基于 Object.defineProperty 的决定是基于 TC39 内部的广泛讨论以及与开发者社区的协商。不幸的是,社区意见相当分裂,而 TC39 则相当强烈地偏向 Object.defineProperty。

    作为缓解措施,装饰器提案提供了工具来编写装饰器,使公有字段声明使用 [[Set]] 语义。即使你不同意默认行为,另一个选项也可用。(无论 TC39 选择哪个默认行为,情况都会如此。)

    公有字段在 Chrome 72 中发布,采用 [[Define]] 语义,并且关于语义的这一决定不太可能被重新审视。

    没有初始化器的字段被设置为 undefined

    公有和私有字段声明都会在实例中创建一个字段,无论是否存在初始化器。如果没有初始化器,该字段被设置为 undefined。这与某些转译器实现略有不同,后者会完全忽略没有初始化器的字段声明。

    例如,在以下示例中,new D 将产生一个对象,其 y 属性为 undefined,而不是 1。

    class C {
      y = 1;
    }
    class D extends C {
      y;
    }

    将没有初始化器的字段设置为 undefined 而不是擦除它们的语义意义在于,字段声明提供了一个可靠的基础,以确保属性存在于所创建的对象上。这有助于程序员让对象保持相同的总体状态,从而使其易于推理,有时在实现中也更容易优化。

    私有语法

    私有字段基于使用 # 的语法,无论是在声明字段时还是访问它时。

    class X {
      #foo;
      method() {
        console.log(this.#foo)
      }
    }

    这种语法试图既简洁又直观,尽管它与其他编程语言相当不同。参见私有语法 FAQ,了解所考虑的替代方案以及导致这种语法的约束。

    没有私有计算属性名:#foo 是私有标识符,而 #[foo] 是语法错误。

    没有访问私有的后门

    私有字段提供了强封装边界:不可能从类外部访问私有字段,除非有一些显式代码暴露它(例如提供 getter)。这与支持各种反射和元编程的 JavaScript 属性不同,而是类似于闭包和 WeakMap 等机制,它们不提供对其内部的访问。参见这些 FAQ 条目,了解这一决定动机的更多信息。

    一些使其更容易访问的缓解措施

    • 实现的开发者工具可能提供对私有字段的访问(V8 问题)。
    • 装饰器提案提供了易于使用且受控地访问私有字段的工具。

    初始化器表达式的执行

    公有和私有字段在构造函数运行期间,按照其声明的顺序被添加到实例中。初始化器会为每个类实例重新求值。字段在初始化器运行之后、求值下一个初始化器之前被添加到实例中。

    作用域:正在构造的实例在初始化器表达式内部作为 this 值处于作用域中。new.target 为 undefined,与方法中一样。对 arguments 的引用是早期错误。super 方法调用 super.method() 在初始化器中可用,但 super 构造函数调用 super() 是语法错误。await 和 yield 在初始化器中不可用,即使类是在 async 函数/生成器内部声明的。

    当字段初始化器被求值以及字段被添加到实例时:

    • 基类:在构造函数执行开始时,甚至在参数解构之前。
    • 派生类:在 super() 返回之后立即。(super() 调用方式的灵活性导致许多实现为此情况制作了一个单独不可见的 initialize() 方法。)

    如果在派生类中没有调用 super(),而是以其他方式,公有和私有字段不会被添加到实例,初始化器也不会被求值。对于基类,初始化器总是会被求值,即使构造函数最终返回了其他内容。new.initialize 提案将添加一种方式,以编程方式向并非来自 super()/基类中 this 值的实例添加字段。

    规范

    完整细节参见规范草案。

    状态

    TC39 中的共识

    该提案于 2017 年 7 月达到第 3 阶段。自那时起,关于各种替代方案进行了广泛思考和冗长讨论,包括:

    在考虑每个提案时,TC39 代表深入研究了其动机、JS 开发者反馈以及对语言设计未来的影响。最终,这一思考过程和持续社区参与使本仓库中的提案重新达成共识。基于该共识,各实现正在推进该提案。

    发展历史

    本文档提出了 公有字段 和 私有字段 的综合愿景,借鉴了早期的 正交类 和 类求值顺序 提案。它的编写目标是向前兼容私有方法和装饰器的引入,其集成在统一类特性提案中说明。方法和访问器在后续提案中定义。

    该提案一直在本 GitHub 仓库以及 TC39 会议的演示和讨论中开发。参见下方的过往演示和讨论笔记。

    实现

    你可以使用以下实现来试验类字段提案:

    • Babel 7.0+
    • Node 12
    • Chrome/V8
      • 公有字段在 Chrome 72 / V8 7.2 中启用
      • 私有字段在 Chrome 74 / V8 7.4 中启用
    • Firefox/SpiderMonkey
      • 公有实例字段在 Firefox 69 中启用
      • 公有静态字段在 Firefox 75 中启用
    • Safari/JSC
      • 公有实例字段在 Safari 14 中启用
      • 公有静态字段在 Safari Technology Preview 117 中启用
      • 私有字段在 Safari Technology Preview 117 中启用
    • Moddable XS
    • QuickJS
    • TypeScript 3.8

    更多实现即将推出:

    欢迎在本仓库中参与

    我们鼓励你在本仓库提交 issue 和 PR,以

    • 询问关于提案、语法如何工作、语义意味着什么等问题。
    • 讨论实现和测试经验,以及该过程中出现的问题。
    • 开发改进的文档、示例代码,以及其他向各个层次的程序员介绍此特性的方式。

    如果你对如何改进 JavaScript 有任何额外想法,请参阅 ecma262 的 CONTRIBUTING.md,了解如何参与。