本文是对文章《ECMAScript 2024 新特性》的中文翻译,原文来自 Paweł Grzybek: What's new in ECMAScript 2024

ECMAScript 2024 语言规范的最终版本已于6月26日获得批准。新的 JavaScript 特性列表现已确认,为了保持我每年的传统,我将为你和未来的自己发布这篇年度回顾。

一些实用的功能已经成为规范的一部分,但有些则更加微妙、底层,并且超出了普通应用开发者常用的工具集。我做了功课,在这篇文章中,我将向那些很少深入复杂正则表达式、Unicode字符编码和缓冲区操作领域的人解释它们。


由 Guy Bedford, Bradley Farias, Michael Ficarra 提出的 Well-Formed Unicode Strings

JavaScript 中的字符串由 UTF-16 代码点序列表示。名称中的“16”表示用于存储代码点的位数,提供 65536 种可能的组合(2^16)。这个数量足以存储拉丁文、希腊文、西里尔文和东亚字母表中的字符,但不足以存储中文、日文和韩文的表意文字或表情符号。额外的字符以16位代码单元对的形式存储。

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

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

首尾代理被限定在不用于编码单代码单元字符的代码单元范围内,以避免歧义。如果一对中缺少首尾代码单元或它们的顺序颠倒,我们就会处理一个“孤立代理”,整个字符串就是“格式不正确”的。为了让字符串“格式正确”,它不能包含孤立代理。

Well-Formed Unicode Strings 提案引入了 String.prototype.isWellFormed() 方法来验证字符串是否格式正确。此外,它还附带了一个 String.prototype.toWellFormed() 辅助方法,用替换字符(U+FFFD,)替换所有孤立代理。

'\ud83e\udd51'
// 🥑

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

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

'\ud83e'.toWellFormed()
// 

由 Shu-yu Guo 和 Lars T Hansen 提出的 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);
}

由 Markus Scherer 和 Mathias Bynens 提出的 RegExp v 标志与集合表示法 + 字符串属性

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

// `u` 和 `v` 模式相似,但不能组合
const pattern = /./vu;
// SyntaxError: Invalid regular expression: invalid flags

检查 Unicode 字符串属性的子集

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

const patternMath = /\p{Math}/u;
const patternDash = /\p{Dash}/u;
const patternHex = /\p{ASCII_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

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

const pattern = /\p{RGI_Emoji}/u
// SyntaxError: Invalid regular expression: /\p{RGI_Emoji}/u: Invalid property name
const pattern = /\p{RGI_Emoji}/v;

// 单代码点表情符号
pattern.test('😀') // true

// 多代码点表情符号
pattern.test('🫶🏾') // true

减法/交集/并集匹配

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

// 匹配所有表情符号,除了便便堆
const pattern = /[\p{RGI_Emoji}--\q{💩}]/v;

pattern.test('😜') // true
pattern.test('💩') // false
// 只有大写字母,十六进制数字安全的字符
const pattern = /[\p{Uppercase}&&\p{ASCII_Hex_Digit}]/v;

pattern.test('f') // true
pattern.test('F') // false
// 只有甜瓜和浆果
const pattern = /^[\q{🍈|🍉|🍓|🫐}]$/v;

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

改进的不区分大小写

u 模式中的不区分大小写检查工作方式令人困惑。针对特定大小写组(Lowercase_LetterUppercase_Letter)的反向模式,当忽略大小写标志(i)启用时,不会产生直观的结果。新的 v 标志使结果更加可预测,这也是这两个标志不能组合的原因。

由 Shu-yu Guo 提出的就地可调整大小和可增长的 ArrayBuffer

JavaScript 中的 ArrayBuffer 对象是表示二进制数据缓冲区的一种方式。在 ECMAScript 2024 之前,调整 ArrayBuffers 的大小是一个繁琐的过程,需要创建一个新的缓冲区并将数据从一个缓冲区移动到另一个缓冲区。多亏了“就地可调整大小和可增长 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

由 Shu-yu Guo, Jordan Harband 和 Yagiz Nizipli 提出的 ArrayBuffer 传输

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

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

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

由 Justin Ridgewell 和 Jordan Harband 提出的数组分组

感谢数组分组提案,一个由 Lodash、Ramda 和其他人推广的流行的 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 }
//   ]
// }

由 Peter Klecha 提出的 Promise.withResolvers

Promise.withResolvers 提案为语言增加了延迟承诺,这是一个以前由 jQuery、bluebird 和许多其他库实现的流行模式。你可以使用它来避免在承诺执行器中嵌套,尽管当你需要将 resolve 或 reject 传递给多个调用者时,它特别有用。处理流或基于事件的系统是一个很好的用例。

看看这个 createEventsAggregator 示例它来自我几个月前发表的“使用 Promise.withResolvers 的延迟 JavaScript 承诺”。它返回一个 add 方法来推送一个新事件和一个 abort 方法来取消聚合。最重要的是,它返回一个 events 承诺,当它达到事件计数限制时解析,或者当 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年的全部内容。我们明年见 👋

看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~

专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)

南城FE
2.2k 声望579 粉丝