Redian新闻
>
ECMAScript 双月报告:Async Context 提案成功进入到 Stage 1

ECMAScript 双月报告:Async Context 提案成功进入到 Stage 1

科技

在本次会议中,Change Array By Copy 提案、 Intl.NumberFormat V3 提案、Symbol as WeakMap Keys 提案成功进入到 Stage 4,分别耗时 22 个、 34 个、以及 32 个月。另外,由阿里巴巴提出的 Async Context 提案也在本次会议中成功进入到 Stage 1。

Stage 3 → Stage 4

当一个提案进入到 Stage 4 时,意味着提案已经可以在多个浏览器、Node.js 上试用,并且这些运行时都已经完成语言合规测试。同时,这个提案将会被吸纳到下一个年度发布的 ECMAScript 版本中,如 ECMAScript 2023 等。

Change Array By Copy

提案链接:proposal-change-array-by-copy[1]

JavaScript 中的数组操作方法中有一部分是会改变原数组的,如 sort、reverse、splice 等,在需要保持原数组不变时,我们往往需要先复制一份原数组。这一提案引入了一系列秉持 Change Array by Copy 理念的方法,它们的功能完全对应于会改变原数组的版本,惟一的区别是,这些方法调用将产生一个新的数组:

  • Array.prototype.toReversed,对应于 Array.prototype.reverse
  • Array.prototype.toSorted,对应于 Array.prototype.sort
  • Array.prototype.toSpliced,对应于 Array.prototype.splice
  • Array.prototype.with,对应于 Array.prototype.copyWithin

同时,TypedArray 以及其子类的原型上也将新增上面除 toSpliced 以外的三个方法。

这些方法实际上最早来自于 Record and Tuple[2] 这个目前处于 Stage2 的提案,其为 JavaScript 引入了不可变的对象(Record)与数组(也称元组,Tuple)结构。在 Tuple 的原型上就不存在数组那样将会修改自身的方法(push,pop等),而是上面列举的 toReversed 这一类方法,你可以查看 Tuple Prototype[3] 了解更多 Tuple 上存在的方法。

为了使数组也能享受一部分“不可变”的特性,同时在未来能更容易处理数组和元组的兼容性,这些方法被抽出成为了一个独立的提案,即 Change Array by Copy 。

这四个方法目前已经拥有对应的 Polyfill 支持,参考 CoreJS[4]ES Shims[5]

Intl.NumberFormat V3

提案链接:proposal-intl-numberformat-v3[6]

Intl.NumberFormat V3 提案为 Intl.NumberFormat 新增了一系列方法,用于简化数字在国际化中的显示内容,如格式化数字的范围、四舍五入、字符串分割等:

const nf = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "CNY",
  maximumFractionDigits: 0,
});

nf.formatRange(3, 5);  // "CNY 3–5"

Symbol as WeakMap Keys

提案链接:proposal-symbols-as-weakmap-keys[7]

这一提案支持了在 WeakMap 中使用 Symbol 类型作为键,而此前 WeakMap 中只允许对象类型作为键。这一特性实际上是为了允许在 Records 与 Tuples 数据类型中引用对象。

Records 与 Tuples 提案为 JavaScript 引入了两个新的数据类型,它们的特性是基于值比较来判断相等性,如对于两个 Tuple 的比较中, #[1, 2,3] === #[1, 2, 3] 是成立的,因为内部的成员值完全一致。然而,这一基于值比较的特性导致了无法在 Record 与 Tuple 中使用基于引用地址比较的对象类型。而如果我们能够在 WeakMap 中使用 Symbol 类型作为键,就可以在 Record 与 Tuple 中使用 Symbol 存放引用,间接地实现对象类型值的存储。

对于 Map 与 WeakMap 的差异,我们知道 Map 类型是通过两个数组来分别存储键和键值的,这两个数组对于其中对象类型键/键值的引用始终存在,从而导致即使已经不存在其它的引用也无法回收处理。因此,WeakMap 持有的引用为弱引用,在对象类型不存在其它引用时,能正确地执行能垃圾回收。

正是因为弱引用的要求,WeakMap 的键是无法枚举的,且需要是唯一的值。对象类型很好地满足了这个要求,两个完全一样的对象类型实际上也拥有着不同的引用。你肯定会想到 Symbol 也具有这种“唯一”的特性,这也是为何此提案想要允许 Symbol 作为 WeakMap 的键。

同时,Symbol 也能够起到比对象类型更好的标识作用:

const weakMap = new WeakMap();

const key = Symbol('ref for data');
const data = { };

weakMap.set(key, data);

在 ECMAScript 中,Symbol 也有多种类型:

  1. Unique Symbol,比如我们通过 Symbol(description) 创建的 Symbol 就是全局唯一的值;
  2. Well-known Symbol,比如 Symbol.iterator,是预知的、在语言特性中广泛使用的 Symbol 值;
  3. Registered Symbol,比如我们通过 Symbol.for(description) 注册的 Symbol,同样也是全局唯一的值,但是每次获取的都是同一个 Symbol 值。

在提案的方案中,Unique Symbol 与 Well-known Symbol 都是可以作为 WeakMap Key 的,但是 Registered Symbol 不能作为 WeakMap Key。这是因为 Registered Symbol 实际上是无法观测到垃圾回收的,而不能观测到垃圾回收的值类型作为 WeakMap Key 没有实际意义。而 Well-known Symbol 虽然也是实际意义上无法被垃圾回收,但是这些 Symbol 是一个确定的列表,无法动态添加删除,所以也被允许作为 WeakMap Key。

细心的同学们可能已经发现了,目前 ECMAScript 中我们还没有办法判断一个 Symbol 的类型,这对于我们实际使用这个提案的特性时会造成“有些 Symbol 能作为 key 而有些不能”的问题。为了解决这个问题,同样在这次进入 Stage 2 的提案 Symbol Predicates 提供了解决方案。

Stage 2 → Stage 3

当一个提案进入 Stage 3 后,意味着浏览器、Node.js 等将会开始实现提案特性。在这个阶段的提案只有在发生重大问题时才会进行修改。

ArrayBuffer transfer

提案链接:proposal-arraybuffer-transfer[8]

这一提案属于 proposal-resizablearraybuffer[9] 提案的衍生,其引入了 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 1 → Stage 2

当一个提案进入 Stage 2,意味着已经完成了特性细节的草稿设计,可能被用于实验性验证等早期实现。

Intl era and monthCode

提案链接:proposal-intl-era-monthcode[10]

这一提案属于 ECMAScript 402 中的 Intl 提案,与我们更熟悉的 Temporal 提案不同的是,Temporal 仅对 ISO8601 时间格式与 UTC 时区下的行为做了明确定义,对 ISO8601 以外的时间格式和 UTC 以外的时区,只提供了最基本的定义。而 Intl.era 提案旨在对这些规范细节进行进一步的完善。

这一提案之所以没有被作为 Temporal 提案的一部分,原因在于 Temporal 是 ECMA262 规范(即 ECMAScript)的一部分,其需要在所有支持 ECMAScript 的环境中运行并保持一致性,而 Intl 提案所属的 ECMAScript 402 作为 ECMAScript 的国际化标准,其在运行时可能会受到限制,如仅保留少数语言支持,此提案的行为也可能受到影响。

Symbol predicates

提案链接:proposal-symbol-predicates[11]

此提案为 Symbol 顶级对象引入了两个新的方法:Symbol.isRegisteredSymbol.isWellKnown,它们分别用于判断一个 Symbol 值是否已被注册,以及是否是 ECMA262 & ECMA402 规范中内置的 Symbol 类型(如 Symbol.iteratorSymbol.toPrimitive 等)。

这个提案主要是为了解决在上面的 Symbol as WeakMap Key 提案中,仅有 Unique Symbol(直接通过 Symbol() 创建的 Symbol 值) 与 Well-known Symbol(内置 Symbol) 可以作为 WeakMap 结构 key 的问题。通过此提案提供的 Symbol.isRegistered 方法,我们能够避免错误地使用了 Registered Symbol 作为 WeakMap Key 。

你也可以使用这两个方法来判断一个 Symbol 类型是否是独一无二的:

const isUniqueSymbol = sym => typeof sym === "symbol" && !(Symbol.isRegistered(sym) || Symbol.isWellKnown(sym));

isUniqueSymbol(Symbol()); // true 一个新的 Symbol 类型
isUniqueSymbol(Symbol.for("foo")); // false Symbol.for 方法会将此 Symbol 注册到全局
isUniqueSymbol(Symbol.asyncIterator); // false 内置 Symbol 类型
isUniqueSymbol({}); // false 非 Symbol 类型

目前这两个方法还没有正式的 polyfill,但你可以通过 is-registered-symbol[12]is-well-known-symbol[13] 来提前尝试。

Stage 0 → Stage 1

所有 ECMAScript 提案都需要论证所提特性的价值、解决方案可行性。当提案进入 Stage 1 意味着提案的价值与设计方案正式被 TC39 接收,并开始标准化流程。

Async Context

提案链接:proposal-async-context[14]

在 JavaScript 异步上下文追踪对于浏览器、Node.js等运行时,应用框架、应用监控程序等来说一直是一个难以攻克的难题。对于同步执行的任务,React 通过 React Context[15] 追踪任务上下文,不过在引入异步回调、Promise、async/await 时即会失效。而 Angular 则选择了通过 Zone.js 实现一定程度上的异步上下文追踪能力。

近年,Node.js 通过 AsyncLocalStorage 等尝试,提供了基础的异步任务追踪的能力。但是对于普通开发者来说,缺少标准化方案还是难以让这个特性在如浏览器、Deno 等运行时上获得支持。

这个提案提议了一个能够将任意 JavaScript 值通过逻辑连接的同步、异步操作,传播到逻辑连接的异步操作的执行上下文的存储 AsyncContext

class AsyncContext<T> {
  // 快照当前执行上下文中所有 AsyncContext 实例的值,并返回一个函数。
  // 当这个函数执行时,会将 AsyncContext 状态快照恢复为执行上下文的全局状态。
  static wrap<R>(fn: (...args: any[]) => R): (...args: any[]) => R;

  // 立刻执行 fn,并在 fn 执行期间将 value 设置为当前 
  // AsyncContext 实例的值。这个值会在 fn 过程中发起的异步操作中被
  // 快照(相当于 wrap)。
  run<R>(value: T, fn: () => R): R;

  // 获取当前 AsyncContext 实例的值。
  get(): T;
}

通过 AsyncContext 即可实现在异步逻辑调用链上获得类似于 ReactContext 等同步调用上下文访问的能力:

// Framework listener
doc.addEventListener('click', () => {
  timer.run(Date.now(), async () => {
    // User code
    const f = await fetch(dataUrl);
    // 不需要额外传递时间戳参数
    patch(dom, await f.json());
  });
});
// Some framework code
const timer = new AsyncContext();
function patch(dom, data) {
  // 异步任务链中间节点不需要关心额外的参数传递
  doLotsOfWork(dom, data, update);
}
function update(dom, html) {
  // 通过 AsyncContext 获取异步任务链的开始时间
  log(Date.now() - timer.get());
  dom.innerHTML = html;
}

如果你想了解更多 Async Context 的介绍,请参考 ECMAScript Async Context 提案介绍[16]

防止原型链污染 Symbol.proto

提案链接:proposal-symbol-proto[17]

JavaScript 中的原型污染是一个具有极大威胁的漏洞,它能够被用于覆盖或注入 JavaScript 中对象原型的属性。此前 Lodash 中就出现过原型污染漏洞 CVE-2019-10744,其中的 defaultsDeep 方法能够被用于篡改 Object.prototype 属性:

import { defaultsDeep } from "lodash";

defaultsDeep({}, JSON.parse('{"__proto__": {"polluted": true}}'));

{}.polluted; // true

现在社区中并没有一种很好地应对原型污染的方式,如 Object.sealObject.freeze 这样的方法会使得许多依赖向原型添加方法的 polyfill 实现无法工作。而此提案引入了「安全模式」以及「专用于动态更改原型的 symbol 类型」两种互相搭配的方式,来“堵住”原型污染这个漏洞。

首先,在安全模式下,不允许使用字符串属性名来动态访问原型,即删除 __proto__prototype(或 constructor )属性,只允许代码中通过 getPrototypeOf 显式获取原型引用。安全模式能够防止如 defaultsDeep 或者 deepMerge 这样的函数无意间修改了原型属性,又不会导致 polyfill 这样需要修改原型的代码无法工作。

其次,此提案还希望引入了内置的 Symbol 类型,用于在安全模式下修改原型。目前提案中提出了 Symbol.protoSymbol.constructor 两个备选方案:

  • 使用 Symbol.proto 能够在安全模式下访问 prototype 上的属性
  • 使用 Symbol.constructor 能够在安全模式下访问构造函数(类似于 Object.getConstructorOf

由于目前处于 Stage 1 阶段,此提案在后续的演进中可能会再次发生较大的变化。

总结

由贺师俊牵头,阿里巴巴前端标准化小组等多方参与组建的 JavaScript 中文兴趣小组(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上开放讨论各种 ECMAScript 的问题,非常欢迎有兴趣的同学参与讨论:https://github.com/JSCIG/es-discuss/discussions。

参考资料

[1]

proposal-change-array-by-copy: https://github.com/tc39/proposal-change-array-by-copy

[2]

Record and Tuple: https://github.com/tc39/proposal-record-tuple

[3]

Tuple Prototype: https://github.com/tc39/proposal-record-tuple/blob/main/NS-Proto-Appendix.md#tuple-prototype

[4]

CoreJS: https://github.com/zloirock/core-js#change-array-by-copy

[5]

ES Shims: https://github.com/es-shims

[6]

proposal-intl-numberformat-v3: https://github.com/tc39/proposal-intl-numberformat-v3

[7]

proposal-symbols-as-weakmap-keys: https://github.com/tc39/proposal-symbols-as-weakmap-keys

[8]

proposal-arraybuffer-transfer: https://github.com/tc39/proposal-arraybuffer-transfer

[9]

proposal-resizablearraybuffer: https://github.com/tc39/proposal-resizablearraybuffer

[10]

proposal-intl-era-monthcode: https://github.com/tc39/proposal-intl-era-monthcode

[11]

proposal-symbol-predicates: https://github.com/tc39/proposal-symbol-predicates

[12]

is-registered-symbol: https://github.com/inspect-js/is-registered-symbol

[13]

is-well-known-symbol: https://github.com/inspect-js/is-well-known-symbol

[14]

proposal-async-context: https://github.com/tc39/proposal-async-context

[15]

React Context: https://reactjs.org/docs/context.html

[16]

ECMAScript Async Context 提案介绍: https://aliyuque.antfin.com/esnext/blog/ypqoah7s7m7ikb9s

[17]

proposal-symbol-proto: https://github.com/tc39/proposal-symbol-proto



微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
Leadership Next Gen is open | Paid fellowship for studentsTired of Cafeterias, Students Go for Lunch at Construction Sites2023 內心成熟的人,看誰都顺眼!把Transformer当通用计算机用,还能执行in-context learning算法,这项研究脑洞大开重访西班牙(2)-大西洋边的名城In-context learning综述篇量子论对狭义相对论的致命的依赖China Proposes To Regulate AI-Generated Content什么是In-Context Learning(上下文学习)?【首发】Syneron Tech呈元科技完成数千万美元融资,加速打造AI+合成肽药物研发平台难诉相思,送谢莉斯老师回国, 十年签证可办了Are Audiences Still Interested in ‘Three Body’ Adaptations?2023年冲出重围,成功进入梦校的孩子长啥样?Synopsys宣布裁员In-Context Learning玩法大全精选UI/UX岗位 | Bose、Synopsys、Myriad Genetics公司发布新岗位!如何避开申请与备考道路上的那些陷阱,成功进入顶级美高进入中国第 23 年,InterSystems 宣布新战略:互联互通构筑医疗数字化转型的新范式第三届 冇(Mǎo)国际青年影像周 开始征片啦!“TypeScript不值得!”前端框架Svelte作者宣布重构代码,反向迁移到JavaScript引争议宇宙人(1226期)快舟一号甲火箭成功发射;马斯克:SpaceX今年将把全球80%的有效载荷送入轨道;白兔-R成功进入环月轨道被GPT带飞的In-Context Learning发展现状如何?这篇综述梳理明白了Crackdown Continues on Illegal ‘Competitions’ Targeting Students【载歌在谷·云集】 秋冬季双月报(2022年12-2023年1月)2022 JavaScript调查:TypeScript持续主导,Vite和Tauri大受欢迎Amid AI Content Boom in China, Douyin Vows Greater Scrutiny国际青少年创新创业大赛! International Youth Innovation&Entrepreneur ContestAsus TUF Gaming VG27AQ HDR G-SYNC Compatible Gaming Monitor当临床医生与网络观点不一致怎办?liver fibrosis stage 2脑洞大开!把Transformer当通用计算机用,还能执行in-context learning算法ECMAScript Async Context 提案介绍美股IPO|Psyence 全资子公司生命科学生物技术公司 Psyence Biomed Corp. 与SPAC合并上市Conagen和Natáur达成合作,生产可持续天然牛磺酸[干货] systemic 和 systematic 有何区别?FastTrack Universität 2023莱比锡大学公立语言项目招生简章
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。