ECMAScript 双月报告:Realms 提案大改仍没能进入 Stage3

简介: 今年因为疫情原因,TC39 的会议频率大为提升,而每一次的会议内容也分散了许多。但是关于提案的讨论热度并不会因为疫情降低,比如这次会议中备受关注的 Realms 提案经过了大改:Realms 在新的提案方案里 Realm 之间无法直接交换除了原始 JavaScript 值(number, string, bigint, symbol 等)和 Callable 以外的 JavaScript 值。
来源: Alibaba F2E公众号
作者:吴成忠(昭朗)

今年因为疫情原因,TC39 的会议频率大为提升,而每一次的会议内容也分散了许多。但是关于提案的讨论热度并不会因为疫情降低,比如这次会议中备受关注的 Realms 提案经过了大改:Realms 在新的提案方案里 Realm 之间无法直接交换除了原始 JavaScript 值(number, string, bigint, symbol 等)和 Callable 以外的 JavaScript 值。其中,Realm 中透出的函数会以一个 Callable 的 Wrapper 对象返回,这个 Wrapper 对象上不能访问到原函数对象的属性,只能用来调用这个对应的函数(参数也只能是原始 JavaScript 值)。当然,基于目前的 API 我们可以通过建立对象桥接来共享 JSON 对象数据,如 https://github.com/caridy/irealm 。不过这方法不能支持交换 ArrayBuffer/TypedArray 对象。另外提案明确增加了 Host Globals Hook 编辑意见,Host 比如浏览器可以向 Realms 的 globalThis 中注入 Host 特有的 API 对象,比如 TextEncoder 等等,这导致了不同的 Host 如 Node.js 和浏览器上的 Realm 可能会有不同的 Global API,让 Realm 不再是纯净的 JavaScript 执行环境。遗憾的是,Realms 暂时没能取得进入 Stage 3 的共识。

TC39 2021年5月会议提案进度汇总, From:阿里巴巴前端标准化组

Stage 3 -> Stage 4

从 Stage 3 进入到 Stage 4 有以下几个门槛:

  1. 必须编写与所有提案内容对应的 tc39/test262 测试,用于给各大 JavaScript 引擎和 transpiler 等实现检查与标准的兼容程度,并且 test262 已经合入了提案所需要的测试用例;
  2. 至少要有两个实现能够兼容上述 Test 262 测试,并发布到正式版本中;
  3. 发起了将提案内容合入正式标准文本 tc39/ecma262 的 Pull Request,并被 ECMAScript 编辑签署同意意见。

RegExp Match Indices

提案链接: https://github.com/tc39/proposal-regexp-match-indices

当前,ECMAScript 中的 RegExp.prototype.exec方法的返回值已经提供了对于匹配的捕获组(Capture Group)文本与对应的捕获组在正则表达式中的索引。但是,有些场景下我们不仅仅只是希望匹配文本,更需要获得被匹配的文本在输出文本中的起始位置与结束位置,比如我们常用的 VSCode 等开发环境提供语法高亮就需要这些信息。因此,提案期望向 RegExp.prototype.exec 返回的数组对象上,新增 indices 属性用来描述这些位置信息。

// 创建一个启用了索引功能的正则表达式对象
const re1 = /a+(?<Z>z)?/d;

// 索引值是相对于输入字符串,而不是被匹配的字符串
const s1 = "xaaaz";
const m1 = re1.exec(s1);
m1.indices[0][0] === 1;
m1.indices[0][1] === 5;
s1.slice(...m1.indices[0]) === "aaaz";

m1.indices[1][0] === 4;
m1.indices[1][1] === 5;
s1.slice(...m1.indices[1]) === "z";

m1.indices.groups["Z"][0] === 4;
m1.indices.groups["Z"][1] === 5;
s1.slice(...m1.indices.groups["Z"]) === "z";

// 捕获组没有命中时值会是 undefined
const m2 = re1.exec("xaaay");
m2.indices[1] === undefined;
m2.indices.groups["Z"] === undefined;

我们已经可以在 Chrome Canary 91,Firefox Nightly 88 等环境使用这个特性。

Top-level await

提案链接: https://github.com/tc39/proposal-top-level-await

从 ECMAScript 引入 Promise 与 Async Function 开始,await 操作符可以说是贯穿了现代 JavaScript 项目的大大小小操作。但是遗憾的是,await 操作符必须在 Async Function 中使用,这也意味着我们不能在一个脚本、模块的顶层逻辑中使用 await

await foo(); // ❌ 不可以,不在 Async Function 内
async function bar() {
  await foo(); // ✅ 可以
}

而很多时候,我们会希望在模块被导入的时候立刻执行一些代码逻辑,此时如果我们想要调用一些异步方法,就必须通过如 IIAFE(立刻调用的 Async Function 表达式):

import { setTimeout } from 'timers/promises';
let value;
export { value };
// 就地创建一个 Async Function 并立即执行
(async () => {
  await setTimeout(1000);
  value = 'foobar';
  throw new Error('boom'); // 没人处理这个异常!
})();

但是这样写很麻烦,问题更大的是导入这个模块的地方无法捕获这个异步操作中抛出的错误,并且导入的地方也无法知晓这个 IIAFE 什么时候执行结束。

此时,更为健壮的做法可能是将这个 IIAFE 的返回 Promise 值作为模块导出,让使用的地方通过 Promise 操作来保证异常处理、逻辑顺序执行。

// a.mjs
import { setTimeout } from 'timers/promises';
let value;
export { value };
// 就地创建一个 Async Function 并立即执行
export default (async () => {
  await setTimeout(1000);
  value = 'foobar';
})();
// b.mjs
import promise, { value } from "./a.mjs";
export function outputValue() { return value; } // ❌ 不安全,所有使用相关值的地方都需要修改函数返回值签名
promise.then(() => {
  // ✅ 可以
  console.log(outputValue());
});

但是这方法显而易见的是所有使用这个模块的地方都需要被传染成 async/await 的方式,并且所有使用这个模块导出值的地方都需要正确的等待 IIAFE 的 Promise 完成。

Top-level await 提案即是期望替代以上几个策略的方案,通过 Top-level await 我们可以在模块内屏蔽更多细节,而使用的地方也可以更加简单:

import { setTimeout } from 'timers/promises';
let value;
export { value };
// 直接 await 异步操作
await setTimeout(1000);
value = 'foobar';
// b.mjs
import { value } from "./a.mjs";
export function outputValue() { return value; } // ✅ 可以
// ✅ 可以
console.log(outputValue());

提案已经可以在 Chrome 89,Node.js 14.8.0 中使用。值得注意的是只有 ECMAScript Module 中可以使用 Top-level await,也就是说我们不能在如 Node.js 的 CommonJS 模块、HTML中非type="module" 的 <script> 标签等中使用 Top-level await。

另外值得注意的是,虽然功能上 Top-level await 看起来像是 IIAFE 的替代品,但是他们的含义并不完全相同。在 Top-level await 中,依赖模块中使用的 Top-level await 会阻塞 import 这个依赖的模块代码的执行。而在 IIAFE 中,await 并不会阻塞当前模块的执行。这即是我们期望的 Top-level await 的优点,却非常容易造成如浏览器页面白屏、卡顿等问题,这是使用 Top-level await 时非常需要注意的问题。

Stage 2 -> Stage 3

提案从 Stage 2 进入到 Stage 3 有以下几个门槛:

  1. 撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见;
  2. ECMAScript 编辑签署了同意意见。

Accessible Object.prototype.hasOwnProperty

提案链接: https://github.com/tc39/proposal-accessible-object-hasownproperty

其实现在我们就可以通过 Object.prototype.hasOwnProperty 来使用提案所包含的特性。但是直接通过对象自身的 hasOwnProperty 来使用 obj.hasOwnProperty('foo') 是不安全的,因为这个 obj 可能覆盖了 hasOwnProperty 的定义,MDN 上也对这种使用方式进行了警告。

JavaScript 并没有保护 hasOwnProperty 这个属性名,因此,当某个对象可能自有一个占用该属性名的属性时,就需要使用外部的 hasOwnProperty 获得正确的结果...
Object.create(null).hasOwnProperty("foo")
// Uncaught TypeError: Object.create(...).hasOwnProperty is not a function
let object = {
  hasOwnProperty() {
    throw new Error("gotcha!")
  }
}
object.hasOwnProperty("foo")
// Uncaught Error: gotcha!

所以一个正确的方式就得写成这样繁琐的方式:

let hasOwnProperty = Object.prototype.hasOwnProperty
if (hasOwnProperty.call(object, "foo")) {
  console.log("has property foo")
}

而提案期望在 Object 上增加一个 hasOwn 方法,便于大部分场景使用:

let object = { foo: false }
Object.hasOwn(object, "foo") // true
let object2 = Object.create({ foo: true })
Object.hasOwn(object2, "foo") // false
let object3 = Object.create(null)
Object.hasOwn(object3, "foo") // false

Resizable and growable ArrayBuffers

提案链接: https://github.com/tc39/proposal-resizablearraybuffer

这个提案可以给 Streaming、WebAssembly 等场景提供一个更加方便、高效的内存扩展方式。目前调整一个 ArrayBuffer 的大小需要复制内容,但是因为复制非常慢,而且可能导致内存空间碎片化,实际实践中限制非常多。

提案给 ArrayBuffer 与 SharedArrayBuffer 的构造函数分别增加了一个参数,可以设置 ArrayBuffer 与 SharedArrayBuffer 最大可以增长的大小 maximumByteLength

设置了 maximumByteLength 的 ArrayBuffer 是一个内部存储区域可以拆卸的 ArrayBuffer。提案设计上希望这些 ArrayBuffer 可以原地调整大小,但是对于 JavaScript 代码来说,实际 JavaScript 引擎有没有对这个大小调整是否是原地的是无法观测到的(如在 JavaScript 中判断内部存储区域实际内存地址是否改变等)。

let rab = new ArrayBuffer(1024, { maximumByteLength: 1024 ** 2 });
assert(rab.byteLength === 1024);
assert(rab.maximumByteLength === 1024 ** 2);
assert(rab.resizable);
rab.resize(rab.byteLength * 2);
assert(rab.byteLength === 1024 * 2);
// Transfer the first 1024 bytes.
let ab = rab.transfer(1024);
// rab is now detached
assert(rab.byteLength === 0);
assert(rab.maximumByteLength === 0);
// The contents are moved to ab.
assert(!ab.resizable);
assert(ab.byteLength === 1024);

SharedArrayBuffer 是可以在多个执行环境中共享的 ArrayBuffer,但是考虑到多个执行环境的同步问题,所以在提案中 SharedArrayBuffer 增加了 grow(newByteLength) 的方法,无法缩小 SharedArrayBuffer 的大小。

Intl.DisplayNames V2

提案链接: https://github.com/tc39/intl-displaynames-v2

很多时候,我们不只是需要将一些动态值显示为用户能够阅读的语言文本,还需要将如历法名字、货币名、语言名、日期单位名等等信息也显示为用户所熟悉的文本。

目前,Intl.DisplayNames API 已经提供了基本的地名、语言名、书写系统名、货币名等各种名字的国际化显示文本支持。而这个提案则对 Intl.DisplayNames 的国际化数据进行了扩充,增加了日期时间单位名(“日”、“月”、“年”等)、历法名(公历等)、语言方言标准化名字(如通俗的 “简体中文” 和标准写法 “中文(简体)” 等)。

let dn = new Intl.DisplayNames("zh-Hans", {type: "calendar"})
dn.of("chinese") // "农历"
dn = Intl.DisplayNames("zh-Hans", {type: "dateTimeField"})
dn.of("month") // "月"
dn = Intl.DisplayNames("zh-Hans", languageDisplay: "dialect"})
dn.of("zh-Hans") // "简体中文"
dn = Intl.DisplayNames("zh-Hans", languageDisplay: "standard"})
dn.of("zh-Hans") // "中文(简体)"

Intl Extend TimeZoneName Option

提案链接: https://github.com/tc39/proposal-intl-extend-timezonename

这个 ECMA402 国际化提案扩展了 Intl.DateTimeFormat 中的 timeZoneName 选项,支持更多的格式化选项,让开发者可以更方便地控制日期格式化格式。

let timeZoneNames = ["short", "long", "shortOffset", "longOffset", "shortWall", "longWall"];
timeZoneNames.forEach(function(timeZoneName) { 
  console.log((new Date()).toLocaleTimeString("zh-Hans", {timeZoneName}));
});
// => 上午9:27:27 [PST]
// => 上午9:27:27 [太平洋标准时间]
// => 上午9:27:27 [GMT-8]
// => 上午9:27:27 [GMT-08:00]
// => 上午9:27:27 [PT]
// => 上午9:27:27 [太平洋时间]

Stage 1 -> Stage 2

从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。

RegExp set notation

提案链接: https://github.com/tc39/proposal-regexp-set-notation
Spec 链接: https://docs.google.com/document/d/1Tbv3hfX9CxQtzH9r-JdxJsQZhmmDsidRUKKxg345JV0/edit

许多正则表达式引擎都支持预设的字符集(通常都是 Unicode 的各种字符集),避免开发者需要在正则表达式中硬编码字符集。同时提案也包含了字符集的交集、差集操作,便于自由组合多个字符集。

// 差集
[A--B]
// 交集
[A&&B]
// 嵌套字符集
[A--[0-9]]

比如下面这个正则表达式可以匹配所有非 ASCII 数字,然后我们就可以将这些非 ASCII 数字转换成 ASCII 数字:

[\p{Decimal_Number}--[0-9]]

或者匹配所有非 ASCII 的 Emoji:

[\p{Emoji}--\p{ASCII}]

结语

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

https://github.com/JSCIG/es-discuss/discussions

f441bb4cf20944bda66ddae869a2c488.png

相关文章
|
编译器 开发者 Windows
windows10LTSC下载与安装
windows10LTSC下载与安装
498 0
|
JavaScript 前端开发 安全
抽象语法树(AST):理解JavaScript代码的抽象语法树
抽象语法树(AST):理解JavaScript代码的抽象语法树
|
10月前
|
存储 Kubernetes Docker
使用 Docker 搭建碎片化知识卡片-Memos
Memos 是一个功能全面、易于使用的开源知识库和社交平台,适合个人和企业使用。它支持标签、过滤、搜索和多账户管理,提供多种隐私设置和后端存储选择,支持单点登录和 Docker 部署,注重数据的安全性和私密性。
328 12
使用 Docker 搭建碎片化知识卡片-Memos
|
9月前
|
缓存 Java 应用服务中间件
nginx的正向代理和反向代理以及tomcat
Nginx的正向代理和反向代理功能在不同的场景中具有重要作用,正向代理主要用于客户端访问控制和匿名浏览,而反向代理则用于负载均衡和高可用性服务。Tomcat作为Java Web应用服务器,与Nginx结合使用,可以显著提升Web应用的性能和稳定性。通过合理配置Nginx和Tomcat,可以构建高效、稳定和可扩展的Web服务架构。
345 11
|
10月前
|
机器学习/深度学习 人工智能 JavaScript
JavaScript和TypeScript的未来发展趋势及其在Web开发中的应用前景
本文探讨了JavaScript和TypeScript的未来发展趋势及其在Web开发中的应用前景。JavaScript将注重性能优化、跨平台开发、AI融合及WebAssembly整合;TypeScript则强调与框架整合、强类型检查、前端工程化及WebAssembly的深度结合。两者结合发展,特别是在Vue 3.0中完全采用TypeScript编写,预示着未来的Web开发将更加高效、可靠。
434 4
|
10月前
|
缓存 前端开发 JavaScript
webpack 原理
【10月更文挑战第23天】Webpack 原理是一个复杂但又非常重要的体系。它通过模块解析、依赖管理、加载器和插件的协作,实现了对各种模块的高效打包和处理,为现代前端项目的开发和部署提供了强大的支持。同时,通过代码分割、按需加载、热模块替换等功能,提升了应用程序的性能和用户体验。随着前端技术的不断发展,Webpack 也在不断演进和完善,以适应不断变化的需求和挑战。
|
存储 Web App开发 JavaScript
ECMAScript Async Context 提案介绍
ECMAScript Async Context 提案介绍
23525 1
ECMAScript Async Context 提案介绍
|
算法 计算机视觉 开发者
OpenCV图形检测中绘制图像的轮廓讲解与实战应用(附Python源码)
OpenCV图形检测中绘制图像的轮廓讲解与实战应用(附Python源码)
334 0
|
安全 开发工具 iOS开发
探索macOS原版镜像ISO的下载之道
探索macOS原版镜像ISO的下载之道
|
人工智能 安全 Devops
让研发规范管得住,在流水线之上做研发流程
研发规范的目标,是为了解决或降低出现软件危机的风险。但传统流水线受限于工具的定位,无法解决研发规范的落地问题,需要在更高的层面来解决。阿里云云效团队经过内部启发后推出的新产品:云效应用交付平台 AppStack 给出了解决方案,快来使用体验吧!
79723 7