Redian新闻
>
ECMAScript 双月报告:findLast 提案成功进入到 Stage 4

ECMAScript 双月报告:findLast 提案成功进入到 Stage 4

科技

本次会议中,findLast 提案成功进入到了 Stage 4,这是第二个由中国开发者推动进入到 Stage 4 的提案。另外,较受关注的 String Dedent 与 JSON.parse source text access 等提案也在本次会议中取得了阶段性进展。


Stage 3 → stage 4



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

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

findFromLast


提案链接:https://tc39.es/proposal-array-find-from-last/index.html

这一提案为数组(Array 与 TypedArray)引入了两个新方法 findLastfindLastIndex,来支持从数组的结尾开始查找一个元素,以及它在数组中位于倒数第几项(如 -1、-2)。

我们知道 JavaScript 中 Array.find 方法会返回第一个符合条件的数组成员,如果我们想做的是获取最后一个符合条件的成员(如多次操作中取最后一次操作),就需要复制一个数组,调用 reverse 方法,然后才能进行搜索:

[...[]].reverse().find();

这意味着你需要额外创建一个数组并进行操作。

类似的,Array.findIndex 方法也会返回第一个符合条件的数组成员的索引,如果你希望获得最后一个符合条件成员的索引,也需要进行数组的复制和反转,然后配合数组的长度进行计算:

const arr = [1234];

arr.length - 1 - [...arr].reverse().findIndex(i => i % 2 === 1); // 2
arr.length - 1 - [...arr].reverse().findIndex(i => i % 2 === 10); // 4,错误

在第二处调用中,由于 findIndex 会在没有找到符合条件成员时返回 -1,此时就需要进行额外的处理。

基于此提案引入的方法,你可以使用符合直觉的方式来找到最后一个满足条件的成员:

const arr = [1234];

arr.findLast(i => i % 2 === 1); // 3
arr.findLastIndex(i => i % 2 === 1); // 2
arr.findLastIndex(i => i % 2 === 10); // -1

目前,我们已经可以在 Chrome 97 中使用这些 API 了。另外,现在也可以通过 core-js 和 es-shims 来使用这两个方法。


Stage 2 → Stage 3



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

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

Symbol as WeakMap Keys


提案链接:https://github.com/tc39/proposal-symbols-as-weakmap-keys

这一提案支持了在 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 的键值的,但是 Registered Symbol 不能作为 WeakMap 的键值。这是因为 Registered Symbol 实际上是无法观测到垃圾回收的,而不能观测到垃圾回收的值类型作为 WeakMap Key 没有实际意义。而 Well-known Symbol 虽然也是实际意义上无法被垃圾回收,但是这些 Symbol 是一个确定的列表,无法动态添加删除,所以也被允许作为 WeakMap 键值。

JSON.parse source text access


提案链接:https://github.com/tc39/proposal-json-parse-with-source

JavaScript 中对 JSON 的自定义类型支持一直都不是特别全面,如 JSON.parse 中存在的大数精度丢失问题,以及 JSON.stringify 中无法转换 JSON 中不存在的类型(如函数、Date 等),而 Stringify replacer 的输出会被再次序列化等问题。

// 大数精度丢失
JSON.parse(" 9999999999999999")
// → 10000000000000000

// reviver 函数的参数中,val 是一个已经被解析过的值,而非原始值
JSON.parse(" 9999999999999999", (key, val) => BigInt(val))
// → 10000000000000000n

// 前后值不一致
JSON.parse(JSON.stringify(new Date("2018-09-25T14:00:00Z")))
// → "2018-09-25T14:00:00.000Z"

// 字符串被再次序列化,加上了引号
JSON.stringify(9999999999999999n, (key, val) => String(val))
// → "\"9999999999999999\""

// 无法序列化的值类型会导致报错
JSON.stringify(9999999999999999n, (key, val) => val)
// → TypeError

为了解决 JSON.parse 中 reviver 函数的 val 参数是已经解析过(parsed)的值这一问题,此提案为 JSON.parse 的 reviver 函数引入了第三个参数 sourceText,以支持在 parse 过程中基于原来的值进行处理:

const tooBigForNumber = BigInt(Number.MAX_SAFE_INTEGER) + 2n;
// 第三个参数 source 
const intToBigInt = (key, val, {source}) => typeof val === "number" && val % 1 === 0 ? BigInt(source) : val;
const roundTripped = JSON.parse(String(tooBigForNumber), intToBigInt);
tooBigForNumber === roundTripped; // → true

对于 JSON.stringify 的序列化问题,此提案新增了 JSON.rawJson 方法来在 JSON.stringify 的 replacer 序列化过程中标记已经完成序列化的 JSON 值,而不必被二次序列化:

JSON.stringify(9999999999999999n, (key, val) => JSON.rawJSON(val))
// → "9999999999999999"


Regular Expression Pattern Modifiers for ECMAScript


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

我们在使用正则表达式时,可以指定多种执行模式,包含 i(大小写通配),m(多行匹配),s(单行匹配),还有目前同样作为 TC39 提案的 x(增强模式,见对应的提案 RegExp X Mode)。但是这些模式均为全量应用,即只能对整个正则表达式启用,并不能控制只对于其中的某一个部分生效。

为了解决这一问题,RegExp Modifiers 提案为正则表达式引入了子表达式,来实现局部范围内的模式启用与禁用。最初此提案包括 self-bounded 与 unbounded 两种模式,unbounded 模式在 21 年 12 月的 TC39 会议上被移除,目前仅有 self-bounded 模式,即自约束。

自约束(self-bounded)的基础语法为 (?imsx-imsx:subexpression) ,其使用 - 来在子表达式作用域内启用或禁用 flag 对应的模式。如(?-i:A(?i:B)C) 匹配 ABCAbC,但是不能匹配 aBC  或 ABc,其使用示例如下:

// 为 [a-z] 表达式取消大小写通配模式
const re1 = /^[a-z](?-i:[a-z])$/i;
re1.test("ab"); // true
re1.test("Ab"); // true
re1.test("aB"); // false



Stage 1 → Stage 2



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

Duplicate named capturing groups


提案链接:https://github.com/tc39/proposal-duplicate-named-capturing-groups

在正则表达式中,我们可以使用捕获组(Capturing Group)来对匹配模式中的某一部分做独立的匹配,如 es+ 会匹配 essssesssss+ 代表匹配一次或更多),而使用匹配组,我们可以将 es 作为一个匹配部分,如 (es)+ 会匹配 es 以及  eseses  等。

我们也可以对捕获组进行命名,如 ?<name> 这样的形式,常见的一个场景是结合 str.match 方法:

const dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
const str = "2022-06-01";

const groups = str.match(dateRegexp).groups;

groups.year; // 2022
groups.month; // 06
groups.day; // 01

每个捕获组的命名都需要是唯一的,这就使得我们无法使用同名捕获组匹配一组联合模式,如日期格式还可能是 06-01-2022,我们希望能这么使用联合模式:

const dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})|(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})/;

但由于捕获组的命名唯一约束,上面这个表达式是不合法的。

为了解决这一问题,此提案提出允许捕获组的命名不唯一,以此来支持如上面在联合模式中使用捕获组的场景。


String Dedent


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

String dedent 提案在 21 年 9 月的 TC39 会议上从 Stage 0 进入到 Stage 1,它引入了 String.dedent 方法来优化多行模板字符串下的行首空格表现。

举例来说,如果我们希望生成多行顶格的字符串,可能会这么写:

class Foo {
  methodA() {
    const foo = `create table student(
  id int primary key,
  name text
)`
;
    return foo;
  }
}
create table student(
  id int primary key,
  name text
)

虽然最终结果是正常的,但是这种使用方式会导致代码中与实际结果的字符串格式不一致,在缩进较深的情况下显得尤为怪异。

如果使用  String.dedent 方法,我们可以确保代码中与实际结果的格式一致:


class Foo {
  methodA() {
    const foo = String.dedent(`
      create table student(
        id int primary key,
        name text
      )
      `
);
    return foo;
  }
}

String.dedent 的核心功能就是移除所有非空内容行的公共缩进,同时删除开头、结尾的文字换行符,来使模板字符串的代码与最终结果完全一致。

你也可以通过 Playground 进行更多尝试。



Stage 0 → Stage 1



从 Stage 0 进入到 Stage 1 有以下门槛:

  1. 找到一个 TC39 成员作为 champion 负责这个提案的演进;
  2. 明确提案需要解决的问题与需求和大致的解决方案;
  3. 有问题、解决方案的例子;
  4. 对 API 形式、关键算法、语义、实现风险等有讨论、分析。
    Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。


Import Reflection


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

此提案的原名为 Evaluator Attributes 提案,在 2021 年 10 月会议上已进入到 Stage 1,本次属于更名的同时进行了提案内容的更新。

Import Reflection 提案为 import 语句支持了使用 as 关键字来声明导入反射属性(元数据)的能力,如:

import x from "<specifier>" as "<reflection-type>";

这一标注会改变 import 语句的对于目标模块的执行方式,以此提案的主要驱动场景之一为例, 为 WebAssembly 模块指定额外的类型,如实例导入(WebAssembly.Instance)与模块导入(WebAssembly.Module)。

import FooModule from "./foo.wasm" as "wasm-module";
FooModule instanceof WebAssembly.Module; // true

// WASI 是适用于 WebAssembly 的模块化系统调用规范
import { WASI } from 'wasi';
const wasi = new WASI({ args, env, preopens });

// 实例化 WebAssembly 模块,并与 WASI 实现链接
const fooInstance = await WebAssembly.instantiate(FooModule, {
  wasi_snapshot_preview1: wasi.wasiImport
});

// 执行
wasi.start(fooInstance);

以上示例使用了 wasm-module 作为反射信息,以改变对一个已编译完毕(但尚未链接)的 WebAssembly 模块对象的导入行为。

与另外一个在 import 语法中引入新语法的提案 Import Assertion 对比,其在导入语句中新增了断言语法,使得我们可以将模块断言为指定的类型,来提高引擎对模块导入的处理效率。以派生自 Import Assertion 提案的 JSON Modules 提案为例,其语法大致如下:

import json from "./foo.json" assert { type"json" };
import("foo.json", { assert: { type"json" } });

对于 Import Assertion,不同的断言并不会影响其解析结果。这也是其与 Import Reflection 的核心差异之一。


Regular Expression Atomic Operators for ECMAScript


提案链接:https://github.com/tc39/proposal-regexp-atomic-operators

这一提案将为 ECMAScript 中的正则表达式引入新的原子操作符(Atomic Operators)支持,包括原子组 ?> 与占有式量词 n*+n++ 等,来解决正则表达式匹配时的回溯问题。

举例来说,正则表达式 /a(bc|b)c/ 能同时匹配到 abccabc,在前者中,我们依次匹配 a、bc、c,但对于 abc 的情况则并不完全符合直觉,我们依次匹配 a、bc,由于匹配到 bc 耗尽了字符串的剩余部分,导致剩下的正则表达式 c 无法进行匹配。此时执行会重新回到 (bc|b) 的位置,改为匹配 b,然后才匹配到 c。

也就是说,在这种联合模式匹配时,如果其中的某一种匹配模式会导致整个正则表达式匹配失败,那么实际执行时会重新回溯,尝试切换到另一种匹配模式,以尽可能完成对整个正则表达式的匹配。我们可以使用原子组将原来的表达式改写为 a(?>bc|b)c ,此时如果联合模式中的某一部分成功匹配上了,那么即使在后续执行过程中正则表达式匹配失败,也不会再次回到此联合模式尝试重新匹配。也就是说在这种模式下,abc 将不再会被匹配。

原子组的作用是在联合模式成功匹配时避免后续可能的回溯匹配,而占有式量词则用于在满足某些条件时才阻止回溯匹配,其可以被视为原子组的语法糖。如 atom*+ 等价于 (?>atom*)atom++ 等价于 (?>atom+)atom{n,m}+ 等价于 (?>atom{n,m}) 等。

Faster Promsie Adoption


提案链接:https://github.com/tc39/proposal-faster-promise-adoption

Promise 的优化一直是引擎与 JavaScript 开发者关注的重点问题之一。比如 V8 团队曾经对 await 的标准行为提议了优化,去除了一个多余的 Promise Wrap 即去除了一次多余的异步循环 Tick 以降低 await 行为的损耗。

而这次,同样也是期望降低特定场景下使用 Promise 时的 tick 次数:

const outer = new Promise(res => {
  const inner = Promise.resolve(1);
  res(inner);
});

outer.then(log);

比如对于上面这段代码片段,其中 outer 这个 Promise 会需要 2 轮 tick 才能转换为 "resolve" 状态。即当你在 Promise 构造器的 resolve 方法中返回另一个的 Promise inner 时(Promise.resolve(1)),它实际上会在下一个 tick 才调用这个 Promise inner 的 then 方法;然后在再下一个 tick 时将 Promise outer 的状态设置为 "resolve" 状态;再在下一次 tick 时才会执行 Promise outer 的 then 方法。

也就是说,上面这个代码片段相当于:

NEXT_TICK(() => inner.then(settleOuter));
NEXT_TICK(() => settleOuter(inner.[[Res]]));
NEXT_TICK(() => log(outer.[[Res]]));

这在 async/await 已经非常流行的 JavaScript 来说,比如我们经常会写以下代码片段,带来额外 tick 的成本是非常可观的:

// 在 async 函数中返回一个 promise 需要 2 次 tick 才会完成 `direct` 的 resolve。
const direct = (async () => Promise.resolve(1))();

为了解决这一问题,这一提案提出让一个 promise 可以快速获得另外一个 promise 的状态,而无需额外 tick 周期的方案:

// 如果 inner 是个 Promise,不再需要一次额外的 tick 调用
inner.then(settleOuter);
NEXT_TICK(() => settleOuter(inner.[[Res]]));
NEXT_TICK(() => log(outer.[[Res]]));



结语



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





关注「Alibaba F2E」微信公众号把握阿里巴巴前端新动向


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

戳这里提交新闻线索和高质量文章给我们。
相关阅读
Wheat Destroyed Before Harvest Prompts Food Crisis Discussion美国迈阿密最贵的富人区:Fish Island硬核观察 #645 JavaScript 和 Python 继续统治编程语言,但 Rust 在崛起ReadLexington:The Last Rose of Shanghai by Weina Dai RandelAmazon Turns the Page on Its Chinese Kindle BookstoreLidl告Tesco抄袭?!网友:这也算侵权??从引入到改革,SCI指标如何影响中国科研评价?How the Kindle Lost China爆料!带手写功能的大屏Kindle,正在研发中!上一个说“丼”不读jǐng的人,已经被我骂哭了在地球日说说我们能做什么Residents Crowd COVID Test Sites to Move Across Shanghai Freely美国证券交易委员会(SEC)提出最新关于基金名称的提案,甚至影响到ESG基金Their Secret Sales Weapons? Language LessonsChina’s Tutoring Ban Has Become an Endless Game of Whack-a-Mole【微报告】中国SCRM市场行业简析报告丨甲子光年智库学人习语249:feather your own nest | ​现在订阅就送三重好礼!美国医生的收入到底有多高?(2022)[腕表] Jaeger-LeCoultre Rendez-Vous Dazzling“中学”不能翻译成middle school吗?望向宇宙的最深处2024年美国总统将是川普吗你悼念的不是 Kindle,是过去爱看书的自己浸化论|首个剧本杀元宇宙:FindTruman专访措手不及!Kindle 宣布在中国停止运营,「泡面盖」终成回忆贺多伦多第八届华人作家节China Writing Contest Deadline Extended to May 14, 2022旧款Kindle将无法使用亚马逊书店!快看看有没有你的型号?Behind China’s Nurse Shortage, a Lack of Respect「Kindle」变彩色、可折叠、可卷曲,你还会再爱它一次吗?Omecamtiv Mecarbil治疗心衰,低收缩压患者获益更显著|ESC-HF 2022热点速递使用 External Secrets Operator 安全管理 Kubernetes Secrets硬核观察 #625 Kindle 将支持 EPUB 格式的电子书深度剖析 VS Code JavaScript Debugger 功能及实现原理野蘑菇
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。