中文标题:提取器(Extractors)
提案速览
该提案向 ECMAScript 引入提取器,扩展解构语法以支持用户自定义的验证和转换逻辑。它利用模式匹配提案中的 Symbol.customMatcher 符号,允许像 const Foo(y) = x 这样的模式调用用户代码。
Note
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
一个向 ECMAScript 引入提取器(又称“提取器对象”)的提案。
提取器将扩展 BindingPattern 和 AssignmentPattern 的语法,允许新的解构形式,如下例所示:
// 绑定模式
const Foo(y) = x; // 实例数组解构
const Foo([y]) = x; // 嵌套数组解构
const Foo({y}) = x; // 嵌套对象解构
const [Foo(y)] = x; // 嵌套
const { z: Foo(y) } = x; // ...
const Foo(Bar(y)) = x; // ...
const X.Foo(y) = x; // 限定名称(即 a.b.c)
// 赋值模式
Foo(y) = x; // 实例数组解构
Foo([y]) = x; // 嵌套数组解构
Foo({y}) = x; // 嵌套对象解构
[Foo(y)] = x; // 嵌套
({ z: Foo(y) } = x); // ...
Foo(Bar(y)) = x; // ...
X.Foo(y) = x; // 限定名称(即 a.b.c)
此外,这将利用模式匹配提案新增的内置符号 Symbol.customMatcher。当使用新形式进行解构时,会调用 Symbol.customMatcher 方法,并对其结果进行解构。
状态
阶段: 1
提案负责人: Ron Buckton (@rbuckton)
有关更多信息,请参阅 TC39 提案流程。
作者
动机
ECMAScript 目前没有在解构期间执行用户自定义逻辑的机制,这意味着与数据验证和转换相关的操作可能需要多条语句:
function toInstant(value) {
if (value instanceof Temporal.Instant) {
return value;
} else if (value instanceof Date) {
return Temporal.Instant.fromEpochMilliseconds(+value);
} else if (typeof value === "string") {
return Temporal.Instant.from(value);
} else {
throw new TypeError();
}
}
class Book {
constructor({
isbn,
title,
createdAt = Temporal.Now.instant(),
modifiedAt = createdAt
}) {
this.isbn = isbn;
this.title = title;
this.createdAt = toInstant(createdAt);
// 如果 `modifiedAt` 为 `undefined`,则一些工作重复
this.modifiedAt = toInstant(modifiedAt);
}
}
new Book({ isbn: "...", title: "...", createdAt: Temporal.Instant.from("...") });
new Book({ isbn: "...", title: "...", createdAt: new Date() });
new Book({ isbn: "...", title: "...", createdAt: "..." });
使用 Extractors,此类验证和转换逻辑可以封装并在绑定模式 内部 复用:
const InstantExtractor = {
[Symbol.customMatcher](value) {
if (value instanceof Temporal.Instant) {
return [value];
} else if (value instanceof Date) {
return [Temporal.Instant.fromEpochMilliseconds(+value)];
} else if (typeof value === "string") {
return [Temporal.Instant.from(value)];
}
}
};
class Book {
constructor({
isbn,
title,
// 将 `createdAt` 提取为 Instant
createdAt: InstantExtractor(createdAt) = Temporal.Now.instant(),
modifiedAt: InstantExtractor(modifiedAt) = createdAt
}) {
this.isbn = isbn;
this.title = title;
this.createdAt = createdAt;
this.modifiedAt = modifiedAt;
}
}
new Book({ isbn: "...", title: "...", createdAt: Temporal.Instant.from("...") });
new Book({ isbn: "...", title: "...", createdAt: new Date() });
new Book({ isbn: "...", title: "...", createdAt: "..." });
当与一个支持代数数据类型(ADT)的即将推出的 enum 提案结合使用时,这也将非常有用:
// 类似 Rust 的代数数据类型枚举:
enum Option of ADT {
Some(value),
None
}
// 构造
const x = Option.Some(1);
// 解构
const Option.Some(y) = x;
y; // 1
// 模式匹配
match (x) {
when Option.Some(y): console.log(y); // 1
when Option.None: console.log("none");
}
// 另一个 ADT 枚举示例:
enum Message of ADT {
Quit,
Move({x, y}),
Write(message),
ChangeColor(r, g, b),
}
// 构造
const msg1 = Message.Move({ x: 10, y: 10 });
const msg2 = Message.Write("Hello");
const msg3 = Message.ChangeColor(0x00, 0xff, 0xff);
// 解构
const Message.Move({ x, y }) = msg1; // x: 10, y: 10
const Message.Write(message) = msg2; // message: "Hello"
const Message.ChangeColor(r, g, b) = msg3; // r: 0, g: 255, b: 255
// 模式匹配
match (msg) {
when Message.Move({ x, y }): ...;
when Message.Write(message): ...;
when Message.ChangeColor(r, g, b): ...;
when Message.Quit: ...;
}
提议的解决方案
Extractors 松散地基于 Scala 的提取器对象和 Rust 的模式匹配。提取器扩展了 BindingPattern 和 AssignmentPattern 的语法,以允许评估用于验证和转换的用户定义逻辑。
提取器在成功的匹配结果上执行数组解构,从使用 ExtractorMemberExpression 的作用域中的值引用开始,该表达式本质上是一个 IdentifierReference(例如 Point、InstantExtractor 等)或点号名称(例如 Option.Some、Message.Move 等)。
当在解构期间评估提取器时,会评估其 ExtractorMemberExpression,并使用当前要解构的值调用该评估结果的 [Symbol.customMatcher]() 方法,返回一个可迭代对象,指示匹配成功以及用于进一步解构的提取元素。为了解构的目的,任何其他值都将产生 TypeError。在模式匹配的情况下,true 和 false 也是有效的返回值。
提取器由 ExtractorMemberExpression 后跟括号列表的其他解构模式组成:
// 绑定模式
let Foo(a, { b }, [c]) = ...;
// 赋值模式
Foo(a, { b }, [c]) = ...;
使用括号(())而不是方括号([])的原因有几个:
- 当提取器是赋值模式的一部分时,避免与 ElementAccessExpression 冲突:
Option.Some[value] = opt; // 已经是元素访问表达式
- 确保绑定和赋值模式之间的语法一致:
let Option.Some(value) = opt;
Option.Some(value) = opt;
- 允许解构(和模式匹配)镜像构造/应用:
let opt = Option.Some(x);
let Option.Some(y) = opt;
opt = Option.Some(x);
Option.Some(y) = opt;
使用提取器的对象解构
提取器不会引入用于对象提取的新颖语法。相反,提取器可以返回包含要进一步解构的对象的单个元素匹配结果:
// 绑定模式
const Message.Move({ x, y }) = ...;
// 赋值模式
(Message.Move({ x, y }) = ...);
可迭代包装器开销
值得注意的是,这具有分配可迭代包装器对象以执行进一步解构的开销。如有必要,如果考虑一种替代的匹配结果表示,指示结果是唯一的提取值,则可以消除此开销,即:
const Message = {
Move: class Move {
#x;
#y;
...
static [Symbol.match](value) {
if (value instanceof Message.Move) {
// 'match: "unary"' 表示 'value' 是唯一的提取值
return { match: "unary", value: { x: value.#x, y: value.#y } };
}
return false;
}
}
};
const Message.Move({ x, y }) = ...;
未来的对象提取器语法
未来可能会出现一种属性字面量构造语法,可能被代数数据类型或其他构造函数使用,即:
const enum Message of ADT {
Move{ x, y },
Write(text),
Quit
}
// ADT 构造
const msg = Message.Move{ x: 10, y: 20 };
// 可能用于固定形状对象(即“struct”)的属性字面量构造,
// 或通过内置符号命名方法的用户定义构造机制:
struct Point { x, y };
const pt = Point{ x: 10, y: 20 };
然而,目前这种语法不在本提案的范围内。此外,为提取器引入 Identifier { 语法很可能会占用太多其他提案可能使用的语法空间。因此,像 const Point{ x, y } = p 这样的“对象提取器”类语法目前不在本提案的考虑范围内。
先例
相关提案
示例
本节示例使用脱糖来解释底层语义,给定以下辅助函数:
function %InvokeCustomMatcherOrThrow%(extractor, subject, receiver) {
if (typeof extractor !== "object" || extractor === null) {
throw new TypeError();
}
const f = extractor[Symbol.customMatcher];
if (typeof f !== "function") {
throw new TypeError();
}
const result = f.apply(extractor, [subject, "list", receiver]);
if (typeof result !== "object" || result === null) {
throw new TypeError();
}
return result;
}
const C(x) = subject
给定,
class C {
#data;
constructor(data) {
this.#data = data;
}
[Symbol.customMatcher](subject) {
return #data in subject && [this.#data];
}
}
const subject = new C("data");
const C(x) = subject;
x; // "data"
语句
近似等同于转置表示
const [x] = %InvokeCustomMatcherOrThrow%(C, subject, undefined);
使得 x 的值为 "data"。
const C(x, y) = subject
给定,
class C {
#first;
#second;
constructor(first, second) {
this.#first = first;
this.#second = second;
}
[Symbol.customMatcher](subject) {
return #first in subject && [this.#first, this.#second];
}
}
const subject = new C(1, 2);
const C(x, y) = subject;
x; // 1
y; // 2
语句
近似等同于转置表示
const [x, y] = %InvokeCustomMatcherOrThrow%(C, subject, undefined);
使得 x 和 y 分别得到值 1 和 2。
const C(x, ...y) = subject
给定,
class C {
#first;
#second;
#third;
constructor(first, second, third) {
this.#first = first;
this.#second = second;
this.#third = third;
}
[Symbol.customMatcher](subject) {
return #first in subject && [this.#first, this.#second, this.#third];
}
}
const subject = new C(1, 2, 3);
const C(x, ...y) = subject;
x; // 1
y; // [2, 3]
语句
const C(x, ...y) = subject;
近似等同于转置表示
const [x, ...y] = %InvokeCustomMatcherOrThrow%(C, subject, undefined);
使得 x 和 y 分别得到值 1 和 [2, 3]。
const C(x = -1, y) = subject
给定,
class C {
#first;
#second;
constructor(first, second) {
this.#first = first;
this.#second = second;
}
[Symbol.customMatcher](subject) {
return #first in subject && [this.#first, this.#second];
}
}
const subject = new C(undefined, 2);
const C(x = -1, y) = subject;
x; // -1
y; // 2
语句
const C(x = -1, y) = subject;
近似等同于转置表示
const [x = -1, y] = %InvokeCustomMatcherOrThrow%(C, subject, undefined);
使得 x 和 y 分别得到值 -1 和 2。
const C({ x }) = subject
给定,
class C {
#data;
constructor(data) {
this.#data = data;
}
[Symbol.customMatcher](subject) {
return #data in subject && [this.#data];
}
}
const subject = new C({ x: 1, y: 2 });
const C({ x, y }) = subject;
x; // 1
y; // 2
语句
const C({ x, y }) = subject;
近似等同于转置表示
const [{ x, y }] = %InvokeCustomMatcherOrThrow%(C, subject, undefined);
使得 x 和 y 分别具有值 1 和 2。
const C(D(x)) = subject
给定,
class C {
#data1;
constructor(data1) {
this.#data1 = data1;
}
[Symbol.customMatcher](subject) {
return #data1 in subject && [this.#data1];
}
}
class D {
#data2;
constructor(data2) {
this.#data2 = data2;
}
[Symbol.customMatcher](subject) {
return #data2 in subject && [this.#data2];
}
}
const subject = new C(D("data"));
const C(D(x)) = subject;
x; // "data"
语句
近似等同于转置表示
const [_a] = %InvokeCustomMatcherOrThrow%(C, subject, undefined);
const [x] = %InvokeCustomMatcherOrThrow%(D, _a, undefined);
使得 x 的值为 "data"。
解构期间的自定义逻辑
给定,
const MapExtractor = {
[Symbol.customMatcher](map) {
const obj = {};
for (const [key, value] of map) {
obj[typeof key === "symbol" ? key : `${key}`] = value;
}
return [obj];
}
};
const obj = {
map: new Map([["a", 1], ["b", 2]])
};
const { map: MapExtractor({ a, b }) } = obj;
a; // 1
b; // 2
语句
const { map: MapExtractor({ a, b }) } = obj;
近似等同于转置表示
const { map: _temp } = obj;
const [{ a, b }] = %InvokeCustomMatcherOrThrow%(MapExtractor, _temp, undefined);
使得 a 和 b 分别得到值 1 和 2。
正则表达式
// 可能作为模式匹配的一部分内置
RegExp.prototype[Symbol.customMatcher] = function (value) {
const match = this.exec(value);
return !!match && [match];
};
const IsoDate = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/;
const IsoTime = /^(?<hours>\d{2}):(?<minutes>\d{2}):(?<seconds>\d{2})$/;
const IsoDateTime = /^(?<date>[^TZ]+)T(?<time>[^Z]+)Z/;
// 匹配 `input`,提取,并(如果匹配失败则抛出异常)使用...解构
// ...嵌套对象提取器
const IsoDate({ groups: { year, month, day } }) = input;
// ...嵌套数组提取器
const IsoDate([, year, month, day]) = input;
// 使用嵌套解构进行简洁的多步提取:
const IsoDateTime({
groups: {
date: IsoDate({ groups: { year, month, day } }),
time: IsoTime({ groups: { hours, minutes, seconds }})
}
}) = input;
// 1. 通过 `IsoDatTime` 正则表达式匹配 `input` 并提取 `date` 和 `time`
// 2. 通过 `IsoDate` 正则表达式匹配 `date` 并提取 `year`、`month` 和 `day` 作为词法绑定
// 3. 通过 `IsoTime` 正则表达式匹配 `time` 并提取 `hours`、`minutes` 和 `seconds` 作为词法绑定
当 ExtractorMemberExpression 的结果是引用时,接收者会被保留并传递给自定义匹配器:
给定,
class C {
#f;
constructor(f) {
this.#f = f;
}
extractor = {
[Symbol.customMatcher](subject, _kind, receiver) {
return receiver.#f(subject);
}
};
}
const obj = new C(data => data.toUpperCase());
const subject = "data";
const obj.extractor(x) = subject;
x; // "DATA"
语句
const obj.extractor(x) = subject;
近似等同于转置表示
const _receiver = obj;
const [x] = %InvokeCustomMatcherOrThrow%(_receiver.extractor, subject, _receiver);
使得 x 的值为 "DATA"。
可能的语法
有关建议的语法,请参阅规范文本。
可能的语义
有关建议的语义,请参阅规范文本。
API
本提案将采用(并继续与模式匹配提案的 Custom Matchers 行为保持一致):
- 自定义匹配器 是一个常规的 ECMAScript 对象值,具有
[Symbol.customMatcher] 方法,该方法接受三个参数:subject(要匹配的值)、hint(要么是 "boolean",要么是 "list")和 receiver,并根据 hint 的值返回布尔值或 Iterable。
- 当
hint 是 "boolean" 时,返回值将通过 ToBoolean() 抽象操作强制转换为布尔值。当 hint 是 "list" 时,返回值必须是 Iterable 对象或假值(即 false、0、null、undefined 等)。
- 在模式匹配中,使用
"boolean" 的 hint 可以避免在返回值不用于进一步匹配时(如 x is C)分配 Iterable 对象的昂贵开销,而 "list" 的 hint 表示将对结果进行进一步匹配(如 x is C(1, 2))。
- 对于解构和绑定模式(例如本提案),
hint 将始终为 "list"。
与模式匹配的关系
我们认为提取器作为模式匹配提案的一部分也将非常有价值,并打算在本提案被采纳后与提案负责人讨论采用事宜。
提取器可以轻松地添加到 MatchPattern 中,使用与解构相同的语法,这将允许更简洁且可能更容易理解的代码:
match (opt) {
// 不使用提取器
when (${Option.Some} with [value]): ...;
// 使用提取器
when (Option.Some(value)): ...;
}
match (msg) {
// 不使用提取器
when (${Message.Move} with { x, y }): ...;
// 使用提取器
when (Message.Move({ x, y })): ...;
}
对于复杂的嵌套模式,这一点更为明显:
match (opt) {
// 不使用提取器
when (${Option.Some} with [${Message.Move} with { x, y }]): ...;
when (${Option.Some} with [${Message.Write} with [text]]): ...;
// 使用提取器
when (Option.Some(Message.Move({ x, y }))): ...;
when (Option.Some(Message.Write(text))): ...;
}
与枚举和代数数据类型的关系
我们坚信 ECMAScript 最终会采用当前 enum 提案的某种形式,鉴于代数数据类型可能提供的特殊价值。枚举提案将强烈支持在 声明、构造、解构 和 模式匹配 之间保持一致且连贯的语法,如下例所示:
enum Message of ADT {
Quit,
Move({x, y}),
Write(message),
ChangeColor(r, g, b),
}
// 构造
const msg1 = Message.Move({ x: 10, y: 10 });
const msg2 = Message.Write("Hello");
const msg3 = Message.ChangeColor(0x00, 0xff, 0xff);
// 解构
const Message.Move({x, y}) = msg1; // x: 10, y: 10
const Message.Write(message) = msg2; // message: "Hello"
const Message.ChangeColor(r, g, b) = msg3; // r: 0, g: 255, b: 255
// 模式匹配
match (msg) {
when Message.Move({x, y}): ...;
when Message.Write(message): ...;
when Message.ChangeColor(r, g, b): ...;
when Message.Quit: ...;
}
在这里,ADT 枚举成员和值的声明、构造、解构和模式匹配是一致的:
enum Message of ADT { Move({ x, y }) } // 声明
const msg = Message.Move({ x, y }); // 构造
const Message.Move({ x, y }) = msg; // 解构
match (msg) {
when Message.Move({ x, y }): ...; // 模式匹配
}
enum Message of ADT { Write(message) } // 声明
const msg = Message.Write(message); // 构造
const Message.Write(message) = msg; // 解构
match (msg) {
when Message.Write(message): ...; // 模式匹配
}
如前所述,未来可能会出现可能被代数数据类型或其他构造函数使用的属性字面量构造语法。添加匹配的对象提取语法将是该提案的责任,超出本提案的范围。
待办事项
以下是推进 TC39 提案流程各个阶段的高层任务列表:
阶段 1 进入标准
阶段 2 进入标准
阶段 3 进入标准
阶段 4 进入标准