专栏名称: 前端从进阶到入院
我是 ssh,只想用最简单的方式把原理讲明白。wx:sshsunlight,分享前端的前沿趋势和一些有趣的事情。
目录
相关文章推荐
杭州交通918  ·  太意外!44岁女演员官宣分手 ·  2 天前  
钱江晚报  ·  大S去世后,小S首度更新动态 ·  2 天前  
杭州日报  ·  终于!他官宣喜讯!网友沸了:期待今年3月 ·  2 天前  
FM93交通之声  ·  晚高峰突发!杭州一新能源车起火! ·  3 天前  
51好读  ›  专栏  ›  前端从进阶到入院

12 项 ECMAScript 提案的最新进展!

前端从进阶到入院  · 公众号  ·  · 2024-10-16 09:38

正文

大家好,我是 ssh

近日,ECMA 国际技术委员会 39(TC39)在东京召开了第 104 次大会,讨论了多项 ECMAScript (JavaScript)提案的进展情况,批准了其中多项提案进入下一个阶段。

  • 「Stage 4」迭代器助手(Iterator Helpers)
  • 「Stage 4」导入属性与 JSON 模块(Import Attributes & JSON Modules)
  • 「Stage 4」正则表达式修饰符(Regular Expression Modifiers)
  • 「Stage 4」Promise.try
  • 「Stage 3」精确求和(Math.sumPrecise)
  • 「Stage 3」Atomics.pause
  • 「Stage 2.7」Error.isError
  • 「Stage 2.7」迭代器序列化(Iterator Sequencing)
  • 「Stage 2」结构体与共享结构体(Structs & Shared Structs)
  • 「Stage 2」Extractors
  • 「讨论中」Array.zip
  • 「讨论中」不可变的 ArrayBuffer(Immutable ArrayBuffers)

Stage 2.7

在介绍这些提案前,我们先聊聊 ECMAScript 的提案流程引入了一个新阶段 — 2.7 阶段。

每个新特性在被正式纳入 JavaScript 规范之前,需要通过一个提案流程。这一流程从 0 阶段(初步想法)一直到 4 阶段(准备发布)。所有提案流程都是以零为编号开始的,通常包含 0(草案提案)、1(提案通过)、2(特性定义)、3(推荐实施)、和 4(完成并发布)。

  • 0 阶段 :一个新的提案(构思和探索)。
  • 1 阶段 :提案进入考虑阶段(特性设计)。
  • 2 阶段 :达成共识并定义了可能的解决方案(改进和优化)。
  • 2.7 阶段 :编写测试(测试和验证)。
  • 3 阶段 :推荐实施提案(集成和兼容性探索)。
  • 4 阶段 :新特性准备纳入规范并发布!

2.7 阶段的关键在于,它相当于过去的 3 阶段,但更强调测试的编写和验证。提案进入 2.7 阶段时,设计已经完成,规范也已完整,此时需要编写实际代码(包括测试和非 polyfill 实现)来获取反馈,以便进一步推进。

在 2023 年底,TC39 正式引入了 2.7 阶段。这个阶段源于对提案流程的优化,希望能在提案进入 3 阶段前,确保所有的测试都已经编写并通过了验证。之前,3 阶段并不包含测试的内容,这可能导致当测试实现时发现新的问题,从而出现从 3 阶段退回到 2 阶段的情况。

为什么不直接增加一个新的阶段编号,而选择使用 2.7 呢?主要是为了避免大规模的文档更新和链接破损。如果将现有的阶段重新编号,比如将 3 阶段改为 4 阶段,可能会导致大量的文档和链接失效,维护成本会非常高。

下面是一些关键提案的详细介绍及其进展:

1. 「Stage 4」迭代器助手(Iterator Helpers)

迭代器在表示大型或无限可枚举数据集时非常有用。然而,迭代器缺乏与数组或其他有限数据结构同样易用的辅助方法,导致一些问题不得不通过数组或外部库来解决。许多库和编程语言已经提供了类似的接口。

该提案引入了一系列新的迭代器原型方法,允许开发者更方便地使用和消费迭代器。

  • map(mapperFn)

应用映射函数,返回处理后的值的迭代器。

iter.map(value => value * value);
  • filter(filtererFn)

根据过滤函数筛选元素,返回通过条件的值的迭代器。

iter.filter(value => value % 2 == 0);
  • take(limit)

获取有限数量的元素,返回新的迭代器。

iter.take(3);
  • drop(limit)

跳过指定数量的元素,返回剩余元素的新迭代器。

iter.drop(3);
  • flatMap(mapperFn)

将映射函数作用于元素,并展平结果,返回扁平化后的新迭代器。

iter.flatMap(value => value.split(" "));
  • reduce(reducer, initialValue)

通过 reducer 函数累计处理元素,返回汇总结果。

iter.reduce((sum, value) => sum + value, 0);
  • toArray()

将迭代器转换为数组。

iter.toArray();
  • forEach(fn)

对每个元素执行副作用操作,不返回值。

iter.forEach(value => console.log(value));
  • some(fn)

检查是否有任意一个元素满足条件,返回布尔值。

iter.some(value => value > 1);
  • every(fn)

检查是否所有元素都满足条件,返回布尔值。

iter.every(value => value >= 0);
  • find(fn)

找到第一个满足条件的元素,返回该元素,没有找到返回 undefined

iter.find(value => value > 1);
  • Iterator.from(object)

将“类似迭代器”的对象转换为迭代器。

Iterator.from(arrayLike);

GitHub 链接:Iterator Helpers Proposal

https://github.com/tc39/proposal-iterator-helpers

2. 「Stage 4」导入属性与 JSON 模块(Import Attributes & JSON Modules)

导入属性与 JSON 模块提案已进入 Stage 4,此提案增加了在导入文件时附带额外信息的能力。初始应用包括支持 JSON 模块,使开发者能够在导入 JSON 文件时明确指定其类型为 json ,增强代码的可读性和安全性。

标准化 JSON ES 模块的提案使得 JavaScript 模块可以轻松导入 JSON 数据文件,类似于许多非标准 JavaScript 模块系统中的支持。此提案不仅获得了 Web 开发者和浏览器的广泛支持,还被合并到了 HTML 标准中,由微软为 V8/Chromium 实现。然而,为了增强安全性,提出需要在导入 JSON 模块时使用语法标记,以防服务器意外返回不同 MIME 类型,导致意外代码执行。

为支持不同模块类型,标准化了以下语法:

// 静态导入 JSON 模块
import json from "./foo.json" with { type"json" };

// 动态导入 JSON 模块
import ("foo.json", { with: { type"json" } });

使用 with 语法可以在不同上下文中设置各种属性:

  • 导入声明中的语法
import json from "./foo.json" with { type"json" };
  • 二次导出中的语法
export { val } from './foo.js' with { type"javascript" };
  • 动态导入中的语法
import("foo.json", { with: { type"json" } });

下面是一些使用场景示例

  • Worker实例化
new Worker("foo.wasm", { type"module"with: { type"webassembly" } });
  • HTML 中的 script 标签
<script src="foo.wasm" type="module" withtype="webassembly">script>
  • 静态导入 JSON 模块
import json from "./data.json" with { type"json" };
console.log(json); // JSON 数据
  • 动态导入 JSON 模块
import("./data.json", { with: { type"json" } })
  .then(json => {
    console.log(json); // JSON 数据
  });
  • 导入 WebAssembly 模块
new Worker("module.wasm", { type"module"with: { type"webassembly" } });

GitHub 链接:Import Attributes Proposal

https://github.com/tc39/proposal-import-attributes

3. 「Stage 4」正则表达式修饰符(Regular Expression Modifiers)

正则表达式修饰符提案已进入 Stage 4,该提案允许在子表达式内更改正则表达式的标志,从而使正则表达式变得更加灵活。

正则表达式标志是许多正则表达式引擎中常见的功能,用于解析器、语法高亮等工具。然而,在当前 JavaScript 中,这些标志要么全局启用,要么全局禁用,缺乏细粒度的控制能力。这个提案提出了让这些标志可以在子表达式范围内生效的机制。

该提案引入了在正则表达式中动态设置或取消各种标志的语法:

  • 设置或取消指定子表达式的标志
  (?imsx-imsx:子表达式)
  • 设置或取消从当前位置直到下一个关闭括号或表达式结尾的标志 (注意:这部分提案已不再被考虑)
  (?imsx-imsx)

支持的标志包括:

  • i - 忽略大小写
  • m - 多行模式
  • s - 单行模式(也称 "dot all" 模式)
  • x - 扩展模式

示例:


  1. 忽略大小写的局部子表达式
   const re1 = /^[a-z](?-i:[a-z])$/i;
   re1.test("ab"); // true
   re1.test("Ab"); // true
   re1.test("aB"); // false

  1. 全局忽略大小写(只是对照)
   const re2 = /^(?i:[a-z])[a-z]$/;
   re2.test("ab"); // true
   re2.test("Ab"); // true
   re2.test("aB"); // false

https://github.com/tc39/proposal-regexp-modifiers

4. 「Stage 4」Promise.try

Promise.try 提案已进入 Stage 4,这个提案用于简化同步和异步函数的统一处理。它将任意函数包装在一个 Promise 中,确保函数在当前调用栈中执行,并返回一个 Promise ,处理可能的返回值或异常。

动机

  • 现有问题 :使用 Promise.resolve().then(f) 会导致函数 f 异步调用,而 new Promise(resolve => resolve(f())) 使用不便。
  • 解决方案 Promise.try(f) 提供了简洁的 API,同步执行函数,并处理生成的 Promise

主要功能

  • 同步执行函数 f
  • 包装返回值或异常为 Promise ,支持链式操作。

同步函数:

function syncFunction({
    return 42;
}

Promise.try(syncFunction)
    .then(console.log)  // 输出:42
    .catch(console.error);

异步函数:

async function asyncFunction({
    return 42;
}

Promise.try(asyncFunction)
    .then(console.log)  // 输出:42
    .catch(console.error);

处理异常:

function riskyFunction({
    throw new Error('Error!');
}

Promise.try(riskyFunction)
    .then(console.log)
    .catch(console.error);  // 输出:Error: Error!

GitHub 链接:Promise.try Proposal

https://github.com/tc39/proposal-promise-try

5.「Stage 3」精确求和(Math.sumPrecise)

精确求和提案已进入 Stage 3,该提案建议在 JavaScript 数学库中增加一个新的静态方法 Math.sumPrecise ,用于精确计算多个浮点数的和,避免传统加法中的浮点数精度问题。

动机

  • 常见操作 :对列表求和是非常常见的操作,目前很多情况依赖 Array.prototype.reduce
  • 精度问题 :简单的 .reduce((a, b) => a + b, 0) 在处理浮点数时可能会有精度问题,通过更聪明的算法可以提高精度。

因此提议添加一个 Math.sumPrecise 相对于传统求和方法在精度上的改进:

let values = [1e200.1-1e20];

values.reduce((a, b) => a + b, 0); // 0

Math.sumPrecise(values); // 0.1

GitHub 链接:Math.sumPrecise Proposal

https://github.com/tc39/proposal-math-sum-precise

6.「Stage 3」Atomics.pause

Atomics.pause(N) 提案已进入 Stage 3,该提案建议增加 Atomics.pause 方法,用于多线程编程中优化 CPU 资源利用。 Atomics.pause 可以在指定的纳秒时间内暂停当前线程,从而提高 CPU 使用效率。

在多线程编程中,锁的高效实现非常关键。当前的锁获取算法通常如下:

let spins = 0;
do {
  if (TryLock()) {
    // 锁定成功
    return;
  }

  SpinForALittleBit();
  spins++;
while (spins 
// 慢速路径
PutThreadToSleepUntilLockReleased();

对于这种情况,通过短暂的空转(spinning)可以提高性能,因为避免了线程进入内核。相反,在竞争激烈时,将线程置于休眠状态可以提高效率。然而,在 JavaScript 中编写优化的 SpinForALittleBit 方法非常困难。

“空转” 是计算机科学中的一个术语,英文通常叫做 “spinning” 或者 “busy waiting”。指的是一个线程或进程在等待某个条件满足期间,仍然保持在执行状态,而不进入阻塞状态(即不让出 CPU)。它会不断地检查这个条件,比如一个锁是否已经被释放。

提案中引入的新方法是 Atomics.pause(N) ,该方法执行一段非常短的有限等待时间,运行时可以通过适当的 CPU 提示来实现。它不具有阻塞性,因此可以在主线程和工作线程中调用。

以下是如何使用 Atomics.pause 进行空转的代码示例:

// 使用 Atomics.pause 进行空转
let spins = 0;
do {
  if (TryLock()) {
    // 锁定成功
    return;
  }

  Atomics.pause(spins);
  spins++;
while (spins 
  • CPU 提示 :不同架构可能有不同实现。以 x86 为例,Intel 推荐的 pause 指令和指数退避(exponential backoff)结合使用,可以实现有效的 CPU 提示。
  • 控制参数 N :非负整数参数 N 控制暂停时间,值越大,暂停时间越长。它可以用于在循环中实现退避算法。

GitHub 链接:Atomics.pause Proposal

https://github.com/tc39/proposal-atomics-microwait

7. 「Stage 2.7」Error.isError

Error.isError 提案已进入 Stage 2.7。

proposal-is-error 提案旨在为 JavaScript 引入一种新的方法 Error.isError ,用于可靠地判断一个值是否为原生 Error 对象。这将解决 instanceof Error 在跨上下文(如 iframe 或 Node.js 的 vm 模块)使用时可能导致的误判问题。

当前,判断一个对象是否为 Error 实例主要依赖于 instanceof Error ,但这种方法在跨越不同环境时并不可靠。不同 JavaScript 运行环境之间创建的错误对象实例无法通过 instanceof 进行可靠的验证。此外, Symbol.toStringTag 也影响了通过 Object.prototype.toString 进行检验的可靠性。

  • 调试 :在调试过程中,能确定一个值是否为原生错误,这对错误报告库非常有益。
  • 序列化 :平台如 RunKit 需要安全地序列化值并在用户浏览器中重建或描述它们,品牌检查对此至关重要。
  • 结构化克隆 :HTML 的 structuredClone 方法以及 Node.js 中的克隆方法对原生错误对象有特殊处理。JavaScript 程序需要一种方法来提前知道这种行为是否会被应用。

基本用法:

class CustomError extends Error {}

const error = new Error('This is an error');
const customError = new CustomError('This is a custom error');
const notAnError = {};

console.log(Error.isError(error));        // 输出: true
console.log(Error.isError(customError));  // 输出: true
console.log(Error.isError(notAnError));   // 输出: false
console.log(Error.isError(undefined));    // 输出: false

处理跨域实例:

// 假设我们有一个 iframe 引入的页面
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);

const iframeError = iframe.contentWindow.Error('This error comes from an iframe');

console.log(Error.isError(iframeError));  // 输出: true
console.log(iframeError instanceof Error); // 输出: false

GitHub 链接:Error.isError Proposal

https://github.com/tc39/proposal-error-iserror

8. 「Stage 2.7」迭代器序列化(Iterator Sequencing)

迭代器序列化提案已经进入 Stage 2.7。

在 JavaScript 编程过程中,我们经常会遇到需要依次消费多个迭代器中的值的情况,这就像它们是一个单独的迭代器一样。在其他语言以及一些迭代器库(例如标准库)中,通常有类似 concat chain 的功能来实现这种需求。在当前的 JavaScript 中,可以通过生成器实现这一点,如下所示:

let lows = Iterator.from([0123]);
let highs = Iterator.from([6789]);

let lowsAndHighs = function* ({
  yield* lows;
  yield* highs;
}();

console.log(Array.from(lowsAndHighs)); // [0, 1, 2, 3, 6, 7, 8, 9]

此外,我们还能通过生成器方法在迭代器之间插入即时值:

let digits = function* ({
  yield* lows;
  yield 4;
  yield 5;
  yield* highs;
}();

console.log(Array.from(digits)); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

为了使这种操作更方便和实用,TC39 提出了新的解决方案。

新的解决方案使用了 Iterator.concat 方法来连接多个迭代器:

let digits = Iterator.concat(lows, [45], highs);

对于一些特殊情况,例如无限多的迭代器,可以将 flatMap 与身份函数结合使用:

functionp({
  for (let n = 1;; ++n) {
    yield Array(n).fill(n);
  }
}
let repeatedNats = p().flatMap(x => x);

GitHub 链接:Iterator Sequencing Proposal

https://github.com/tc39/proposal-iterator-sequencing

9. 「Stage 2」结构体与共享结构体(Structs & Shared Structs)

结构体与共享结构体提案已进入 Stage 2。

该提案 proposal-structs 旨在为 JavaScript 引入固定布局的对象(结构体),以提高性能和并行处理能力。结构体的设计目标是为高性能应用提供更高的性能上限,并且使其容易进行静态分析:

  1. 结构体 :固定布局对象。类似于类实例,但具有更多限制,有助于优化和分析。

结构体实例在创建时采用封闭的完整性级别,即固定布局。无法添加新属性,也不能改变原型,所有声明的字段可写、可枚举和不可配置。

struct Box {
  constructor(x) { this.x = x; }
  x;
}

let box = new Box(0);
box.x = 42;  // x 是已声明的
// 下面的操作会抛出异常,因为结构体是封闭的
assertThrows(() => { box.y = 8.8; });
assertThrows(() => { box.__proto__ = {}; });
  • 继承结构体

结构体只能继承其他结构体。

struct Point extends Box {
  constructor(x, y) {
    this.y = y;  // this 值可以立即使用
    super(x);    // 调用父结构体构造函数
    return {};   // 返回值被丢弃,无法覆盖返回
  }

  distance(other) {
    return Math.sqrt((other.x - this.x) ** 2 + (other.y - this.y) ** 2);
  }

  y;
}

let p = new Point(12);
let fake = { x4y5 };
// 方法是不可泛化的
assertThrows(() => Point.prototype.distance.call(fake, p));
p.distance(fake); // 允许,接收者是 Point
  • 共享结构体






请到「今天看啥」查看全文