Tagged Collection Literals ?
中文标题:标签化集合字面量
提案速览
该提案引入了带标签的集合字面量,允许通过 .from() 方法将自定义构造函数与对象、数组和值字面量语法一起使用。它还通过 Symbol.valueOf 协议扩展了解构和模式匹配,使自定义集合类型能够参与这些操作。其目标是为构建和匹配自定义数据结构提供简洁的语法,同时保留它们各自的不变量。
Note
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
ECMAScript 标签化集合字面量
阶段:已撤回
作者:Kat Marchán (npm, @maybekatz)
提案负责人:Kat Marchán (npm, @maybekatz)
已撤回
本提案于 2018 年 5 月提交给 TC39,并在讨论期间自愿撤回。作者认为该问题最好通过一种与序列迭代器对应的对象解构协议,并配合 Map.from() 等方法来解决。
与许多模式匹配概念所基于的静态语言不同,JavaScript 没有足够的静态类型信息来使解构或模式匹配在这些情况下足够高效和可缓存,以保持良好性能。此外,这种额外的语法并未增加足够的价值,不值得投入语法预算。
基于本提案将提交另一个不同的提案,该提案将以统一的方式讨论创建/提取协议,但不增加额外的语法。
引言
本提案扩展了解构绑定/赋值和 match 语句,使其能够对匹配的数据应用自定义的解构和匹配操作。它还添加了一种新的构造器语法,用简洁的对象和数组风格语法构建这些自定义数据结构,同时保留它们各自的优点和不变量。
该语法本身源自多种其他语言用于此目的的类似语法,并意在让人联想到 JavaScript 中的标签模板字面量——只是使用语言中可用的其他字面量语法。
本提案源自先前讨论过的可扩展集合字面量,但在该语法如何与解构和匹配交互方面增加了大量工作。
动机示例
便捷地构造类对象和类数组的数据结构:
const map = Map!{1: 2, three: 4, [[5]]: 6, 1: 'again'}
// Map { 1 => 'again', 'three' => 4, [5] => 6 }
const set = Set![1,2,3,2]
// Set [1, 2, 3]
const opt = Some!1
// Some { value: 1 }
解构赋值/绑定:
const Map!{1: x, three: y} = map
x // 2
y // 4
const Set![x,y] = set
x // 1
y // 2
const Some!x = opt
x // 1
匹配语句兼容性:
match (input) {
when Map!{1: x, 2: y} ~> ...
when /(?<year>\d{4})-(?<month>\d{2})/u!{groups: {year, month}} ~> {
console.log(`The year is ${year}, and the month is ${month}`)
},
when Some!1 ~> `option succeeded with an internal value of 1`
when Some!x ~> `option succeeded with a non-1 value of ${x}`,
when None!{} ~> `option failed`
}
整体构想
相关的活跃提案
构造字面量
对于构造来说,字面量是 Constructor.from() 函数之上的一层轻量语法糖。当遇到字面量构造表达式时,左侧会被求值以取其值,右侧会被转换为迭代器或原子值。传递给 .from() 的值类型取决于使用三种语法中的哪一种:
// Tagged Object Literals
Map!{foo: 1, 'foo': 1, [Symbol('bar')]: 2, 3: 4, [{}]: 5}
=== Map.from({[Symbol.iterator]: function* () {
// IdentifierName interpreted as string
yield ['foo', 1]
// StringLiteral PropertyNames
yield ['foo', 1]
// Symbols preserved
yield [Symbol('bar'), 2]
// Numeric literals do not get ToString
yield [3, 4]
// Other kinds of computer property also do not get ToString
yield [{}, 5]
}}
// Tagged Array Literals
Set![1,2,3]
=== Set.from({[Symbol.iterator]: function* () {
// Nothing special here, except the argument is not an Array
yield 1; yield 2; yield 3
}})
// Tagged Value Literals
Some!1
=== Some.from(1)
优点
对于对象,优点更为明显:不转换为 ToString,对无效的键/值语法进行解析时早期错误,以及更适合键/值类型的语法,而不必编写嵌套数组。
对于数组和值,这一端的优点较小,主要基于便利性,但它确实有助于缓解一个常见的 new 陷阱:
class Bar { constructor (iter) { this.val = [...iter] } }
function foo () { return {Bar} }
foo.Bar = Bar
new foo().Bar()
// TypeError: Class constructor Bar cannot be invoked without `new`
new foo.Bar()
// => Bar {}
尽管这种行为是一致的,但它偶尔确实会绊倒人们。字面量语法有助于缓解这一点,尤其是在数据结构密集的代码中:
foo().Bar![1,2,3]
// Bar { val: [1,2,3] }
foo.Bar![1,2,3]
// Bar { val: [1,2,3] }
但如前所述,将标签字面量语法扩展到数组和单个值的主要好处是它与解构的对应关系……
解构字面量
当用户学会用某种语法构造后,就更容易教会他们如何用它进行解构。
常见的 .from() 方法机制是构造字面量工作的基础,而解构则通过 Symbol.valueOf 构造器方法使用标准迭代器协议。如果 Symbol.valueOf 不存在,则尝试使用 .valueOf()。如果使用数组或对象解构语法且没有 RestProperty/RestElement,.valueOf() 将收到一个从对象请求的键数组。如果请求的是原子解构,则 valueOf() 的第二个参数为 undefined。根据这些键过滤条目是可选的。
valueOf 方法应返回一个迭代器
const Map!{1: x, y} = Map!{1: 'x', y: 'y'}
===
let x, y
for (let entry of Map.valueOf(Map!{1: 'x', y: 'y'}, [1, 'y'])) {
match (entry) {
when [1, _x] ~> {
x = _x
}
when ['y', _y] ~> {
y = _y
}
}
}
const Set![a, b, c] = Set![1,2,3,4]
===
let [a,b,c] = Array.from(Set.valueOf(Set![1,2,3,4], [0,1,2]))
class Some { constructor (val) { this._val = val } }
Some.valueOf = (some) => some._val
const Some!x = Some!1
===
let x = Some.valueOf(new Some(1))
// x === 1
当使用解构序列时,迭代器将按原样用于匹配和填充解构数组或对象中的条目。
如果 valueOf 返回 undefined,则匹配被认为失败,不会匹配任何值。当与 match 一起使用时,这会导致匹配子句失败。