导读:本篇文章分析了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 年做好准备。
“它在每个项目中至少出现一次。几乎每个项目都在编写这个小助手,因此它成为语言中真正优秀的开发人员幸福 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 委员会希望避免添加太多可能让新开发人员感到困惑的小众功能。
“作为标准委员会,我们不应该要求他们承担停机风险,因为我们已经知道某些事情的风险很高。”
– 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 补充道,在浏览器中发布,然后撤回实施会使添加新功能的成本更高。“作为标准委员会,我们不应该要求开发者们承担停机风险的成本,因为我们已经知道某些事情风险很高。”这意味着类似的提案将来可能会更多地使用静态方法来避免此类问题。
“人们很容易对 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 的工具也可能依赖它。
即使我们没有直接使用它,但都可以从中受益。”
“如果你有一个像 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 生态系统中不同社区之间良好合作的一个例子。
“这些工作涉及多个不同的标准委员会,需要它们之间明确的合作。这可能很复杂,因为你必须让很多人参与进来,但最终,我认为这会带来一个良好的设计流程。”
作者:万能的大雄
本文为 @ 万能的大雄 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。