JavaScript 开发者在 ECMAScript 2024 中的新特性

原文信息: 查看原文查看原文

What’s New for JavaScript Developers in ECMAScript 2024

- Mary Branscombe

JavaScript 的 ECMAScript 标准继续以审慎的方式添加新的语言特性。今年新增了一些 API,这些 API 标准化了开发者手工编写或从第三方库导入的常见模式——包括一些特别针对库作者的——以及在字符串处理、正则表达式、多线程和 WebAssembly 互操作性方面的改进。

与此同时,评估提案的 TC39 委员会也在一些更大的提案上取得了进展,比如备受期待的 TemporalDecorators,它们可能准备好在 ECMAScript 2025 中使用,Ecma 副主席 Daniel Ehrenberg 告诉 The New Stack。

“回顾我们过去一年的工作,ECMAScript 2024 在某些方面与 ECMAScript 2023 类似,它看到了较小的特性;但与此同时,我们正在非常坚定地向这些大特性迈进。” 许多这些特性只需要“最后的润色”。

“你需要合理高效且频繁地从 JavaScript 端访问 WebAssembly 堆,因为在实际应用中,你不会有两者之间的通信。” — Daniel Ehrenberg,Ecma 副主席

实际上,自从今年 3 月 ECMAScript 2024 的完成特性提案被签署以来,准备在 7 月批准标准,至少有一个重要的提案——Set Methods——已经达到了第四阶段,准备在 2025 年使用。

使用 Promises 让开发者更快乐

尽管 promises 是在 ECMAScript 2015 中引入的一个强大的 JavaScript 特性,但 promise 构造函数使用的模式在 JavaScript 中其他地方并不常见,结果证明这并不是开发者想要编写代码的方式,Ehrenberg 解释道。“使用这些奇怪的习惯用法需要一些心智带宽。”

希望随着时间的推移,在 Web 平台上,足够的 API 会原生返回 promises 而不是回调,这样开发者就不需要经常使用 promise 构造函数。然而,现有的 API 并没有变得更符合人体工程学。

“这至少在每个项目中都会出现一次。几乎每个项目都在编写这个相同的小助手,所以它在语言中是那些真正让开发者感到快乐的 API 之一。” — Ashley Claymore,彭博软件工程师

相反,开发者们面临着一个繁琐的变通方法,许多库、框架和其他工具——从 React 到 TypeScript——都实现了不同版本的:在 jQuery 中作为延迟函数。“人们有这种样板模式,他们不得不一遍又一遍地写,他们调用 promise 构造函数,他们得到 resolve 和 reject 回调,他们将这些写入变量,然后他们不可避免地用它们做其他事情。这只是一个烦人代码段,”Ehrenberg 说。

在 ECMAScript 2015 之前实现 promises 的库通常涵盖了这一点,但该特性并没有进入语言;Chrome 短暂支持然后移除了一个类似的选项。但开发者仍然经常需要这个,以至于 Promise.withResolvers 提案在 ECMAScript 2023 确定后的十二个月内通过了整个 TC39 流程,直到今年语言更新的截止日期——这是一个如此不寻常的成就,以至于 TC-39 联合主席 Rob Palmer 称之为速跑。

“以前,当你创建一个 promise 时,你解决它并给它最终状态的方式只能在你构建 promise 的函数内部访问的 API,”Ehrenberg 继续说道。“Promise.withResolvers 为你提供了一种创建 promise 的方式,并直接给你访问这些解决函数的方式。”

你的代码中的其他函数可能依赖于 promise 是否已解决或已拒绝,或者你可能想要将函数传递给其他可以为你解决 promise 的东西,这反映了 promises 在现代 JavaScript 中用于编排的复杂方式,Ashley Claymore(一位在多个 TC39 提案上工作过的彭博软件工程师)建议。

“传统的创建 promise 的方式在处理小的异步任务时效果很好;拿着一些纯粹基于回调的或类似 promise 的东西,然后包装它,使其实际上成为一个 promise,”Claymore 说。“在我开始进行许多请求并需要用其他地方的 ID 将它们排列起来的地方,所以我把 promises 或 resolve 函数放在一个映射中,因为我正在编排许多不是基于 promise 的异步事物,你总是必须这样做。我需要拿出这些东西,因为我将它们发送到不同的地方。”

“这至少在每个项目中都会出现一次。几乎每个项目都在编写这个相同的小助手,所以它在语言中是那些真正让开发者感到快乐的 API 之一。”

对 promises 的其他改进则更遥远;Claymore 参与了一个提案,以简化在不使用数组的情况下 合并多个 promises ——这涉及到跟踪所有 promises 的顺序。“对于一两个或三个事物来说,这样做很好:在那之后,代码可能开始变得更难跟踪,”他说。“第五个是什么?你得数代码行,以确保你得到了正确的东西。”

拥有一个 Await 字典的 Promises 将允许开发者命名 promises:特别是当它们涵盖不同领域时——比如收集用户信息、数据库设置和可能在非常不同的时间返回的网络细节。这将不是一个难以开发的特性:延迟是决定它是否足够有用以进入语言,因为 TC39 委员会希望避免添加太多可能使新开发者感到困惑的小众特性。

保持兼容性和数据分类

对于 ECMAScript 2024 的第二个主要特性,使用 数组分组 来对对象进行分类:在其他语言中(包括 SQL)很常见,开发者经常为此导入 Lodash 用户空间库。

你可以传入不同的项目,并通过某个属性对它们进行分类,比如颜色。“结果是按 '这里有所有绿色的东西,这里有你的橙色的东西' 索引的键值字典,这个字典可以表示为对象或映射”,Palmer 解释道。如果你想对不仅仅是字符串和符号的键进行分组;如果你想同时提取多个数据值(称为解构),你需要一个对象。

“作为一个标准委员会,我们不应该要求他们承担冒着中断的风险,当我们已经知道某些事情风险很高时。” — Ehrenberg

这对于从对你的网站性能数据进行分组到按状态分组已解决的 promises 列表等都很有用,这是一个常见的使用 Promise.allSettled 的情况,Claymore 说。“你给它一个 promises 数组,它会等待所有 promises 结束,然后你会得到一个对象数组,说 '这个被拒绝了还是解决了?' 它们与你开始的顺序相同,但很常见的是,我想给所有成功并解决的 promises 一段代码,另一段代码给拒绝的 [promises]。”为此,你可以将 Promise.allSettled 的结果传递给 groupBy 按 promise 状态进行分组,这将分别分组所有已解决的 promises 和所有已拒绝的 promises。

构建新的分组功能也提供了一个关于 web 兼容性的有用教训。

Palmer 指出,Lodash 中的实用程序是开发者可以在 5-10 行代码中编写的功能。“但当你看到它们的使用频率时,它们被如此多的程序广泛使用,以至于在某个时候值得取出最常用的并放入平台中,这样人们就不必自己编写。” 它们中的一些现在已经变成了原生结构。

“这个功能在语言中对于试图不拥有大量依赖项同时仍然可以访问这些真正常见事物的项目来说是一个非常好的便利,”Claymore 同意道。“它们不是最难手写的,但手写它们并不好玩,它们可能会微妙地出错。”

不同寻常的是,新的 Map.groupBy 和 Object.groupBy 方法是静态方法,而不是数组方法,就像 Lodash 功能以前被添加到 JavaScript 中的方式一样。这是因为两次尝试将此功能作为数组方法添加都与已经在使用提案提出的两个名称的现有网站上的代码发生了冲突(以不同的方式),包括流行的 Sugar 库。

Palmer 警告说,这个问题可能会在 TC39 提案尝试向数组或实例方法添加新原型方法时再次出现。“每当你尝试思考任何合理的英语动词,你可能会添加,它似乎在互联网上的某个地方触发了 web 兼容性问题。”

理想情况下,良好的编码标准会避免这种情况,但将新特性添加到 JavaScript 中需要时间的部分原因是需要测试这些类型的问题,并在它们出现时解决它们。

“我们可以说的是,网络的最佳实践是用户不应该污染全局原型:人们不应该在他们的代码中向数组或原型添加属性,因为这可能导致 web 兼容性问题。但不管我们怎么说;这些网站已经在那里了,我们有责任不破坏它们。”

在浏览器中发布然后撤回实现会使添加新特性更加昂贵,Ehrenberg 补充道。“作为一个标准委员会,我们不应该要求他们承担冒着中断的风险,当我们已经知道某些事情风险很高时。”这意味着将来类似的提案可能会使用静态方法以避免问题。

更新 JavaScript 以更好地处理 Unicode

JavaScript 已经有了一个需要处理 Unicode 的 /u 标志的 regexp(在 ECMAScript 2015 中引入),但事实证明它有一些奇特之处和缺少的特性。新的 /v 标志 修复了其中一些(比如当你匹配时使用大写或小写字符得到不同的结果,即使你指定你不在乎大小写)并强制开发者转义特殊字符。它还允许你使用新的 unicodeSets 模式进行 更复杂的模式匹配和字符串操作,它让你可以命名 Unicode 集合,以便你可以引用 ASCII 字符集或表情符号字符集。

新选项将简化国际化,并使支持多样性特性更容易。/u 标志已经让你可以引用表情符号,但前提是它们只是一个字符——排除了组合多个字符以获得新表情符号或指定代表人的表情符号的性别或肤色的表情符号,甚至是一些国家国旗。

它还简化了像清理或验证输入这样的操作,通过添加更多的集合操作,包括交集和嵌套,使复杂的正则表达式更易读。“它增加了减法,所以你可以说,例如,‘所有 ASCII 字符’,但然后减去数字零到九,那将匹配比所有 ASCII 字符更窄的范围,”Palmer 解释道。你可以移除不可见字符或将表示为单词的数字转换为数字。

“对 Unicode 做出假设很容易,这是一个如此大的主题;世界上真正足够理解这些事情而不会犯错的人数非常少。” — Claymore

你还可以匹配字符串的各种属性,比如它们是用哪种脚本写的,这样你就可以找到像 π 这样的字符,并将其与另一种语言中的 p 区别对待。

你不能同时使用 /u 标志和 /v 标志,你可能会一直想使用 /v。Palmer 将选择描述为“/v 标志启用了 /u 标志的所有优点,并带有新特性和改进,但其中一些与 /u 标志向后不兼容。”

ECMAScript 2025 将为 regexp 添加另一个有用的改进:能够在 regexp 的不同分支中 使用相同的名称。目前,如果你正在编写一个正则表达式来匹配可以用多种方式表达的东西,比如日期中的年份可能是 /2024 或只是 /24,你不能在正则表达式的两个分支中使用‘year’,尽管只有一个分支可以匹配,所以你不得不说‘year1’和‘year2’或‘shortyear’和‘longyear’。

“现在我们说这不再是一个错误,你可以在正则表达式的多个部分给出相同的名称,只要它们在不同的分支上,并且只要只有一个可以被匹配,”Claymore 解释道。

ECMAScript 2024 中的另一个新特性通过确保代码使用 格式良好的 Unicode 字符串 来改进 Unicode 处理。

JavaScript 中的字符串在技术上是 UTF-16 编码的:实际上,JavaScript(像许多其他语言一样)并不强制这些编码是有效的 UTF-16 即使这对于 URI API 和 WebAssembly 组件模型等来说很重要。“网络平台上有各种 API 需要格式良好的字符串,如果它们得到错误的数据,它们可能会抛出错误或默默地替换字符串,”Palmer 解释道。

因为有效的 JavaScript 代码可能使用无效 UTF 序列的字符串,开发者需要检查的方法。新的 isWellFormed 方法检查 JavaScript 字符串是否正确编码;如果不是,新的 .toWellFormed 方法通过用 0xFFFD 替换字符 替换任何未正确编码的内容来修复字符串。

虽然经验丰富的 Unicode 开发者已经可以编写这样的检查,但“这很容易出错,”Claymore 指出。“对 Unicode 做出假设很容易,这是一个如此大的主题;世界上真正足够理解这些事情而不会犯错的人数非常少。这鼓励人们走向成功的坑,而不是试图手写这些事情并因为不知道所有边缘情况而犯错。”

将此纳入语言本身甚至可能证明更有效率,Palmer 建议。“潜在的,将此委托给 JavaScript 引擎的一个优势是,它可能能够找到更快的方法来执行此检查。你可以想象,例如,它可能只是缓存每个字符串的单个位信息,以说‘我已经检查过了,这个字符串不好’,以便每次你将它传递到需要好字符串的地方时,它不需要再次遍历每个字符进行检查,而只是查看那个位。”

添加异步代码的锁

“在主线程上,你为用户提供交互性,这是网络的一条规则:你不得暂停主线程!” — Rob Palmer,TC-39 联合主席

JavaScript 从技术上讲是一种支持多线程和异步代码的单线程语言。这是因为除了拥有与提供用户界面的主线程隔离的 Web 工作者和服务工作者之外,网络的本质通常是你经常在等待来自网络或操作系统的某些东西,所以主线程可以运行其他代码。

JavaScript 中用于管理这一点的工具随着 ECMAScript 2024 中的一个新选项变得更加强大,Atomics.waitAsync

“如果你想在 JavaScript 中进行多线程,我们有 Web 工作者,你可以启动另一个线程,最初的模型基于消息传递,这是很好且安全的,”Palmer 解释道。“但对于想要更快的人,当这成为瓶颈时,共享内存是让这些线程在共享数据上合作的更原始、更低级别的方式,SharedArrayBuffer 很早就被引入以允许这种内存共享。而当你拥有一个带有共享内存的多线程系统时,你需要锁来确保你有有序的操作。”

“当你等待时,你锁定了线程,它无法取得任何进展。这在工作者线程上是可以的。在主线程上,你为用户提供交互性,这是网络的一条规则:你不得暂停主线程!”

Async 避免了阻塞主线程,因为它可以继续进行任何其他准备好的任务,比如从网络加载数据,Atomics.wait API 提供了事件等待,当你不在主线程上时。但有时你确实想让主线程等待。

“即使你不在主线程上,你大多数时候也不应该阻塞,”Ehrenberg 警告,同时指出“对于游戏引擎来说,允许它们在不在主线程上时阻塞是很重要的,这样它们就可以在 C++ 代码库中重现它们能够做到的事情。”

需要这个的开发者为此创建了变通方法,再次使用消息传递,但这有一些开销并减慢了事情的速度。Atomics.waitAsync 可以在主线程上使用,并提供了一种等待锁的一级方式。“关键是它不会使主线程停滞,”Palmer 说。

“如果你调用它,锁还没有准备好,它会给你一个备用的 promise,这样你就可以使用常规的 async.await 并像对待任何其他 promise 一样对待它。这解决了如何在锁上进行高性能访问操作的问题。”

另一个仍在开发中的提案采取了稍微不同的方法来等待主线程,并且对于使用 Emscripten 编写的多线程应用程序更加高效将是有用的。Atomics.pause 承诺‘微等待’,可以从主线程或工作者线程调用。“它会阻塞,但它的阻塞时间有限,”Claymore 告诉我们。

大多数 JavaScript 开发者都不会直接使用这些选项,Palmer 指出:“写线程代码非常困难。”但互斥体库可能会依赖它,也可能用于编译到 WebAssembly 的工具。

“即使我们不直接使用它,我们都可以从中受益。”

更容易的 WebAssembly 集成

另一个提案是为以前在 DOM API 中处理或对 WebAssembly 字节码可用但对 JavaScript 不可用的特性添加 JavaScript API,可调整大小的数组缓冲区

WebAssembly 和 JavaScript 程序需要共享内存。“你需要合理高效且频繁地从 JavaScript 端访问 WebAssembly 堆,因为在实际应用中,你不会有两者之间的通信;它们主要是通过堆共享它们的信息,”Ehrenberg 解释道。

“如果你有一个像 Emscripten 这样的 WebAssembly 工具链,这意味着它可以在不创建包装对象的情况下做到这一点。” — Palmer

WebAssembly 内存如果需要可以增长:但如果它这样做了,你想要从 JavaScript 访问它,那意味着你要分离你用于处理内存中二进制数据的 ArrayBuffer,并在下次需要访问堆时在底层 ArrayBuffer 上构建一个新的 TypedArray。这是额外的工作,会在 32 位系统上分割内存空间。

现在你可以创建一种新的可调整大小的数组缓冲区,所以它可以在不需要分离的情况下增长或缩小。

“如果你有一个像 Emscripten 这样的 WebAssembly 工具链,这意味着它可以在不创建包装对象的情况下做到这一点,这只会增加效率,”Palmer补充道。同样,尽管很少有 JavaScript 开发者会直接使用它,但库可能会使用可调整大小的数组以提高效率,并且不再需要绕过语言中缺失的部分来实现它——通过减少需要下载的代码量,使它们更小且加载速度更快。

开发者必须明确选择这个,因为数组缓冲区和类型化数组是黑客尝试攻击浏览器的最常用方式,使现有的 ArrayBuffer 可调整大小将意味着更改大量经过广泛测试和加固的代码,可能创建错误和漏洞。

“那个分离操作最初是为了启用在工作者之间传输而创建的,”Ehrenberg 解释道。“当你通过 POST 消息传递一个 ArrayBuffer 以将其传输到另一个工作者时,所有权转移,所有封闭在那个 ArrayBuffer 上的类型化数组都会分离。”一个配套的提案允许开发者 传输数组缓冲区的所有权,以便它们不能被篡改。

“如果你将它传递给一个函数,接收者可以使用这个传输函数来获得所有权,所以任何曾经拥有它的处理者都不再拥有它,”Palmer 解释道。“这有利于完整性。”

除了作为 ECMAScript 2024 的一部分,可调整大小的数组缓冲区也已经集成到 WebAssembly JS API 中;Ehrenberg 将此作为 web 生态系统中不同社区之间协作良好的一个例子。

“这些是跨越多个不同标准委员会的努力,需要它们之间的明确合作。这可能很复杂,因为你需要让很多人参与进来,但最终,我认为它会导致一个好的设计过程。你得到了很多审查。”

分享于 2024-08-13

访问量 79

预览图片