Stage 2 → Stage 3
提案从 Stage 2 进入到 Stage 3 有以下几个门槛:
- 撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见;
- ECMAScript 编辑签署了同意意见。
Array.fromAsync
提案链接:proposal-array-from-async[1]
在 JavaScript 中,Array.from
方法用于从一个类数组或可迭代对象(Iterable,即部署了 [Symbol.iterator]
接口的对象)创建一个新的数组。
在从可迭代对象创建数组时,其实际上等价于以下的代码:
const arr = []; for (const v of iterable) { arr.push(v); } // 等价于 const arr = Array.from(iterable);
然而还有一种常见的场景是,从异步迭代对象(Async Iteratable,即部署了 [Symbol.asyncIterator]
接口的对象)类型创建数组,此时常见的方式是使用 for await of
语法:
const arr = []; for await (const v of asyncIterable) { arr.push(v); }
而为了在语言层面支持这一能力,此提案引入了 Array.fromAsync
方法,来从异步迭代对象生成数组。
此方法会从迭代对象(包括 Iteratable 与 Async Iteratable)立即生成一个 Promise ,其成功 resolve 将返回一个数组:
function sleep() { return new Promise((res, rej) => { setTimeout(res, 1000); }); } Array.fromAsync = async (source) => { const arr = []; for await (const entry of source) { arr.push(entry); } return arr; }; const arr = [1, 2, 3, 4]; // 异步迭代 async function* asyncGen() { for (const i of arr) { await sleep(); yield i; } } // 同步迭代 function* syncGen() { for (const i of arr) { yield i; } } (async () => { console.log(Array.fromAsync(syncGen())); console.log(Array.fromAsync(asyncGen())); })();
以上调用均会立刻输出两个 Promise :
Promise { <pending> } Promise { <pending> }
而如果我们 await 这两个 Promise,那么对 asyncGen()
的调用将会创建一个异步迭代器(Async Iterator),然后依次等待每一个内部的 Promise resolve,再将其值添加进结果数组,最后返回这个数组:
// 来自于 syncGen() 的调用会立刻返回 [ 1, 2, 3, 4 ] // 来自于 asyncGen() 的调用等待 4s 后才打印 [ 1, 2, 3, 4 ]
而如果同步可迭代对象也返回了 Promise ,那么 fromAsync 同样会顺序地依次等待每一个 Promise resolve:
// 异步迭代 async function* asyncGen() { for (const i of arr) { await sleep(); yield i; } } // 生成 Promise 的同步迭代 function* syncGenWithPromise() { for (const i of arr) { yield sleep().then(() => i); } } (async () => { console.log(await Array.fromAsync(syncGenWithPromise())); console.log(await Array.fromAsync(asyncGen())); })();
// 等待 4s 后打印 [ 1, 2, 3, 4 ] // 再等待 4s 后打印 [ 1, 2, 3, 4 ]
但如果使用 Array.from
方法来迭代返回 Promise 的同步可迭代对象,实际上其中的各个 Promise 会是彼此独立的,即无需等待上一个 Promise settle :
// 生成 Promise 的同步迭代 function* syncGenWithPromise() { for (const i of arr) { // 越往后,越快 resolve yield sleep(2000 - i * 100).then(() => { console.log(`${i} resolved`); return i; }); } } (async () => { console.log(await Promise.all(Array.from(syncGenWithPromise()))); })();
4 resolved 3 resolved 2 resolved 1 resolved [ 1, 2, 3, 4 ]
最后,你可能会想到与 Array.fromAsync
有些相似的 Promise.all
方法,但Promise.all
将并行地等待内部所有的 Promise resolve,然后一次性返回所有结果:
// 生成 Promise 的同步迭代 function* syncGenWithPromise() { for (const i of arr) { yield sleep().then(() => i); } } (async () => { console.log(await Promise.all(Array.from(syncGenWithPromise()))); })();
// 只需等待 1s [ 1, 2, 3, 4 ]
Stage 1 → Stage 2
从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。
Well-formed Unicode strings
提案链接:proposal-is-usv-string[2]
ECMAScript 字符串都是 UTF-16 编码的字符串。在 Web API 中,我们可以发现有些 API (如 URL、URLSearchParams 等等系列 API)都声明了需要 USVString 作为参数。什么是 USVString?USV 代表 Unicode Scalar Value,即 Unicode 标量值。根据 Unicode 定义,Unicode 的码位(Code Point)可以分成几个类别,分别是图形码(Graphic),格式码(Format),控制码(Control),私有码(Private-Use),代理码(Surrogate),非字符码(Noncharacter),与保留码(Reserved)。而其中的代理码又分成了高位代理码与低位代码码,只有当一个高位代码码与一个低位代理码组合成一个代理码对,才是一个合法的 Unicode 字符。
目前,JavaScript 字符串并不限制这个字符串的值是否是合法的 Unicode 值,比如我们可以编码一个字符串只有高位代理码,而没有低位代理码等等。而如严格的 Web URL API 定义必须要求参数字符串是合法的 Unicode 标量值,因此我们需要有方法能够去区分一个字符串是否是合法的 Unicode 标量值。
这个提案提出为 ECMAScript 引入新的内置方法 String.prototype.isWellFormed
, 用于检查这个字符串是否是一个合法的 Unicode 标量值:
'\ud800'.isWellFormed(); // => false '\ud800\udc00'.isWellFormed(); // => true
另外此提案也提供了 String.prototype.toWellFormed
方法,来将普通字符串转换到一个格式正确的 USV 字符串。类似的,NodeJs 中也提供了 util.toUSVString
这样的方法来实现此功能。
Stage 0 → Stage 1
从 Stage 0 进入到 Stage 1 有以下门槛:
- 找到一个 TC39 成员作为 champion 负责这个提案的演进;
- 明确提案需要解决的问题与需求和大致的解决方案;
- 有问题、解决方案的例子;
- 对 API 形式、关键算法、语义、实现风险等有讨论、分析。Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。
Extractor Objects
提案链接:proposal-extractors[3]
提取器语法是 Scala 中用于快速提取实例属性的语法糖,在 Scala 中,我们可以通过 apply 方法定义类的实例化方法,通过 unapply 方法(即提取器)反转这个过程——从实例获得实例化时的入参。
如以下的 Scala 代码:
object UserId: // 生成一个 UserId 字符串 def apply(name: String) = s"userId--$name" // 从 UserId 字符串获得生成时的 name def unapply(userId: String): Option[String] = val stringArray: Array[String] = userId.split("--") if stringArray.tail.nonEmpty then Some(stringArray.tail) else None // 定义了 apply 方法后,才能通过这种方式进行实例化 val userId1 = UserId("小明") // userId-小明 // 通过提取器获得其 name val UserId(name1) = userId1 println(name1) // 小明 // 也可以直接应用于字符串,在无法提取时会返回一个 None 类型 val UserId(name2) = "userId-大明" println(name2) // 大明
而其提案即旨在为 ECMAScript 引入提取器语法,包括数组提取器与对象提取器两种使用形式,如以下 JavaScript 代码:
class Foo { constructor(foo, bar, baz) { this.foo = foo; this.bar = bar; this.baz = baz; } } const foo = new Foo(); // 提取 foo bar const Foo(arg1, arg2) = foo; // 提取 foo baz const Foo{foo, baz} = foo;
以上代码使用的是绑定模式语法(Binding Pattern),你也可以使用分配模式(Assignment Pattern),有点类似函数声明与函数表达式的区别:
Foo(arg1, arg2) = foo; Foo{foo, baz} = foo;
而提取器语法也可以和 Pattern Matching[4] 提案协作,我们还是先看看 Scala 中这两种语法的组合:
userId1 match case UserId(name1) => println(name1) // 小明 case _ => println("提取用户 ID 失败")
而在 ECMAScript 中,结合提取器语法和模式匹配,我们能够实现在解构赋值的同时进行校验或是二次处理,如以下的例子:
// 确保值为 Instance 类型,即一个不包含时区信息的精确时间 const InstantExtractor = { // 通过部署 Symbol.matcher 接口实现自定义匹配 [Symbol.matcher]: value => value instanceof Temporal.Instant ? { matched: true, value: [value] } : value instanceof Date ? { matched: true, value: [Temporal.Instant.fromEpochMilliseconds(value.getTime())] } : typeof value === "string" ? { matched: true, value: [Temporal.Instant.from(value)] } : { matched: false }; } }; class Book { constructor({ title, // 在解构出这个值的同时,对其进行格式转换 createdAt: InstantExtractor(createdAt) = Temporal.Now.instant(), modifiedAt: InstantExtractor(modifiedAt) = createdAt }) { this.title = title; this.createdAt = createdAt; this.modifiedAt = modifiedAt; } }
而这也是解构赋值自 ES6 加入 JavaScript 以来一个呼声强烈的功能——解构时的额外处理逻辑。通过解构赋值结合提取器,我们能够将值的读取、校验与处理合并在一处,确保在后续消费时可以直接使用。
总结
由贺师俊牵头,阿里巴巴前端标准化小组等多方参与组建的 JavaScript 中文兴趣小组(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上开放讨论各种 ECMAScript 的问题,非常欢迎有兴趣的同学参与讨论:esdiscuss。
参考资料
[1]
proposal-array-from-async: https://github.com/tc39/proposal-array-from-async
[2]
proposal-is-usv-string: https://github.com/tc39/proposal-is-usv-string
[3]
proposal-extractors: https://github.com/tc39/proposal-extractors
[4]
Pattern Matching: https://github.com/tc39/proposal-pattern-matching