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」微信公众号把握阿里巴巴前端新动向


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

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