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/pending/proposal-grouped-and-auto-accessors.md.
  • 简体中文
  • Grouped Accessors and Auto-Accessors S1

    中文标题:分组访问器与自动访问器

    提案概览
    提案速览

    该提案为类和对象字面量引入了分组访问器语法,并为类引入了自动访问器语法。分组访问器允许将 get 和 set 方法定义在一起,而自动访问器则提供了一种带有隐式私有后备字段的简化形式。其旨在提高可读性、编辑器体验和装饰器能力。

    Note

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

    ECMAScript 的分组访问器与自动访问器

    这引入了一项针对类和对象字面量的 分组访问器(grouped accessors) 以及针对类的 自动访问器(auto-accessors) 新语法进行的研究。 分组访问器 是包含访问器的 getset 方法之一或两者的单个声明。 自动访问器分组访问器 的一种简化变体,它省略了 getset 方法的方法体,并 引入一个供 getter 和 setter 两者使用的私有后备字段。

    考虑中:未来我们可能会考虑将 自动访问器 扩展到对象字面量,然而目前尚未为对象字面量定义所需的 私有名称语义。

    状态

    阶段: 1
    提案倡导者: Ron Buckton (@rbuckton)

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

    作者

    • Ron Buckton (@rbuckton)

    分组访问器

    class C {
      accessor x {
        get() { ... } // 等价于 `get x() { ... }`
        set(value) { ... } // 等价于 `set x(value) { ... }`
      }
    
      accessor y {
        get() { ... } // 等价于 `get y() { ... }`
        #set(value) { ... } // 等价于 `set #y(value) { ... }`
      }
    
      accessor #z {
        get() { ... } // 等价于 `get #z() { ... }`
        set(value) { ... } // 等价于 `set #z(value) { ... }`
      }
    
      static accessor x {
        get() { ... } // 等价于 `static get x() { ... }`
        set(value) { ... } // 等价于 `static set x(value) { ... }`
      }
    }
    
    const obj = {
      accessor x {
        get() { ... }
        set(value) { ... }
      }
    };

    分组访问器 本质上是在单个逻辑组中定义访问器的 getset 方法之一或两者的方法。这提供了以下好处:

    • getset 声明在逻辑上被分组在一起,这提高了可读性。
      • 对于支持折叠的编辑器(即▶ accessor x { ... }),这也可以带来更好的编辑体验。
    • 具有 计算属性名(ComputedPropertyName) 的成组 getset 访问器对只需对其名称求值一次。
    • class 中,可以使用 #set 将公共属性的 setter 标记为私有,这会为该标识符引入一个公共绑定, 并为该标识符(但以 # 作为前缀)引入一个私有绑定。例如:
      class C {
        accessor y {
          get() { ... }
          #set(value) { ... }
        }
      }
      在原型上引入一个 y getter,并在实例上引入一个 #y setter。
    • 应用于该组的装饰器可以同时观察到 getset 方法,以用于纠缠操作。例如:
      function dec({ get, set }, context) {
        ...
        return { get, set, };
      }
      class C {
        @dec
        accessor x {
          get() { ... }
          set(value) { ... }
        }
      }
      • 这允许您重用为具有成对 getset 声明的 accessor 字段而设计的装饰器。
      • 这也提供了第 1 阶段装饰器提案中存在的、而当前第 3 阶段装饰器提案中所缺失的能力。

    自动访问器

    class C {
      accessor a = 1;                   // 等同于 `accessor a { get; set; } = 1;`
      accessor b { } = 1;               // 等同于 `accessor b { get; set; } = 1;`
      accessor c { get; set; } = 1;     // 等同于 `accessor c = 1;`
      accessor d { get; } = 1;          // 有 getter 但没有 setter
      accessor e { set; } = 1;          // 有 setter 但没有 getter(用例:装饰器)
      accessor f { get; #set; };        // 具有私有 setter `#f` 的 getter;
      accessor g { #set; } = 1;         // 私有 setter 但没有 getter(用例:装饰器)
      accessor #h = 1;                  // 等同于 `accessor #h { get; set; } = 1;`
      accessor #i { } = 1;              // 等同于 `accessor #i { get; set; } = 1;`
      accessor #j { get; set; } = 1;    // 等同于 `accessor #j = 1;`
      accessor #k { get; } = 1;         // 有 getter 但没有 setter
      accessor #l { set; } = 1;         // 有 setter 但没有 getter(用例:装饰器)
    
      // 也允许:
      accessor "foo";                   // 等同于 `accessor "foo" { get; set; }`
      accessor 1;                       // 等同于 `accessor 1 { get; set; }`
      accessor [x];                     // 等同于 `accessor [x] { get; set; }`
      static accessor a = 1;            // 以及其他所有非静态变体
    
      // 不允许:
      // accessor "bar" { get; #set; }  // 错误:字符串属性不能有私有 setter
      // accessor 2 { get; #set; }      // 错误:数字属性不能有私有 setter
      // accessor [y] { get; #set; }    // 错误:计算属性不能有私有 setter
      // accessor #m { get; #set; };    // 错误:访问器已经是私有的
      // accessor #n { #set; };         // 错误:访问器已经是私有的
    }

    自动访问器分组访问器 的简化版本,允许您省略 getset 方法的主体,并可选地提供初始化器。自动访问器 在类上引入一个唯一的 未命名 私有字段,该字段被生成的 getter 和可选的生成的 setter 包装。使用 #set 而不是 set 表示与公共成员同名的私有 setter(但以 # 作为前缀)存在于对象上,并提供对设置底层值的特权访问。

    这提供了以下好处:

    • 引入可以在子类中被覆盖的访问器,而无需过多的样板代码。
    • 提供字段的替代方案,允许您使用装饰器观察字段值的读取和写入。
    • 允许您在声明内联执行初始化,类似于字段。
    • 允许您独立于整个访问器来装饰各个 getset 方法存根。例如:
      class C {
        accessor x { @dec get; set; }
      }
      • 这允许您在 accessor 字段定义的上下文中重用仅为 getset 方法设计的装饰器。

    提议的语法

    关于提议的语法,请参阅[规范][Specification]。

    提议的语义

    有关完整的提议语义,请参阅[规范][Specification]。以下内容代表了此提案的一些近似语义。其要点如下:

    • 只有具有 标识符名称(IdentifierName) 名称的 accessor 属性才能具有 #set 存根或 #set 方法:
      class C {
          accessor x { get; #set; } // 可以
          accessor #y { get; #set; } // 语法错误
          accessor "z" { get; #set; } // 语法错误
          accessor 1 { get; #set; } // 语法错误
          accessor [expr] { get; #set; } // 语法错误
      }
    • 同一组中不能同时存在 set#set
      class C {
          accessor x { get; #set; } // 可以
          accessor y { set; #set; } // 语法错误
      }
    • 不能将 getset#set 存根定义与 getset#set 方法混合使用:
      class C {
          accessor x { get; #set; } // 可以
          accessor y { get() { return 1; } set(v) { } } // 可以
          accessor z { get() { return 1; } set; } // 错误
      }
    • 不能将 getset#set 方法与初始化器结合使用:
      class C {
          accessor w = 1; // 可以
          accessor x { get; } = 1; // 可以
          accessor y { get; set; } = 1; // 可以
          accessor z { get() { return 1; } } = 1; // 错误
      }
    • 不能有与类上另一个私有名称冲突的带有 #set 存根或方法的 accessor 属性:
      class C {
          #w;
          accessor w; // 可以
      
          #x;
          accessor x { get; set; }; // 可以
      
          #y;
          accessor y { get; #set; }; // 错误(与 #y 冲突)
      
          #z;
          accessor z { get() { } #set(v) { } }; // 错误(与 #z 冲突)
      }
    • 对象字面量不能有自动访问器:
      const obj = {
        accessor x { get() { return 1; } }, // 可以
        accessor y { get; }, // 语法错误
      };

    与装饰器的交互

    本提案旨在与装饰器提案相衔接,并共享该提案中的自动访问器语法。本提案在以下方面扩展了装饰器提案:

    • 通过向 自动访问器 添加 AccessorGroup,您能够同时装饰整个 accessor 声明以及各个 getset 方法存根:
      class C {
          @dec1 // 作为 `dec1({ get, set }, context)` 调用
          accessor x {
              @dec2 // 作为 `dec2(fn, context)` 调用
              get;
      
              @dec3 // 作为 `dec3(fn, context)` 调用
              set;
          }
      }
    • 分组访问器 上的装饰器能够同时访问 getset 声明,类似于 TypeScript 和 Babel 中的早期装饰器实现:
      class C {
          @dec1 // 作为 `dec1({ get, set }, context)` 调用
          accessor x {
              get() { ... }
              set(v) { ... }
          }
      
          @dec2 get y() { ... } // 作为 `dec2(fn, context)` 调用
      }
    • 自动访问器 类似,分组访问器 可以在 accessor 声明级别以及各个 getter 和 setter 声明级别进行装饰:
      class C {
          @dec1 // 作为 `dec1({ get, set }, context)` 调用
          accessor x {
              @dec2 // 作为 `dec2(fn, context)` 调用
              get() { ... }
      
              @dec3 // 作为 `dec3(fn, context)` 调用
              set(v) { ... }
          }
      }

    此外,自动访问器 的某些方面乍一看似乎是边缘情况,但通过装饰器会变成强大的能力:

    // 只读访问器
    
    // 一个只有 getter 且没有初始化器的访问器,其本身用途有限...
    class Service {
      accessor users { get; }
    }
    
    // ...然而,可以使用装饰器通过替换 getter 来执行依赖注入:
    class Service {
      @inject("userService")
      accessor users { get; }
    }
    
    
    // 只写访问器
    
    // 一个没有 getter 的 setter,其本身用途有限...
    class WriteOnlyCounter {
      accessor inc { set; } 
    }
    
    // ...然而,装饰器可以替换 setter 使其更有用:
    class WriteOnlyCounter {
      @observeChanges()
      accessor inc { set; }
    }
    
    
    // 仅私有设置访问器
    
    // 以下内容本可以写成 `accessor #value { set; }`...
    class Widget {
      accessor value { #set; }
    
      exec() {
        this.#value = ...;
      }
    }
    
    // ...然而,这里的装饰器可以附加一个公共的 getter,如果 `value` 命名为 `#value`,
    // 这将是无法做到的。
    class Widget {
      @decorator
      accessor value { #set; }
    
      exec() {
        this.#value = ...;
      }
    }

    本提案还有助于在装饰形式之间提供一致性,这有助于装饰器的可重用性:

    元素kind: "get"kind: "set"kind: "accessor"
    get x() {}✔️
    set x(v) {}✔️
    accessor x;✔️
    accessor x { get; set; }✔️✔️✔️
    accessor x { get() {} set(v) {} }✔️✔️✔️

    先前工作

    • C#(12

    示例

    分组访问器

    class Point {
      #x = 0;
      #y = 0;
    
      accessor x {
        get() { return this.#x; }
        set(v) {
          if (typeof v !== "number") throw new RangeError();
          this.#x = v;
        }
      }
    
      accessor y {
        get() { return this.#y; }
        set(v) {
          if (typeof v !== "number") throw new RangeError();
          this.#y = v;
        }
      }
    }

    自动访问器

    class Customer {
      accessor id { get; #set; } // 公共 get,私有 set
      accessor name;
      constructor(id, name) {
        this.#id = id;
        this.name = name;
      }
    }
    const c = new Customer(1, "Jane");
    c.id; // 1
    c.id = 2; // TypeError

    待办事项

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

    第 1 阶段准入标准

    • 确定一位将推进该补充的"[倡导者][Champion]"。
    • 概述问题或需求以及解决方案大致形态的[说明][Prose]。
    • 说明性的[示例][Examples]。
    • 高级API

    第 2 阶段准入标准

    • [初始规范文本][Specification]。
    • [转译器支持][Transpiler](可选)。

    第 3 阶段准入标准

    • [完整的规范文本][Specification]。
    • 指定的审阅者已签署当前的规范文本。
    • ECMAScript 编辑已签署当前的规范文本。

    第 4 阶段准入标准

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