译:ECMAScript 2024 中的新内容

原文:https://pawelgrzybek.com/whats-new-in-ecmascript-2024/
作者:Paweł Grzybek
译者:ChatGPT 4 Turbo

编者注:这些,isWellFormed、Atomics.waitAsync、RegExp v Flag、ArrayBuffer 可调整可转移、数组 groupBy(居然放 Object 上…)、Promise.withResolvers。

ECMAScript 2024 语言规范的最终版本已于 6 月 26 日 获批准。新的 JavaScript 特性列表现已确认,为了保持我每年的传统,我为你和未来的我发布了这篇年度回顾。对过去几年感到好奇的朋友,这里有之前几年的帖子:20232022202120202019201820172016

一些实用的功能成为了规范的一部分,但有些则更加细致、底层,不在常规应用制作者(像我)常用工具集之外。我已经做了我的功课,在这篇文章中,我将向那些很少深入研究复杂的正则表达式、Unicode 字符编码和缓冲区操作的人解释它们。


格式良好的 Unicode 字符串

JavaScript 中的字符串由一系列 UTF-16 码点表示。名称中的 16 代表用于存储码点的比特数,它提供了 65536 种可能的组合(2^16)。这个数量足以存储拉丁文、希腊文、西里尔文和东亚字母的字符,但不足以存储如中文、日文、韩文表意文字或表情符号之类的内容。额外的字符存储在一对 16 位码元组中,称为替代对。

'a'.length
// 1
'a'.split('')
// [ 'a' ]

'🥑'.length
// 2
'🥑'.split('')
//[ '\ud83e', '\udd51' ] 👈 替代对

首尾替代对被限定在一个不用于编码单码元字符的码元范围内,以避免歧义。如果一对替代对缺少一个首部或尾部码元,或者它们的顺序被颠倒,我们处理的是“孤立替代”,整个字符串是“格式不良的”。对于字符串来说,“格式良好”,它必须不含有孤立的替代对。

Unicode 字符串格式良好性提案 引入了一个 String.prototype.isWellFormed() 方法来验证字符串是否格式良好。此外,它带有一个 String.prototype.toWellFormed() 辅助方法,该方法将所有孤立的替代码元替换为替换字符(U+FFFD,�)。

'\ud83e\udd51'
// 🥑

'\ud83e\udd51'.isWellFormed()
// true

'\ud83e'.isWellFormed() // 没有尾随替代码
// false

'\ud83e'.toWellFormed()
// �

ECMAScript 异步原子等待

Workers 实现了 JavaScript 中的多线程功能。SharedArrayBuffer 是一个底层 API,它允许我们对主线程和 Workers(代理)之间的共享内存进行操作。Atomics 对象上的一组静态方法帮助我们避免读写操作之间的冲突。

常见的做法是使一个 worker 休眠并在需要时唤醒它。我们结合使用 Atomics.wait()Atomics.notify() 方法来实现这一点。然而,这可能因为 Atomics.wait() 是一个同步 API 而受到限制,并且不能在主线程上使用。

异步原子等待提案 提供了一种在主线程上异步执行此操作的方法,最重要的是,它能在主线程上完成。

// 主线程
let i32a = null;

const w = new Worker("worker.js");
w.onmessage = function (env) {
  i32a = env.data;
};

setTimeout(() => {
  Atomics.store(i32a, 0, 1);
  Atomics.notify(i32a, 0);
}, 1000);
// worker 线程
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const i32a = new Int32Array(sab);
postMessage(i32a);

const wait = Atomics.waitAsync(i32a, 0, 0);

// { async: false; value: "not-equal" | "timed-out"; }
// 或
// { async: true; value: Promise<"ok" | "timed-out">; }

if (wait.async) {
  wait.value.then((value) => console.log(value));
} else {
  console.log(wait.value);
}

RegExp v 标志与集合表示法及字符串属性

新的 RegExp v 标志 类似于 2015 年新增的支持 unicode 的正则表达式(u 标志),但功能更强大。由于与 u 标志的相似性和一些不兼容性,这两个标志不能组合使用。新的 v 正则模式启用了三个功能:针对 Unicode 字符串属性 的子集进行检查,执行减法/交集/并集匹配,并改进了大小写不敏感匹配。

// `u` 和 `v` 模式类似,但它们不能组合
const pattern = /./vu;
// SyntaxError: 无效的正则表达式:无效的标志

针对 Unicode 字符串属性子集的检查

Unicode 标准定义了一个简化正则表达式模式的属性列表。例如,/\p{Math}/u 用于检查数学运算符,/\p{Dash}/u 用于检查连字符标点符号或 /\p{Hex_Digit}/u 用于检查用于表示十六进制数字的符号。

const patternMath = /\p{Math}/u;
const patternDash = /\p{Dash}/u;
const patternHex = /\p{Hex_Digit}/u;

patternMath.test('+'); // true
patternMath.test('z'); // false

patternDash.test('-'); // true
patternDash.test('z'); // false

patternHex.test('f'); // true
patternHex.test('z'); // false

大多数属性适用于单个代码点,但有一些(目前主要是与 emoji 相关的)适用于字符串(多个代码点)。例如 Basic_EmojiRGI_EmojiRGI_Emoji_Flag_Sequence。这些是 u 模式不支持的类型,尽管有一些讨论希望改变这一点。幸运的是,v 模式的一个特性就是能够对 Unicode 字符串属性进行检查。

const pattern = /\p{RGI_Emoji}/u
// SyntaxError: 无效的正则表达式: /\p{RGI_Emoji}/u:无效的属性名称
const pattern = /\p{RGI_Emoji}/v;

// 单个代码点 emoji
pattern.test('😀') // true

// 多个代码点的 emoji
pattern.test('🫶🏾') // true

减法/交集/并集匹配

v 模式的另一个特性是属性的字符串的减法(--)、交集(&&)和并集。在字符类中(多字符字符串)内使用新的 \q 进行字符串字面量的标记也值得注意。

// 匹配所有 emoji 除了一堆 poo

const pattern = /[\p{RGI_Emoji}--\q{💩}]/v;

pattern.test('😜') // true
pattern.test('💩') // false
// 仅匹配大写,十六进制安全字符

const pattern = /[\p{Uppercase}&&\p{Hex_Digit}]/v;

pattern.test('f') // true
pattern.test('F') // false
// 仅匹配瓜类和浆果

const pattern = /^[\q{🍈|🍉|🍓|🫐}]$/v;

pattern.test('🥑') // false
pattern.test('🫐') // true

改进的大小写不敏感性

u 模式下如何检查大小写敏感性令人困惑。启用了忽略大小写标志(i)的反向模式针对特定的大小写组(Lowercase_LetterUppercase_Letter)不会产生直观的结果。新的 v 标志使结果变得更加可预测,这就是为什么这两个标志不能结合使用的原因。

原地可调整大小且可增长的 ArrayBuffer

JavaScript 中的 ArrayBuffer 对象是表示二进制数据缓冲区的一种方式。在 ECMAScript 2024 之前,调整 ArrayBuffer 的大小是一个繁琐的过程,需要创建一个新的并将数据从一个移动到另一个。多亏了“原地可调整大小且可增长的 ArrayBuffer 提案”,我们有了一种使用 options.maxByteLength 属性定义可增长缓冲区并通过调用 resize() 方法来调整其大小的原生方式。

const buffer = new ArrayBuffer(8, { maxByteLength: 16 });

buffer.resizable; // true
buffer.byteLength; // 8
buffer.maxByteLength; // 16

buffer.resize(16);

buffer.byteLength; // 16
buffer.maxByteLength; // 16

ArrayBuffer 转移

随着 ArrayBuffer 的新调整大小能力,arrayBuffer.prototype.transfer 及相关提案 增加了转移所有权的功能。 transfer()transferToFixedLength() 方法允许我们根据目的地重新定位字节。新的 detached getter 是一种检查已释放缓冲区的新原生解决方案。

const buffer = new ArrayBuffer();
buffer.detached; // false

const newBuffer = buffer.transfer();
buffer.detached; // true

数组分组

感谢 数组分组提案,一个受到 LodashRamda 等欢迎的 groupBy 方法现在已成为 ECMAScript 的一部分。最初的想法是将其实现为 Array.prototype.groupBy,这与常用的 Sugar 工具发生了冲突。它作为一个 Object.groupBy / Map.groupBy 静态方法实现。

const langs = [
  { name: "Rust", compiled: true, released: 2015 },
  { name: "Go", compiled: true, released: 2009 },
  { name: "JavaScript", compiled: false, released: 1995 },
  { name: "Python", compiled: false, released: 1991 },
];

const callback = ({ compiled }) => (compiled ? "compiled" : "interpreted");

const langsByType = Object.groupBy(langs, callback);

console.log({ langsByType });
// {
//   compiled: [
//     { name: "Rust", compiled: true, released: 2015 },
//     { name: "Go", compiled: true, released: 2009 }
//   ],
//   interpreted: [
//     { name: "JavaScript", compiled: false, released: 1995 },
//     { name: "Python", compiled: false, released: 1991 }
//   ]
// }

Promise.withResolvers

The Promise.withResolvers proposal 向语言中添加了延迟的 promises,这是一个以前被 jQuerybluebirdp-defer 和许多其他库实现的受欢迎模式。你可以使用它来避免在 promise 执行器中嵌套,尽管当你需要将 resolve 或 reject 传递给多个调用者时,它会更加出色。在流或基于事件的系统中工作是一个很好的使用案例。

看看这个例子,一个 createEventsAggregator,取自“使用 Promise.withResolvers 延迟 JavaScript promises”,我几个月前发布了这篇文章。它返回一个 add 方法来推送一个新事件和一个 abort 方法来取消聚合。最重要的是,它返回一个 events promise,当它达到 eventsCount 限制时解决,或者当触发 abort 时拒绝。

function createEventsAggregator(eventsCount) {
  const events = [];
  const { promise, resolve, reject } = Promise.withResolvers();

  return {
    add: (event) => {
      if (events.length < eventsCount) events.push(event);
      if (events.length === eventsCount) resolve(events);
    },
    abort: () => reject("Events aggregation aborted."),
    events: promise,
  };
}
const eventsAggregator = createEventsAggregator(3);

eventsAggregator.events
  .then((events) => console.log("Resolved:", events))
  .catch((reason) => console.error("Rejected:", reason));

eventsAggregator.add("event-one");
eventsAggregator.add("event-two");
eventsAggregator.add("event-three");

// Resolved: [ "event-one", "event-two", "event-three" ]

就是这样了,2024 年。我们明年再见 👋