作者:穹心

审校:昭朗
在本次 TC39 会议中,或许是由于在亚洲时区(东京时间)举办的原因,整体提交的提案数量较少,也仅有三个提案取得了阶段性进展。另外,本次会议中没有提案进入到 Stage 4 阶段。

Stage 2 → Stage 3

提案从 Stage 2 进入到 Stage 3 有以下几个门槛:
  1. 撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见;
  2. 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 将返回一个数组:
functionsleep
() {

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 有以下门槛:
  1. 找到一个 TC39 成员作为 champion 负责这个提案的演进;
  2. 明确提案需要解决的问题与需求和大致的解决方案;
  3. 有问题、解决方案的例子;
  4. 对 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
继续阅读
阅读原文