Grouped Accessors and Auto-Accessors S1
中文标题:分组访问器与自动访问器
提案速览
该提案为类和对象字面量引入了分组访问器语法,并为类引入了自动访问器语法。分组访问器允许将 get 和 set 方法定义在一起,而自动访问器则提供了一种带有隐式私有后备字段的简化形式。其旨在提高可读性、编辑器体验和装饰器能力。
Note
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
ECMAScript 的分组访问器与自动访问器
这引入了一项针对类和对象字面量的 分组访问器(grouped accessors) 以及针对类的 自动访问器(auto-accessors) 新语法进行的研究。
分组访问器 是包含访问器的 get 和 set 方法之一或两者的单个声明。
自动访问器 是 分组访问器 的一种简化变体,它省略了 get 和 set 方法的方法体,并
引入一个供 getter 和 setter 两者使用的私有后备字段。
考虑中:未来我们可能会考虑将 自动访问器 扩展到对象字面量,然而目前尚未为对象字面量定义所需的
私有名称语义。
状态
阶段: 1
提案倡导者: Ron Buckton (@rbuckton)
有关此提案的详细状态,请参见下面的 TODO。
作者
分组访问器
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) { ... }
}
};
分组访问器 本质上是在单个逻辑组中定义访问器的 get 和 set 方法之一或两者的方法。这提供了以下好处:
get 和 set 声明在逻辑上被分组在一起,这提高了可读性。
- 对于支持折叠的编辑器(即
▶ accessor x { ... }),这也可以带来更好的编辑体验。
- 具有 计算属性名(ComputedPropertyName) 的成组
get 和 set 访问器对只需对其名称求值一次。
- 在
class 中,可以使用 #set 将公共属性的 setter 标记为私有,这会为该标识符引入一个公共绑定,
并为该标识符(但以 # 作为前缀)引入一个私有绑定。例如:
class C {
accessor y {
get() { ... }
#set(value) { ... }
}
}
在原型上引入一个 y getter,并在实例上引入一个 #y setter。
- 应用于该组的装饰器可以同时观察到
get 和 set 方法,以用于纠缠操作。例如:
function dec({ get, set }, context) {
...
return { get, set, };
}
class C {
@dec
accessor x {
get() { ... }
set(value) { ... }
}
}
- 这允许您重用为具有成对
get 和 set 声明的 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; }; // 错误:访问器已经是私有的
}
自动访问器 是 分组访问器 的简化版本,允许您省略 get 和 set 方法的主体,并可选地提供初始化器。自动访问器 在类上引入一个唯一的 未命名 私有字段,该字段被生成的 getter 和可选的生成的 setter 包装。使用 #set 而不是 set 表示与公共成员同名的私有 setter(但以 # 作为前缀)存在于对象上,并提供对设置底层值的特权访问。
这提供了以下好处:
- 引入可以在子类中被覆盖的访问器,而无需过多的样板代码。
- 提供字段的替代方案,允许您使用装饰器观察字段值的读取和写入。
- 允许您在声明内联执行初始化,类似于字段。
- 允许您独立于整个访问器来装饰各个
get 或 set 方法存根。例如:
class C {
accessor x { @dec get; set; }
}
- 这允许您在
accessor 字段定义的上下文中重用仅为 get 或 set 方法设计的装饰器。
提议的语法
关于提议的语法,请参阅[规范][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; } // 语法错误
}
- 不能将
get、set 或 #set 存根定义与 get、set 或 #set 方法混合使用:
class C {
accessor x { get; #set; } // 可以
accessor y { get() { return 1; } set(v) { } } // 可以
accessor z { get() { return 1; } set; } // 错误
}
- 不能将
get、set 或 #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 声明以及各个 get 和 set 方法存根:
class C {
@dec1 // 作为 `dec1({ get, set }, context)` 调用
accessor x {
@dec2 // 作为 `dec2(fn, context)` 调用
get;
@dec3 // 作为 `dec3(fn, context)` 调用
set;
}
}
- 分组访问器 上的装饰器能够同时访问
get 和 set 声明,类似于 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 = ...;
}
}
本提案还有助于在装饰形式之间提供一致性,这有助于装饰器的可重用性:
先前工作
示例
分组访问器
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 阶段准入标准
第 2 阶段准入标准
第 3 阶段准入标准
第 4 阶段准入标准