查看原文
其他

ECMAScript 最新进展汇总!

CUGGZ 前端充电宝 2023-10-06

2023 年 7 月 11 日 - 7 月 13 日,第 97 次 TC39 会议在挪威举行,下面就来看看在这次会议中哪些 ECMAScript 提案取得了新进展吧!

TC39 是一个推动 JavaScript 发展的技术委员会,由各个主流浏览器厂商的代表构成,其主要工作就是制定 ECMAScript 标准。TC39 每两个月举行一次会议。对于新提案,从提出到最后被纳入 ECMAScript 新特性,TC39 的规范中分为五步:

  • Stage 0(strawman),任何TC39的成员都可以提交。
  • Stage 1(proposal),进入此阶段就意味着这一提案被认为是正式的了,需要对此提案的场景与API进行详尽的描述。
  • Stage 2(draft),这一阶段的提案如果能最终进入到标准,那么在之后的阶段都不会有太大的变化,因为理论上只接受增量修改。
  • Stage 3(candidate),这一阶段的提案只有在遇到了重大问题才会修改,规范文档需要被全面的完成。
  • Stage 4(finished),这一阶段的提案将会被纳入到ES每年发布的规范之中。

附: ECMAScript 2023(ES14)已于 6 月 27 日正式发布,详见 >>> 《ECMAScript 2023 正式发布,有哪些新特性?》

Stage 3

数组分组

提案[1] 用于简化数组(和可迭代对象)中的分组操作。数组分组是一种非常常见的操作,其将相似的数据组合成组允许开发者计算更高阶的数据集。

const array = [12345];

// Object.groupBy 根据任意键对元素进行分组,这里通过奇偶数对元素进行分组。
Object.groupBy(array, (num, index) => {
  return num % 2 === 0 ? 'even''odd';
});
// =>  { odd: [1, 3, 5], even: [2, 4] }

// Map.groupBy 返回一个 Map 对象,方便使用对象键进行分组。
const odd  = { oddtrue };
const even = { eventrue };
Map.groupBy(array, (num, index) => {
  return num % 2 === 0 ? even: odd;
});
// =>  Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }

该提案提供了两个方法:Object.groupByMap.groupBy。前者返回一个没有原型的对象,可以方便地进行解构操作,并且可以防止与全局 Object 属性发生意外冲突。后者返回一个普通的 Map 实例,可以对复杂键类型进行分组(比如复合键或元组)。

Promise.withResolvers

当手动创建一个 Promise 时,用户必须传递一个执行器回调函数,该函数接受两个参数:

  • resolve 函数,用于触发 Promise 的解决;
  • reject 函数,用于触发 Promise 的拒绝。

如果回调函数可以嵌入调用一个最终触发解决或拒绝的异步函数(例如注册事件监听器),则这种方式可以很好地工作。

const promise = new Promise((resolve, reject) => {
  asyncRequest(config, response => {
    const buffer = [];
    response.on('data', data => buffer.push(data));
    response.on('end', () => resolve(buffer));
    response.on('error', reason => reject(reason));
  });
});

然而,通常开发人员希望在实例化 Promise 后配置其解决和拒绝行为。目前,这需要一个繁琐的解决方法,从回调范围中提取 resolvereject 函数:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});
asyncRequest(config, response => {
  const buffer = [];
  response.on('callback-request', id => {
    promise.then(data => callback(id, data));
  });
  response.on('data', data => buffer.push(data));
  response.on('end', () => resolve(buffer));
  response.on('error', reason => reject(reason));
});

开发人员可能还有其他要求,需要将 resolve/reject 传递给多个调用方,因此必须以这种方式实现:

let resolve = () => { };
let reject = () => { };

function request(type, message) {
  if (socket) {
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });
    socket.emit(type, message);
    return promise;
  }

  return Promise.reject(new Error('Socket unavailable'));
}

socket.on('response', response => {
  if (response.status === 200) {
    resolve(response);
  }
  else {
    reject(new Error(response));
  }
});

socket.on('error', err => {
  reject(err);
});

Promise.withResolvers[2] 提案简单地在 Promise 构造函数中添加了一个静态方法,暂时称为 withResolvers,该方法返回一个 Promise,并方便地公开其解决和拒绝函数。

const { promise, resolve, reject } = Promise.withResolvers();

源阶段导入

无论是对于 JavaScript 还是 WebAssembly,都需要能够更紧密地定制模块的加载、链接和执行,超出了标准的宿主执行模型。

  • 对于 JavaScript,创建自定义加载器需要一种模块源类型,以便共享宿主的解析、执行、安全性和缓存语义。
  • 对于 WebAssembly,WebAssembly 模块的导入和导出通常需要进行自定义的检查和封装,以便正确设置,这通常需要手动的获取和实例化工作,在当前的宿主 ESM 集成提案中没有提供相应支持。

通过将语法模块源导入支持作为新的导入阶段,可以创建一个基础机制,将模块的静态、安全性和工具化优势从 ESM 集成扩展到这些动态实例化用例。

提案[3]允许ES模块从主机提供的编译后的模块源的反映表达式进行导入:

import source x from "<specifier>";

仅支持上述形式的导入,不支持命名导出和未绑定声明。

动态形式使用 import.<phase>

const x = await import.source("<specifier>");

通过将阶段作为显式语法的一部分,可以在静态上下文中静态区分全动态导入和仅用于源的导入(无需处理依赖项)。

处理时间区域规范化的变化

ECMAScript中的时间区域依赖于IANA时区数据库(TZDB)的标识符,如America/Los_Angeles或Asia/Tokyo。该提案旨在改善开发人员在TZDB中更改时间区域的规范标识符(例如从Europe/KievEurope/Kyiv)时的开发体验。

减少实现之间以及实现与规范之间的差异

  1. 已完成 - 简化处理时区标识符的抽象操作。
  2. 已完成 - 澄清规范以防止更多的分歧。
  3. 在 Temporal 广泛采用之前,帮助V8和WebKit更新13个过时的规范标识(如Asia/Calcutta,Europe/Kiev和Asia/Saigon),以免出现问题。
  4. 制定规范文本以减少实现之间的分歧。这一步需要在实现者和TG2(ECMA-402团队)之间找到共同点,讨论规范化应该如何工作。

减少标准化变化的影响

  1. 避免对链接进行可观察的跟随。如果标准化变化不会影响现有代码,那么未来的标准化变化就不太可能破坏Web。由于标准化是实现定义的,这个变化(或许会、也许不会;需要进一步研究)在Temporal第4阶段之后发布可能是安全的,但最好不要等太久。
Temporal.TimeZone.from('Asia/Calcutta');
// => Asia/Kolkata(Firefox上当前的Temporal行为)
// => Asia/Calcutta(建议:在将标识符返回给调用方时,不要遵循链接)
  1. 添加Temporal.TimeZone.prototype.equals方法。由于(5)会在创建TimeZone对象时停止标准化标识符,因此有一个直观的方法来判断两个 TimeZone 对象是否表示相同的时区。
// 更人性化的标准化相等性测试
Temporal.TimeZone.from('Asia/Calcutta').equals('Asia/Kolkata');
// => true

Stage 2

Time Zone Canonicalization[4]

JavaScript应用程序可能会变得非常庞大,以至于即使加载它们的初始化脚本,执行起来也会产生显著的性能开销。通常,这种情况发生在应用程序的生命周期较晚的阶段,往往需要进行大规模的改动以提高性能。加载性能是一个重要的改进领域,涉及预加载技术以避免瀑布效应,并使用动态导入进行模块的惰性加载。

尽管使用了这些技术解决了加载性能问题,但代码本身的编写方式仍会导致执行性能开销和CPU瓶颈在初始化过程中出现。

提案[5]是引入一种新的导入语法形式,它将始终返回一个命名空间对象。在使用时,模块及其依赖项不会被执行,但会完全加载到可以执行的状态,然后才会认为模块图已加载完成。只有当访问该模块的属性时,才会执行相应的操作。

该API将使用以下语法:

// 或使用自定义关键字: 
import defer * as yNamespace from "y";

Stage 1

DataView get/set Uint8Clamped 方法

现在只有其中 10 个具有DataView的 get/set 方法。

提案[6]旨在添加DataView.prototype.getUint8ClampedDataView.prototype.setUint8Clamped方法。

  • getUint8Clamped(offset: number): number:从指定的偏移量读取一个8位无符号整数(Uint8Clamped)值,并返回该值。
  • setUint8Clamped(offset: number, value: number): void:将一个8位无符号整数(Uint8Clamped)值写入到指定的偏移量。

可选链赋值

提案[7]建议在赋值运算符左侧添加对可选链的支持:a?.b = c。在实际开发中,经常需要对对象的属性进行赋值,但前提是该对象确实存在。

通常的做法是使用if语句来保护赋值操作:

if (obj) {
  obj.prop = value;
}

新语法和现有语法对比如下:

相关链接

[1]

提案: https://github.com/tc39/proposal-array-grouping

[2]

Promise.withResolvers: https://github.com/tc39/proposal-promise-with-resolvers

[3]

提案: https://github.com/tc39/proposal-source-phase-imports

[4]

Time Zone Canonicalization: https://github.com/tc39/proposal-canonical-tz

[5]

提案: https://github.com/tc39/proposal-defer-import-eval

[6]

提案: https://github.com/tc39/proposal-dataview-get-set-uint8clamped

[7]

提案: https://github.com/tc39/proposal-optional-chaining-assignment

往期推荐

推荐10个开源的前端低代码项目!

JavaScript日期时间操作完整指南!

前端新人入职必备清单,保姆级教程!

如何删除未使用的 CSS 代码?

TypeScript 的演进之路

10个开源微信小程序实战项目,yyds!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存