17611538698
webmaster@21cto.com

ECMAScript 2024 为 JS 开发者带来哪些新功能?

前端 0 1018 2024-07-31 07:36:43

图片

导读:本篇文章分析了2024年度最新的 JavaScript 版本,其中有虽然小但有用的功能,包括更容易的 WebAssembly 集成等新特性。

JavaScript 的 ECMAScript 标准继续以固有的方式添加新的语言功能。今年,有多种 API 可以标准化开发人员增强或从第三方库导入的常见模式(包括一些专门针对库作者的模式),此外还有字符串处理、正则表达式、多线程和 WebAssembly 互操作性方面的改进。

Ecma 的技术副总裁Daniel Ehrenberg 表示道,负责评估提案的 TC39 委员会也在一些更大的提案上取得了进展,比如期待已久的Temporal和Decorators,它们可能为 ECMAScript 2025 做好准备。

“回顾我们过去一年所做的事情,ECMAScript 2024 与 ECMAScript 2023 有点相似,因为它包含一些较小的功能;但与此同时,我们正在大力推进这些大功能。其中许多只需要最后的收尾工作”。

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


—— Daniel Ehrenberg,Ecma 技术副总裁

自今年 3 月 ECMAScript 2024 的完整功能提案签署以来,准备在 7 月获得标准批准,有一项重要提案——Set方法——已经进入第四阶段,为 2025 年做好准备。

使用 Promises 让开发者更开心


尽管Promise是ECMAScript 2015中引入的一项强大的 JavaScript 功能,但 Promise 构造函数使用的模式在 JavaScript 中并不常见,而且事实证明这不是开发人员想要编写代码的方式,Ehrenberg 解释道说“使用这些奇怪的习语需要一些脑力。”


人们希望随着时间的推移,比如在 Web 平台上,有足够多的 API 会原生返回 Promise 而不是回调,这样开发人员就 敢 不需要经常使用 Promise 构造函数了。


然而,现有的 API 并没有变得更符合人体工学。

“它在每个项目中至少出现一次。几乎每个项目都在编写这个小助手,因此它成为语言中真正优秀的开发人员幸福 API 之一。”


– Ashley Claymore,彭博软件工程师

相反,开发人员只能采用繁琐的解决方法,许多库、框架和其他工具(从 React 到 TypeScript)都实现了不同的版本:它在 jQuery 中作为延迟函数。

“人们有这种他们必须一遍又一遍编写的样板模式,他们调用承诺构造函数,他们获得解析和拒绝回调,他们将这些回调写入变量,然后他们不可避免地用它们做着其他事情。这只是一段令人讨厌的代码,”Ehrenberg说。

ECMAScript 2015 之前实现承诺的库通常会涵盖这一点,但该功能并未融入语言中。

Chrome 短暂支持了类似的选项,然后删除了它。但开发者仍然经常需要它,以至于 Promise.withResolvers添加静态方法的提案在 ECMAScript 2023 最终确定和今年语言更新截止日期之间的十二个月内通过了整个 TC39 流程。

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

你代码中的其他函数可能取决于 Promise 是否被解决或被拒绝,或者你可能希望将函数传递给可以为你解决 Promise 的其他函数,这反映了 Promise 在现代 JavaScript 中用于编排的复杂方式,Ashley Claymore(一位曾参与多个 TC39 提案的彭博软件工程师)建议道。

“创建Promise在异步小任务时效果很好;将纯粹基于回调或类似Promise的东西打包起来,这样它实际上就是一个Promise,”Claymore 这样说。“在任何代码中,当我开始执行大量请求,并需要将它们与来自其他地方的 ID 对齐时,我会将Promise或解析函数放在映射中,因为我正在编排大量不基于Promise的异步事物,必须要这样做。然后我需要将这些东西取出来,将它们发送到不同的地方。”

“它在每个项目中至少出现一次。几乎每个项目都在编写这个小助手,因此它成为语言中真正优秀的,让开发人员幸福的API 之一。”

Promise 的其他改进则更加深入;Claymore 参与了一项提案,该提案旨在简化多个Promise的组合,而无需使用数组——这涉及到跟踪所有Promise的顺序。“对于一、二或三件事来说,这很好:在那之后,跟踪代码就会变得越来越困难,”他说。“第五件事是什么?你要计算代码行数,以确保你得到了正确的结果。”

当有一个 Await Promises 字典可以让开发人员命名Promise:当它们涵盖不同领域时尤其有用——例如收集用户信息、数据库设置和可能在非常不同的时间返回的网络详细信息时。

这并不是一个难以开发的功能:延迟是决定它是否足够有用以包含在语言中,因为 TC39 委员会希望避免添加太多可能让新开发人员感到困惑的小众功能。

保持兼容性,并对数据进行分类


下面介绍 ECMAScript 2024 的第二个主要特性,它是一种使用数组分组将对象分类的新方法:这在其他语言(包括 SQL)中都很常见,开发人员经常为此导入Lodash用户空间库。


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

“作为标准委员会,我们不应该要求他们承担停机风险,因为我们已经知道某些事情的风险很高。”


– Ehrenberg

Claymore 表示说,从存储网站性能数据到按状态,对已解决的Promise列表进行分组,这是Promise.allSettled 的常见用途,这非常有用。

“您给它一个Promise数组,它会等待所有Promise都解决,然后会得到一个对象数组,它会说‘这是被拒绝还是解决了?’它们的顺序与您开始时的顺序相同,但很常见的是,我想用一段代码给出所有成功和解决的Promise,而另一段代码给出被拒绝的 [Promise]。”为此,可以将 Promise.allSettled 的结果传递给 groupBy,以按Promise状态分组,这会将所有已解决的承诺和所有被拒绝的承诺进行不同且清晰的分组。

构建新的分组功能还提供了有关网络兼容性的有用经验。

Palmer 指出,Lodash 中的实用程序是开发人员可以用 5-10 行代码编写的功能。“但当你看到它们的使用频率时,你会发现它们被如此多的程序广泛使用,以至于在某些时候,值得将使用率最高的程序放在平台中,这样人们就不必自己编写了。”其中许多现在已成为本地构造。

“语言中的这项功能确实为那些试图避免大量依赖关系但又能访问这些非常常见内容的项目提供了极大的便利,”Claymore 表示同意。“手写这些内容并不是最难的,但手写重写却很无趣,而且很容易出错。”

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

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

理想情况下,良好的编码标准可以避免这种情况,但向 JavaScript 添加新功能需要时间的部分原因是需要针对这些问题进行测试,并在出现问题时解决它们。

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

Palmer 补充道,在浏览器中发布,然后撤回实施会使添加新功能的成本更高。“作为标准委员会,我们不应该要求开发者们承担停机风险的成本,因为我们已经知道某些事情风险很高。”这意味着类似的提案将来可能会更多地使用静态方法来避免此类问题。

更新 JavaScript 以实现更好的 Unicode 处理


大家知道,JavaScript 已经有一个 /u 标志,用于需要处理 Unicode 的正则表达式(在 ECMAScript 2015 中引入),但事实证明它有一些奇怪之处和缺失的功能。


新的/v 标志修复了其中一些问题(例如,如果在匹配时使用大写或小写字符,即使您指定不关心大小写,也会得到不同的结果)并强制开发人员转义特殊字符。


它还允许人们使用新的 unicodeSets 模式进行更复杂的模式匹配和字符串操作,该模式允许您命名 Unicode 集,以便可以引用 ASCII 字符集或表情符号字符集。


新的选项将简化国际化,并使其更容易支持多样性功能。/u 标志已经允许您引用表情符号,但前提是它们只是一个字符 — 不包括将多个字符组合在一起以获得新表情符号或指定代表人的表情符号的性别或肤色的表情符号,甚至一些国家的国旗。


它还通过添加更多集合运算(包括交集和嵌套)简化了诸如清理或验证输入之类的操作,使复杂的正则表达式更具可读性。


“它添加了减法,因此您可以说,例如,‘所有 ASCII 字符’,然后减去 0 到 9 的数字,这样匹配的范围就会比所有 ASCII 字符都窄,”Palmer 解释道。


你可以删除不可见的字符,或者将以单词表示的数字转换为数字。

“人们很容易对 Unicode 做出假设,而且这是一个很大的话题;世界上能够很好地理解这些事情而不会犯错误的人非常少。”


– Claymore

你还可以匹配字符串的各种属性,例如它们所用的脚本,以便可以找到像 π 这样的字符,并将它们与其他语言中的 p 区别对待。

但是,开发者不能同时使用 /u 标志和 /v 标志,但是你可以一直使用 /v。Palmer 将此选择描述为“/v 标志启用了 /u 标志的所有优点,并增加了新功能和改进,但其中一些功能与 /u 标志并不是向后兼容。”

此外,ECMAScript 2025 将为正则表达式添加另一项有用的改进:

能够在正则表达式的不同分支中使用相同的名称。目前,如果你编写正则表达式来匹配可能以多种方式表达的内容,例如日期中的年份可能是 /2024 或只是 /24,则不能在正则表达式的两个分支中使用“year”,即使只有一个分支可以匹配,因此必须写成“year1”和“year2”或“shortyear”和“longyear”。

“现在我们认为这不再是一个错误,你可以让正则表达式的多个部分具有相同的名字,只要它们位于不同的分支上,并且只有其中一个可以匹配,”Claymore 这样解释说。

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

从技术上讲,JavaScript 中的字符串就是采用 UTF-16 编码的。实际上,JavaScript(与许多其他语言一样)并不强制要求这些编码是有效的 UTF-16,尽管这对于 URI API 和 WebAssembly 组件模型等来说很重要。

“Web 平台中有各种 API 需要格式正确的字符串,如果它们收到错误的数据,它们可能会抛出错误或默默替换字符串,”Palmer 解释说。

由于有效的 JavaScript 代码可能会使用无效的 UTF 序列字符串,因此开发人员需要一些方法来检查这种情况。

新的 isWellFormed 方法检查 JavaScript 字符串是否正确编码;如果没有,新的 .toWellFormed 方法会通过使用 0xFFFD 替换字符 � ,以替换任何未正确编码的内容来修复字符串。

尽管经验丰富的 Unicode 开发人员已经为此编写了相关检查,但仍然“很容易出错,”Claymore 指出。“很容易对 Unicode 做出假设,这是一个很大的话题;世界上真正正确理解这些事情而不会犯错误的人非常少。这鼓励人们陷入成功的陷阱,而不是尝试手动完成这些事情,并因为不了解所有的边缘情况而犯错误。”

Palmer 建议,如果语言本身具备此功能,效率一定会更高。“将此功能委托给 JavaScript 引擎的一个潜在优势是,它可能会找到更快的方法来执行此检查。例如,你可以想象,它可能只会在每个字符串中缓存一个位信息,以表示‘我已经检查过了,这个字符串不好’,这样每次你将其传递给需要好字符串的地方时,它就不需要遍历每个字符再次检查,而只需查看那一个位即可。”

使用异步代码添加锁

“在主线程上,你为用户提供交互,这是网络规则之一:你不得停止主线程!”


– Rob Palmer,TC-39 联合主席

从技术上讲,JavaScript 是一种支持多线程和异步代码的单线程语言。这是因为除了 Web Worker 和Service Worker 与提供用户界面的主线程隔离之外,Web 经常需要等待网络或操作系统的某些操作,因此主线程还可以运行其他代码。

随着 ECMAScript 2024 中新选项Atomics.waitAsync 的推出,JavaScript 中用于管理此问题的工具变得更加强大。

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

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

Async 避免阻塞主线程,因为它可以继续执行任何其他准备就绪的任务,例如加载来自网络的数据,并且当您不在主线程上时,Atomics.wait API 提供基于事件的等待。但有时您确实希望主线程等待某事。

“即使你不在主线程上,你也不应该在大多数时间里处于阻塞状态,”Ehrenberg 警告说,同时指出“当游戏引擎不在主线程上时,允许其阻塞是很重要的,这样才能够重现它们在 C++ 代码库中所能做的事情。”

需要此功能的开发人员已经创建了解决方法,同样使用消息传递,但这会产生一些开销并减慢速度。Atomics.waitAsync 可以在主线程上使用,并提供一种一流的等待锁的方式。“关键是它不会拖延主线程,”Palmer 说。

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

另一项仍在开发中的提案采用了略有不同的主线程等待方法,有助于提高用 Emscripten 编写的多线程应用程序的效率。Atomics.pause承诺可以从主线程或工作线程调用“微等待”。Claymore 告诉我们:“它确实会阻塞,但阻塞时间有限。”

Palmer 指出,大多数 JavaScript 开发人员不会直接使用这两种选项:

“编写线程代码非常困难。互斥库可能会依赖它,编译为 WebAssembly 的工具也可能依赖它。

即使我们没有直接使用它,但都可以从中受益。”

更简单的 WebAssembly 集成


另一项提议是为以前在 DOM API 中处理或可用于 WebAssembly 字节码(但不适用于 JavaScript)的功能添加 JavaScript API,也就是可调整大小的数组缓冲区。


WebAssembly 和 JavaScript 程序需要共享内存。“你需要从 JavaScript 端合理高效并频繁地访问 WebAssembly 堆栈,因为在实际应用中,双方不会进行通信;他们主要通过堆共享信息,”Ehrenberg 这样解释道。

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


– Palmer

如果需要,WebAssembly 内存可以增长:您想从 JavaScript 访问它,则意味着在下次需要访问堆时,分离用于处理内存中二进制数据的 ArrayBuffer,并在底层 ArrayBuffer 上构建新的 TypedArray。这会在 32 位系统上分割内存空间。

现在,你可以创建一种可调整大小的新型数组缓冲区,它可以增大或缩小,而无需分离。

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

开发人员必需要明确选择这一点,因为数组缓冲区和类型数组是黑客尝试攻击浏览器的最常见方式,并且使现有的 ArrayBuffer 可调整大小意味着更改大量经过广泛测试和强化的代码,可能会产生错误和漏洞。

“最初创建该分离操作是为了实现与工作者之间的传输,”Ehrenberg 解释道。“当您通过 POST 消息发送 ArrayBuffer 以将其传输给另一个工作者时,所有权将转移,并且关闭该 ArrayBuffer 的所有类型化数组都将分离。” 配套提案允许开发人员转移数组缓冲区的所有权,这样它们就不会被篡改。

“如果你将它传递给一个函数,接收者可以使用这个传递函数来获取所有权,因此任何曾经拥有它的人都不再拥有它,”Palmer解释道。“这对完整性非常有好处。”

除了作为 ECMAScript 2024 的一部分之外,可调整大小的数组缓冲区还已集成到 WebAssembly JS API 中;Ehrenberg 称这是 Web 生态系统中不同社区之间良好合作的一个例子。

“这些工作涉及多个不同的标准委员会,需要它们之间明确的合作。这可能很复杂,因为你必须让很多人参与进来,但最终,我认为这会带来一个良好的设计流程。”

作者:万能的大雄

评论