在本次会议中,Intl.Enumeration 提案成功进入到 Stage 4,距离它在 2020 年 6 月的会议上进入到 Stage 1 已经过去了两年半的时间,其它备受关注的提案如 Explicit Resource Management[1] 与 Set Methods[2] 也成功取得进展,进入到 Stage 3 阶段。
Stage 3 → Stage 4
从 Stage 3 进入到 Stage 4 有以下几个门槛:
- 必须编写与所有提案内容对应的 tc39/test262[3] 测试,用于给各大 JavaScript 引擎和 transpiler 等实现检查与标准的兼容程度,并且 test262 已经合入了提案所需要的测试用例;
- 至少要有两个实现能够兼容上述 Test 262 测试,并发布到正式版本中;
- 发起了将提案内容合入正式标准文本 tc39/ecma262[4] 的 Pull Request,并被 ECMAScript 编辑签署同意意见。
Intl.Enumeration
提案链接:proposal-intl-enumeration[5]
这一提案用于获取当前运行环境下国际化选项的支持值,如 calendar、currency、timeZone 等,其引入了 Intl.supportedValuesOf(key)
方法来返回对应的所有支持值,如获取当前运行环境中所有支持的历法:
console.log(Intl.supportedValuesOf('calendar')); // ['buddhist', 'chinese', 'coptic', 'dangi', ...]
Stage 2 → Stage 3
提案从 Stage 2 进入到 Stage 3 有以下几个门槛:
- 撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见;
- ECMAScript 编辑签署了同意意见。
Set Methods
提案链接:proposal-set-methods[6]
此提案为 JavaScript 中的 Set 结构新增了一批内置方法,主要为集合相关,包括交集、并集、差集、子集等:
- Set.prototype.intersection(other)
- Set.prototype.union(other)
- Set.prototype.difference(other)
- Set.prototype.symmetricDifference(other)
- Set.prototype.isSubsetOf(other)
- Set.prototype.isSupersetOf(other)
- Set.prototype.isDisjointFrom(other)
这些方法的入参均为另一个 Set 类型的数据,或者至少是实现了 .size .keys .has 三个方法的对象。
Well-formed Unicode strings
提案链接:proposal-is-usv-string[7]
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
这样的方法来实现此功能。
你也可以使用其提供的 polyfill string.prototype.iswellformed[8] 和 string.prototype.towellformed[9] 来提前试用。
Explicit Resource Management
提案链接:proposal-explicit-resource-management[10]
此提案旨在为 JavaScript 中引入显式的资源管理能力,通过统一的 using 关键字来标记当前块级作用域内的关键资源。
在此前,JavaScript 中对各种资源的管理方式并不统一,如在 Generator 函数中,通过 return 方法来提供执行清理逻辑的方式:
function * g() { const handle = acquireFileHandle(); try { ... } finally { handle.release(); // 释放资源 } } const obj = g(); try { const r = obj.next(); ... } finally { obj.return(); // 显式调用函数 g 中的 finally 代码块 }
而通过 using 关键字,其可以被简化为如下的方式:
function * g() { using handle = acquireFileHandle(); // 定义与代码块关联的资源 } { using obj = g(); // 显式声明资源 const r = obj.next(); } // 自动调用释放逻辑
除此以外,NodeJs FileHandles 上的 handle.close()
方法,WHATWG Stream Readers 上的 reader.releaseLock()
均可以使用这种方式来简化资源的管理。
实际上,这里的“显式”对应的是此前如 WeakSet 与 WeakMap 这样,会由运行时作为垃圾回收的一部分进行的“隐式”工作。显式资源管理意味着用户主动声明块级作用域内依赖的资源,通过 Symbol.disposable[11] 这样的命令式或 using
这样的声明式,然后在离开作用域时自动地释放这些标记的资源。
Stage 1 → Stage 2
从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。
ArrayBuffer transfer
提案链接:proposal-arraybuffer-transfer[12]
这一提案属于 proposal-resizablearraybuffer[13] 提案的衍生,其引入了 ArrayBuffer.prototype.transfer
方法,来支持对 ArrayBuffer 的所有权转移能力。
在 JavaScript 中,可转移对象指的是拥有可在不同上下文间转移的资源的对象,在转移资源后,原始上下文中的对象将不再指向资源,只有新的上下文持有资源的所有权。这一能力通常用于确保在同一时刻只有一个线程能够访问资源。更常见的一个例子是在 Web Worker 场景下,将可转让对象(比如一个 ArrayBuffer)在主线程与工作线程之间传递,传递方仍然持有原始 ArrayBuffer 对象,但其 byteLength 为0,同时无法再对其进行写入。这一过程无需经过任何拷贝操作,也就意味着在数据量较大时能够有明显的性能提升。
此前我们并不能直接将一个 ArrayBuffer 的资源所有权转移到另一个 ArrayBuffer 对象,以此来避免原始的缓冲区输入被篡改,而只能使用 slice
方法来复制一个 ArrayBuffer 对象,如以下这个例子:
function validateAndWriteSafeButSlow(arrayBuffer) { // 复制一份,避免缓冲区被篡改 const copy = arrayBuffer.slice(); await validate(copy); await fs.writeFile("data.bin", copy); } const data = new Uint8Array([0x01, 0x02, 0x03]); validateAndWrite(data.buffer); setTimeout(() => { // 篡改数据 data[0] = data[1] = data[2] = 0x00; }, 50);
这种方式需要将原本的 ArrayBuffer 中的每个字节进行复制,然后开辟新的缓冲区存放,在数据量较大将导致性能问题。而现在,我们可以使用 transfer
方法来直接转移其所有权,使得其无法被篡改:
function validateAndWriteSafeAndFast(arrayBuffer) { // 转移所有权,并直接移动而非复制数据 const owned = arrayBuffer.transfer(); assert(arrayBuffer.detached); await validate(owned); await fs.writeFile("data.bin", owned); }
这里的 arrayBuffer.detached 属性也来自与此提案,用于作为一种清晰且权威的方式,来检查一个 ArrayBuffer 对象是否已从缓冲区分离。
Stage 0 → Stage 1
从 Stage 0 进入到 Stage 1 有以下门槛:
- 找到一个 TC39 成员作为 champion 负责这个提案的演进;
- 明确提案需要解决的问题与需求和大致的解决方案;
- 有问题、解决方案的例子;
- 对 API 形式、关键算法、语义、实现风险等有讨论、分析。
Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。
Intl MessageResource
提案链接:proposal-intl-message-resource[14]
Intl MessageResource 提案是对 Intl.MessageFormat 提案的进一步补充,用于实现一次性对一组资源消息的翻译能力,这是因为 UI 界面中通常同时会存在一组相关联的消息需要翻译,如对话框等。
对于 Intl.MessageFormat 提案,我们的使用方式是这样的,首先给到 MF2 定义:
morning_greeting = {早上好,{$user}!} new_notifications [$count] = [0] 你现在还没有信息 [one] 你收到新信息了~ [_] 你收到了 {$count} 条新信息,快打开看看吧!
MF2 的全称为 MessageFormat 2.0,是由 message-format-wg 制定的,统一的消息描述规范。它是编程语言无关的,目前实现了 MessageFormat 2.0 的语言主要包括 JavaScript 和 Java。
这里定义了无消息、单条消息、多条消息的几种情况,然后在 JavaScript 中就可以通过创建 Intl .MessageFormat 的实例,来进行消息的解析:
const resource = ... // 即以上的 MF2 定义 const mf1 = new Intl.MessageFormat(resource, ['en']); const msg1 = mf.resolveMessage('new_notifications', { count: 3 }); msg1.toString(); // '你收到了 3 条新信息,快打开看看吧!' const mf2 = new Intl.MessageFormat(resource, ['en']); const msg2 = mf.resolveMessage('morning_greeting', { user: '小明' }); msg2.toString(); // '早上好,小明'
可以看到,对于同一个 MF2 定义,需要创建两个 Intl.MessageFormat 实例来分别进行解析。而 Intl Message Resource 提案为 Intl.MessageFormat 新增了 parseResource 静态方法,使得我们可以一次性完成对所有消息资源的解析:
const resource = ... // 即以上的 MF2 定义 const res = Intl.MessageFormat.parseResource(resource, ['en']); const greeting = res.get('morning_greeting').resolveMessage({ user: '小明' }); greeting.toString(); // '早上好,小明' const notifications = res.get('new_notifications').resolveMessage({ count: 3 }); notifications.toString(); // '你收到了 3 条新信息,快打开看看吧!'
另外,MessageFormatter 系列相关提案将引入的 API 具体格式还未完全确定,最终将取决于 MF2 工作组最终为 MF2 落地的语法。
Intl.era and monthCode
提案链接:proposal-intl-era-monthcode[15]
这一提案属于 ECMAScript 402 中的 Intl 提案,与我们更熟悉的 Temporal 提案不同的是,Temporal 仅对 ISO8601 时间格式与 UTC 时区下的行为做了明确定义,对 ISO8601 以外的时间格式和 UTC 以外的时区,只提供了最基本的定义。而 Intl.era 提案旨在对这些规范细节进行进一步的完善。
这一提案之所以没有被作为 Temporal 提案的一部分,原因在于 Temporal 是 ECMA262 规范(即 ECMAScript)的一部分,其需要在所有支持 ECMAScript 的环境中运行并保持一致性,而 Intl 提案所属的 ECMAScript 402 作为 ECMAScript 的国际化标准,其在运行时可能会受到限制,如仅保留少数语言支持,此提案的行为也可能受到影响。
Mass Proxy Revocation
提案链接:proposal-mass-proxy-revocation[16]
在 JavaScript 中,我们可以通过 Proxy.revocable
方法来创建一个可被撤销的代理对象,其返回值中将包含一个 revoke
方法,调用此方法就将撤销掉一起生成的代理对象,而后续对此代理对象的所有可代理操作都将抛出错误。但这种方式仅适用于使用 revocable
方法创建的代理对象,同时需要为每个对象都调用一次 revoke
方法。
而此提案引入了 createSignal 与 finalizeSignal 方法,来支持一次性对一批 Proxy 对象的撤销,甚至是直接通过 new Proxy
创建的代理对象也能够通过这种方式撤销,其使用方式大致如下:
const s1 = Proxy.createSignal(); const p1 = new Proxy([], {}, { signal: s1 }); const { proxy: p2, revoke } = Proxy.revocable({}, {}, { signal: s1 }); Proxy.finalizeSignal(s1); // p1 与 p2 都将被撤销