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/stage/1/proposal-class-access-expressions.md.
  • 简体中文
  • class Access Expressions S1

    中文标题:类访问表达式

    提案概览
    提案速览

    该提案引入了 class 表达式语法,以简化对包含的词法类的静态成员的访问,尤其适用于匿名类。它涵盖了属性访问、赋值、方法调用和私有静态成员,语义与 super 对齐。

    Note

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

    ECMAScript 类属性访问表达式

    类访问表达式旨在简化对类静态成员的访问,并提供了在类未命名时访问类静态成员的能力:

    class C {
      static f() { ... }
      
      g() {
        class.f();
      }
    }

    状态

    阶段: 1
    提案负责人: Ron Buckton (@rbuckton)

    有关此提案的详细状态,请参阅下面的 TODO

    作者

    • Ron Buckton (@rbuckton)

    动机

    如今,ECMAScript 开发者可以编写具有静态成员的类,这些静态成员可以通过以下两种方式之一进行访问:

    • 通过类名:
      class C {
        static x() {}
        y() { C.x(); }
      }
    • 通过静态成员中的 this
      class C {
        static x() {}
        static y() { this.x(); }
      }

    然而,对于没有名称的类,没有简单的机制来访问类静态成员:

    export default class {
      static x() { }
      y() { 
        this.constructor.x(); // 实际上不能保证是正确的 `x`。
      }
    }
    
    
    const x = class { 
      static x() { }
      y() {
        this.constructor.x(); // 实际上不能保证是正确的 `x`。
      }
    }

    在上面的示例中,不能保证是正确的 x,因为您可能正在访问子类中重写的成员。

    此外,对于当前的私有静态字段和方法提案,在静态方法中使用 this 时很容易在运行时遇到错误:

    class C {
      static #x() {}
      static y() { this.#x(); }
    }
    class D extends C {}
    D.y(); // TypeError

    语法

    // 从非静态方法中
    class C {
      static f() { }
      g() {
        class.f();
        class["f"]();
      }
    }
    
    // 从静态方法中
    class C {
      static f() { }
      static g() {
        class.f();
        class["f"]();
      }
    }
    
    // 使用静态私有成员
    class C {
      static #f() {}
      static g() {
        class.#f();
      }
    }

    语义

    • 函数环境记录在表 15 中有一个新字段:
      字段名含义
      [[InitialClassObject]]对象 | undefined如果关联函数具有 class 属性访问且不是 ArrowFunction,则 [[InitialClassObject]] 是该函数作为方法绑定的类。[[InitialClassObject]] 的默认值为 undefined
    • 函数环境记录在表 16 中有一个新方法:
      方法用途
      GetClassBinding()返回作为此环境记录中绑定的 class 属性访问基础的对象。
    • ECMAScript 函数对象在表 27 中有一个新的内部槽:
      内部槽类型描述
      [[InitialClassObject]]对象 | undefined如果函数具有 class 属性访问,则这是 class 属性查找开始的对象。
    • 在 ClassDefinitionEvaluation 期间,类构造函数 (F) 被设置为方法上的 [[InitialClassObject]]。
    • 在 NewFunctionEnvironment 期间,[[InitialClassObject]] 从方法 (F) 复制到 envRec.[[InitialClassObject]]。
    • 箭头函数使用其包含的词法环境的 [[InitialClassObject]](类似于 superthis)。
    • 当评估 ClassProperty: `class` `.` IdentifierName 时,我们返回一个新的引用,具有以下属性:
      • 引用的名称组件是 IdentifierName 的 StringValue。
      • 基础值组件是 GetThisEnvironment() 的 [[InitialClassObject]]。
    • 当评估 ClassProperty: `class` `[` Expression `]` 时,我们返回一个新的引用,具有以下属性:
      • 引用的名称组件是对评估 Expression 的结果调用 GetValue 后调用 ?ToPropertyKey 的结果。
      • 基础值组件是 GetThisEnvironment() 的 [[InitialClassObject]]。
    • 当评估 ClassProperty: `class` `.` PrivateIdentifier 时,我们执行以下步骤:
      1. fieldNameStringPrivateIdentifier 的 StringValue。
      2. bv 为 GetThisEnvironment() 的 [[InitialClassObject]]。
      3. 返回 ?MakePrivateReference( bv, fieldNameString )
    • class 绑定仅可用于以下声明:
    • class 绑定在以下任何声明中都是 无效 的:
      • FunctionDeclarationFunctionExpression
      • 对象字面量的方法。
      • 对象字面量的访问器。
      • 未嵌套在词法 class 声明中的任何执行上下文,包括模块或全局作用域。
      • 注意: 这与 super 的行为一致。

    示例

    属性访问

    在类方法或类构造函数中,获取 class.x 的值始终指的是包含的词法类上属性 x 的值:

    class Base {
        static f() {
            console.log(`this: ${this.name}, class: ${class.name})`);
        }
    }
    class Sub extends Base {
    }
    
    Base.f();                           // this: Base, class: Base
    Sub.f();                            // this: Sub, class: Base
    Base.f.call({ name: "Other" });     // this: Other, class: Base

    此行为提供了以下好处:

    • 能够引用包含的词法类的静态成员,而无需重复类名。
    • 能够引用匿名类声明或表达式的静态成员:
      export default class {
          static f() { ... }
          g() { class.f(); }
      }

    属性赋值

    在类方法或类构造函数中,设置 class.x 的值始终更新包含的词法类上属性 x 的值:

    function print(F) {
        const { name, x, y } = F;
        const hasX = F.hasOwnProperty("x") ? "own" : "inherited";
        const hasY = F.hasOwnProperty("y") ? "own" : "inherited";
        console.log(`${name}.x: ${x} (${hasX}), ${name}.y: ${y} (${hasY})`);
    }
    
    class Base {
        static f() {
            this.x++;
            class.y++;
        }
    }
    
    Base.x = 0;
    Base.y = 0;
    
    class Sub extends Base {
    }
    
    print(Base);                        // Base.x: 0 (own), Base.y: 0 (own)
    print(Sub);                         // Sub.x: 0 (inherited), Sub.y: 0 (inherited)
    
    Base.f();
    
    print(Base);                        // Base.x: 1 (own), Base.y: 1 (own)
    print(Sub);                         // Sub.x: 1 (inherited), Sub.y: 1 (inherited)
    
    Sub.f();
    
    print(Base);                        // Base.x: 1 (own), Base.y: 2 (own)
    print(Sub);                         // Sub.x: 2 (own), Sub.y: 2 (inherited)
    
    Base.f();
    
    print(Base);                        // Base.x: 2 (own), Base.y: 3 (own)
    print(Sub);                         // Sub.x: 2 (own), Sub.y: 3 (inherited)

    此行为提供了以下好处:

    • 赋值总是发生在当前词法类上,这对用户来说应该不意外。

    方法调用

    在方法、初始化器或构造函数中调用 class.x() 使用包含的词法类的值作为接收者:

    class Base {
        static f() {
            console.log(`this.name: ${this.name}, class.name: ${class.name})`);
        }
        static g() {
            class.f();
        }
        h() {
            class.f();
        }
    }
    class Sub extends Base {
    }
    
    Base.g();                           // this: Base, class: Base
    Sub.g();                            // this: Sub, class: Base
    Base.g.call({ name: "Other" });     // this: Other, class: Base
    
    let b = new Base();
    let s = new Sub();
    b.h();                              // this: Base, class: Base
    s.h();                              // this: Sub, class: Base
    b.h.call({ name: "Other" });        // this: Other, class: Base

    无效用法

    class C {
      static x = 1;
      constructor() {
        function f() {
          return class.x; // 函数无法访问 `class.`
        }
        f(); // 抛出 TypeError
    
        const obj = {
          method() {
            return class.x; // 对象字面量的方法无法访问 `class`。
          }
        };
        obj.method(); // 抛出 TypeError
      }
    }

    语法

    MemberExpression[Yield, Await] :
      ClassProperty[?Yield, ?Await]
    
    ClassProperty[Yield, Await] :
      `class` `[` Expression[+In, ?Yield, ?Await] `]`
      `class` `.` IdentifierName
      `class` `.` PrivateIdentifier

    与其他提案的关系

    类字段

    本提案可以轻松地与当前的类字段提案对齐,提供更容易访问静态字段的方式,而不会产生意外行为:

    class Base {
        static counter = 0;
        id = class.counter++;       // `Base` 用作 `this`
    }
    
    class Sub extends Base {
    }
    
    console.log(new Base().id);     // 0
    console.log(new Sub().id);      // 1
    console.log(Base.counter);      // 2
    console.log(Sub.counter);       // 2

    类私有字段

    除了私有方法之外,本提案还可以与当前的类私有字段提案对齐,提供对类静态私有状态的访问,而不会因 this 错误而引入 TypeError:

    class Base {
        static #counter = 0;
        static increment() {
            return class.#counter++;
        }
    }
    
    class Sub extends Base {
    }
    
    console.log(Base.increment());  // 0
    console.log(Sub.increment());   // 1
    console.log(Base.increment());  // 2

    类私有方法

    class 访问表达式的好处之一是它们保证在访问静态私有成员时使用正确的引用。但是,在使用 class 访问表达式调用私有和非私有静态方法时必须特别小心,因为被调用方法中的 this 绑定将是词法类声明:

    class Base {
      static #counter = 0;
      static #increment() {
        class.#counter++;
        this.printCounter();
      }
      static doIncrement() {
        class.#increment();
      }
      static printCounter() {
        console.log(class.#counter);
      }
    }
    class Sub extends Base {
      static printCounter() {
        console.log("Custom Counter");
        super.printCounter();
      }
    }
    
    Base.doIncrement(); // 打印:1
    Sub.doIncrement(); // 打印:2

    在上面的示例中,Sub 重写的 printCounter 永远不会被调用。此类方法调用需要重写:

    // 选项 1:
    class Base {
      ...
      static #increment() {
        ...
      }
      static doIncrement() {
        class.#increment.call(this);
      }
    }
    
    // 选项 2:
    class Base {
      ...
      static #increment(C) {
        ...
        C.printCounter();
      }
      ...
      static doIncrement() {
        class.#increment(this);
      }
    }

    这是因为在此示例中,class. 本质上是 Base. 的替代品,因此在这些方法调用中 Base 成为接收者。

    TODO

    以下是推进 TC39 提案流程 每个阶段所需完成的高级任务列表:

    Stage 1 进入标准

    • 确定了一位“提案负责人”来推进该添加。
    • 散文 概述了问题或需求以及解决方案的大致形式。
    • 说明性的 示例 用法。
    • 高级 API

    Stage 2 进入标准

    Stage 3 进入标准

    Stage 4 进入标准

    • 已为主要使用场景编写了 Test262 验收测试,并 合并
    • 两个兼容的实现通过了验收测试:[1][2]
    • 已向 tc39/ecma262 发送了包含集成规范文本的 拉取请求
    • ECMAScript 编辑器已签署 拉取请求