12 项 ECMAScript 提案的最新进展!

科技   2024-10-16 09:40   江苏  

大家好,我是 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 < kSpinCount);

// 慢速路径
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 < kSpinCount);
  • 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 编程过程中,我们经常会遇到需要依次消费多个迭代器中的值的情况,这就像它们是一个单独的迭代器一样。在其他语言以及一些迭代器库(例如标准库)中,通常有类似 concatchain 的功能来实现这种需求。在当前的 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
  • 共享结构体

共享结构体是可以在多个代理间共享并并行访问的结构体,除了遵循上述结构体属性外,还具有以下特性:

  • 只能继承其他共享结构体。
  • 具有 null 原型。
  • 不能包含实例方法或实例私有名称。
  • 实例可以在不复制的情况下与其他代理通信。
// main.js
shared struct SharedBox {
  x;
}

let sharedBox = new SharedBox();
let sharedBox2 = new SharedBox();

unsafe {
  sharedBox.x = 42;          // x 是已声明的
  sharedBox.x = sharedBox2;  // x 是已声明的且 rhs 是共享的
  assertThrows(() => { sharedBox.x = {}; }) // rhs 不是共享结构体
}

// 可编程测试值是否可以共享
assert(Reflect.canBeShared(sharedBox2));
assert(!Reflect.canBeShared({}));

let worker = new Worker('worker.js');
worker.postMessage({ sharedBox });

unsafe {
  sharedBox.x = "main";      // x 是已声明的且 rhs 是原语
  console.log(sharedBox.x);
}

// worker.js
onmessage = function(e{
  let sharedBox = e.data.sharedBox;
  unsafe {
    sharedBox.x = "worker";  // x 是已声明的且 rhs 是原语
    console.log(sharedBox.x);
  }
};
  • 互斥锁和条件变量

用于同步访问共享内存的高级抽象:

  • 互斥锁 (Mutex):非递归互斥锁,适用于同步对共享内存的访问。
  • 条件变量 (Condition):用于管理协作线程之间的等待和通知机制。
shared struct MicrosoftSharePoint {
  x;
  y;
  mutex;
}

let point = new MicrosoftSharePoint();
point.mutex = new Atomics.Mutex();

let worker = new Worker('worker_mutex.js');
worker.postMessage({ point });

// 假设此代理可以阻塞
unsafe {
  using lock = Atomics.Mutex.lock(point.mutex);
  point.x = "main";
  point.y = "main";
}

unsafe {
  using lock = Atomics.Mutex.lock(point.mutex);
  console.log(point.x, point.y);
}

// worker_mutex.js
onmessage = function(e{
  let point = e.data.point;
  unsafe {
    using lock = Atomics.Mutex.lock(point.mutex);
    point.x = "worker";
    point.y = "worker";
  }
};

GitHub 链接:Structs & Shared Structs Proposal

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

10. 「Stage 2」Extractors

Extractors 提案已进入 Stage 2。

ECMAScript 当前缺乏在解构时执行用户定义逻辑的机制,这使得数据验证和转换需要多条语句才能完成。通过引入 Extractors,可以将此类逻辑封装到一个解构模式中,从而简化代码。

Extractors 对象通过 Symbol.customMatcher 方法,允许自定义解构逻辑,并在解构时调用该方法。该方法返回一个可迭代对象,指示匹配成功以及要提取的元素。

以下是使用 Extractors 的一些关键代码示例:

  • 基本解构示例
const Foo = {
  [Symbol.customMatcher](value) {
    return [value];  // 简单返回值包装成数组
  }
};

const x = [123];
const Foo(y) = x;  // 使用 Foo 提取器进行解构
console.log(y);    // 输出:1
  • 处理不同类型的数据
const DateExtractor = {
  [Symbol.customMatcher](value) {
    if (value instanceof Date) {
      return [value];
    } else if (typeof value === 'string') {
      return [new Date(value)];
    } else {
      throw new TypeError('Invalid date');
    }
  }
};

const data = '2024-10-13T06:41:07Z';
const DateExtractor(date) = data;  // 使用 DateExtractor 提取器进行解构
console.log(date);  // 输出:Sun Oct 13 2024 06:41:07 GMT+0000 (UTC)
  • 结合嵌套和模式匹配使用
const InstantExtractor = {
  [Symbol.customMatcher](value) {
    if (value instanceof Temporal.Instant) {
      return [value];
    } else if (value instanceof Date) {
      return [Temporal.Instant.fromEpochMilliseconds(+value)];
    } else if (typeof value === 'string') {
      return [Temporal.Instant.from(value)];
    } else {
      throw new TypeError();
    }
  }
};

const obj = {
  createdAt'2024-10-13T06:41:07Z',
  modifiedAtnew Date('2024-10-14T08:00:00Z')
};

const {
  createdAt: InstantExtractor(createdAt),
  modifiedAt: InstantExtractor(modifiedAt)
} = obj;

console.log(createdAt);  // 输出:2024-10-13T06:41:07Z
console.log(modifiedAt); // 输出:Mon Oct 14 2024 08:00:00 GMT+0000 (UTC)

GitHub 链接:Extractors

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

11.「讨论中」Array.zip

Array.zip 提案目前在讨论阶段。该提案建议为 Array 构造函数添加 Array.zipArray.zipKeyed 两个静态方法。使用这些方法,开发者可以将多个数组交叉合并为一个数组,从而实现更便捷的数据处理。

示例代码:

const list1 = [123];
const list2 = ['a''b''c'];
console.log(Array.zip(list1, list2));  // [[1, 'a'], [2, 'b'], [3, 'c']]

GitHub 链接:Array.zip Proposal

https://github.com/tc39/proposal-array-zip

12.「讨论中」不可变的 ArrayBuffer(Immutable ArrayBuffers)

不可变的 ArrayBuffer 提案目前在讨论阶段,该提案建议允许创建内容不可改变的缓冲区,以提高内存使用的安全性和稳定性。这种缓冲区一旦创建,其内容将无法修改,从而避免了数据的意外更改。

示例代码:

const buffer = new ArrayBuffer(10);  // 可变
const immutableBuffer = ArrayBuffer.immutable(10);  // 不可变

GitHub 链接:Immutable ArrayBuffers Proposal

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





  • 我是 ssh,工作 6 年+,阿里云、字节跳动 Web infra 一线拼杀出来的资深前端工程师 + 面试官,非常熟悉大厂的面试套路,Vue、React 以及前端工程化领域深入浅出的文章帮助无数人进入了大厂。
  • 欢迎长按图片加 ssh 为好友,我会第一时间和你分享前端行业趋势,学习途径等等。2024 陪你一起度过!


  • 关注公众号,发送消息:
    指南获取高级前端、算法学习路线,是我自己一路走来的实践。
    简历获取大厂简历编写指南,是我看了上百份简历后总结的心血。
    面经获取大厂面试题,集结社区优质面经,助你攀登高峰
因为微信公众号修改规则,如果不标星或点在看,你可能会收不到我公众号文章的推送,请大家将本公众号星标,看完文章后记得点下赞或者在看,谢谢各位!

前端从进阶到入院
我是 ssh,只想用最简单的方式把原理讲明白。wx:sshsunlight,分享前端的前沿趋势和一些有趣的事情。
 最新文章