这篇文章将带你了解 2024 年最令人期待的 JavaScript 新特性!
以下提案很有可能被纳入今年的 ECMAScript 版本:
- Temporal
- 管道操作符
- 记录和元组
- 正则表达式
/v标志 - 装饰器
JavaScript 的更新历程
每次 JavaScript 更新都会引起轰动。
ES6 是一个重大更新,距离它的前身 ES5 已经有六年了。浏览器厂商和 JavaScript 开发者都被大量的新特性所淹没,需要学习和适应。为了避免一次性出现大量新特性,从那时起,JavaScript 就开始了一年一度的发布周期。
这个年度发布周期包括提出新特性,然后由委员会进行讨论、评估和投票,最后才会被添加到语言中。这个过程也允许浏览器在正式添加到语言之前尝试实现提案,这可能有助于解决任何实现问题。
如前所述,JavaScript(或 ECMAScript)的新特性是由 技术委员会 39 (TC39) 决定的。TC39 由所有主要浏览器厂商的代表以及 JavaScript 专家组成。他们定期开会讨论 JavaScript 的新特性以及如何实现它们。新特性以提案的形式提出(任何人都可以提出),然后委员会成员投票决定每个提案是否可以进入下一阶段。每个提案都有 4 个阶段;一旦提案达到第 4 阶段,就预计会被包含在下一个版本的 ES 中。
ES 规范的一个重要部分是它必须向后兼容。这意味着任何新特性都不能通过改变以前版本的 ES 的工作方式来破坏互联网。所以他们不能改变现有方法的工作方式,只能添加新方法,因为任何使用可能预先存在的方法运行的网站都有可能被破坏。
所有当前提案的完整列表可以在这里看到:github.com/tc39/propos…。
Temporal:告别日期处理难题
在 2022 年 JavaScript 状态调查 中,对“你认为 JavaScript 目前缺少什么?”这一问题的第三个最常见的答案是更好的日期管理。
这导致了 Temporal 提案 的出现,它提供了一个标准的全局对象来代替 Date 对象,并解决了许多年来导致开发者在 JavaScript 中处理日期时感到痛苦的问题。
在 JavaScript 中处理日期几乎总是一项令人头疼的任务;必须处理一些微小但令人恼火的矛盾,例如月份从 0 开始计数,而日期从 1 开始计数,真是让人抓狂。
日期处理的困难导致了 Moment、Day.JS 和 date-fns 等流行库的出现,它们试图解决这些问题。然而,Temporal API 旨在从根本上解决所有这些问题。
Temporal 将开箱即用地支持多个时区和非格里高利历,并将提供一个易于使用的 API,使从字符串解析日期变得更加容易。此外,所有 Temporal 对象都是不可变的,这将有助于避免任何意外的日期更改错误。
让我们来看看 Temporal API 提供的一些最有用方法的示例。
Temporal.Now.Instant()
Temporal.Now.Instant() 将返回一个精确到纳秒的 DateTime 对象。你可以使用 from 方法指定特定的日期,如下所示:
const olympics = Temporal.Instant.from('2024-07-26T20:24:00+01:00');
这将创建一个 DateTime 对象,表示今年晚些时候巴黎奥运会的开始时间,即 2024 年 7 月 26 日 20:24(UTC)。
PlainDate()
这允许你只创建一个日期,不带时间:
new Temporal.PlainDate(2024, 7, 26);
Temporal.PlainDate.from('2024-07-26');
// 两者都返回一个表示 2024 年 7 月 26 日的 PlainDate 对象
PlainTime()
作为 PlainDate() 的补充,我们可以使用 .PlainTime() 来只创建一个时间,不带日期:
new Temporal.PlainTime(20, 24, 0);
Temporal.PlainTime.from('20:24:00');
// 两者都返回一个 20:24 的 PlainTime 对象
PlainMonthDay()
PlainMonthDay() 与 PlainDate 类似,但它只返回月份和日期,没有年份信息(对于每年在同一天重复的日期很有用,例如圣诞节和情人节):
const valentinesDay = Temporal.PlainMonthDay.from({ month: 2, day: 14 });
PlainYearMonth()
类似地,还有 PlainYearMonth,它将只返回年份和月份(用于表示一年中的整个月):
const march = Temporal.PlainYearMonth.from({ month: 3, year: 2024 });
计算
可以使用 Temporal 对象进行许多计算。你可以对日期对象添加和减去各种时间单位:
const today = Temporal.Now.plainDateISO();
const lastWeek = today.subtract({ days: 7 });
const nextWeek = today.add({ days: 7 });
until 和 since 方法可以让你找出距离某个日期还有多少时间,或者自该日期发生以来已经过去了多少时间。例如,以下代码将告诉你距离巴黎奥运会还有多少天:
olympics.until().days;
valentinesDay.since().hours;
这些方法返回一个 Temporal.Duration 对象,可以用来衡量一段时间,它有许多不同的单位和舍入选项。
额外功能
你可以从 Date 对象中提取年、月和日,从 Time 对象中提取小时、分钟、秒、毫秒、微秒和纳秒(当前 DateTime 对象中没有微秒和纳秒)。例如:
olympics.hour;
// 输出 20
还有其他属性,例如 dayOfWeek(星期一返回 1,星期日返回 7)、daysInMonth(根据月份返回 28、29、30 或 31)和 daysinYear(根据闰年返回 365 或 366)。
Temporal 日期对象还将有一个 compare 方法,可以用来使用各种排序算法对日期进行排序。
Temporal 目前是一个第 3 阶段的提案,正在由浏览器厂商实现,所以它似乎已经成熟了。你可以在 这里 查看完整的文档。这里 也有一个有用的用例食谱。当与 Intl.DateTimeFormat API 结合使用时,你将能够进行一些非常漂亮的日期操作。
管道操作符:让函数调用更流畅
在 2022 年 JavaScript 状态调查 中,对“你认为 JavaScript 目前缺少什么?”这一问题的第六个最常见的答案是管道操作符。
你可以在 这里 查看管道操作符提案。
管道操作符是函数式语言中的一个标准特性,它允许你将一个值从一个函数“管道”到另一个函数,前一个函数的输出作为下一个函数的输入(类似于 Fetch API 将从一个 Promise 返回的任何数据传递到下一个 Promise 的方式)。
例如,假设我们想连续对一个字符串应用三个函数:
- 将字符串“Listen up!”连接到原始字符串的开头。
- 将三个感叹号连接到字符串的末尾。
- 将所有文本转换为大写。
这三个函数可以写成如下:
const exclaim = string => string + "!!!";
const listen = string => "Listen up! " + string;
const uppercase = string => string.toUpperCase();
这三个函数可以通过将它们嵌套在一起应用,如下所示:
const text = "Hello World";
uppercase(exclaim(listen(text)));
// 输出 "LISTEN UP! HELLO WORLD!!!"
但是,像这样深度嵌套多个函数调用会很快变得混乱,尤其是作为参数传递的值(text)最终会深深地嵌入到表达式中,使其难以识别。
函数嵌套的另一个问题是函数应用的顺序是颠倒的,即最里面的函数首先应用。所以在这种情况下,listen 被应用于 text 的原始值,然后是 exclaim,最后是 最外面的函数 uppercase。特别是对于大型和复杂的函数,这变得难以理解和不直观。
另一种方法是使用函数链,如下所示:
const text = "Hello World";
text.listen().exclaim().uppercase();
这解决了嵌套函数的很多问题。传递的参数在开头,每个函数都按其应用的顺序出现,所以 listen() 首先应用,然后是 exclaim(),最后是 uppercase()。
不幸的是,这个例子不能工作,因为 listen、exclaim 和 uppercase 函数不是 String 类的成员方法。可以通过 猴子补丁 String 类来添加它们,但这通常不被认为是一种好的技术。
这意味着,虽然链式调用看起来比函数嵌套好得多,但它实际上只能与内置函数一起使用(就像经常使用数组方法一样)。
管道结合了链式调用的易用性,但能够与任何函数一起使用。根据目前的提案,上面的例子将写成如下:
text |> listen(%) |> exclaim(%) |> uppercase(%);
% 标记是一个占位符,用于表示前一个函数的输出值,尽管很有可能在正式版本中,% 字符将被其他字符替换。这允许在管道中使用接受多个参数的函数。
管道结合了链式调用的易用性,但可以与你编写的任何自定义函数一起使用。唯一的条件是,你需要确保一个函数的输出类型与链中下一个函数的输入类型匹配。
管道最适合与只接受一个参数的 柯里化函数 一起使用,该参数从任何先前函数的返回值中进行管道传输。它使函数式编程变得更加容易,因为可以将小的构建块函数链接在一起,以创建更复杂的复合函数。它也使 部分应用 更容易实现。
尽管很受欢迎,但管道操作符一直难以超越第 2 阶段。这是因为在如何表达符号以及对内存性能和它如何与 await 一起工作的担忧方面存在分歧。不过,似乎委员会正在慢慢达成某种协议,所以希望管道操作符能迅速通过各个阶段,并在今年亮相。
值得庆幸的是,管道操作符已经 在 Babel 7.15 版本中实现。
就个人而言,我们希望管道操作符能够在今年实现并推出,因为它确实有助于提高 JavaScript 作为一门严肃的函数式编程语言的信誉。
记录和元组:不可变数据结构
记录和元组提案 旨在将不可变数据结构引入 JavaScript。
元组类似于数组——一个有序的值列表——但它们是深度不可变的。这意味着元组中的每个值必须是 原始值 或另一个记录或元组(不能是数组或对象,因为它们在 JavaScript 中是可变的)。
元组的创建方式与数组字面量类似,但在前面有一个前导哈希符号 (#):
const heroes = #["Batman", "Superman", "Wonder Woman"];
一旦创建了这个元组,就不能添加或删除其他值。值也不能更改。
记录类似于对象——一个键值对的集合——但它们也是深度不可变的。它们的创建方式与对象类似——但与元组一样,它们以一个前导哈希开头:
const traitors = #{
diane: false,
paul: true,
zac: false,
harry: true
};
记录仍然可以使用点符号来访问属性和方法:
traitors.paul;
// 输出 true
数组使用的方括号符号也可以用于元组:
heroes[1];
// 输出 "Superman"
但是,由于它们是不可变的,因此你不能更新任何属性:
traitors.paul = false;
// 输出 错误
heroes[1] = "Supergirl";
// 输出 错误
元组和记录的不可变性意味着你可以使用 === 操作符轻松地比较它们:
heroes === #["Batman", "Superman", "Wonder Woman"];
// 输出 true
需要注意的是,在考虑记录的相等性时,属性的顺序并不重要:
traitors === #{
ross: false,
zac: false,
paul: true,
harry: true
};
// 仍然为 true,即使人物的顺序发生了变化
// 输出 true
但是,对于元组来说,顺序确实很重要,因为它们是一个有序的数据列表:
heroes === #["Wonder Woman", "Batman", "Superman"];
// 输出 false
这个页面 有一个方便的教程和一个在线游乐场,可以让你熟悉记录和元组的工作方式。
正则表达式 /v 标志:增强正则表达式功能
正则表达式从 JavaScript 3 版本开始就被纳入其中,并且从那时起进行了许多改进(例如 ES2015 中使用 u 标志的 Unicode 支持)。v 标志提案 旨在完成 u 标志所做的所有事情,但它添加了一些额外的好处,我们将在下面的示例中介绍。
简单地说,实现 v 标志包括在正则表达式的末尾添加 /v。
例如,以下代码可以用来测试一个字符是否是表情符号:
const isEmoji = /^\p{RGI_Emoji}$/v;
isEmoji.test("💚");
// 输出 true
isEmoji.test("🐨");
// 输出 true
这使用 RGI_Emoji 模式来识别表情符号。
v 标志还允许你在正则表达式中使用集合表示法。例如,你可以使用 -- 操作符从一个模式中减去另一个模式。以下代码可以用来从表情符号集合中移除任何爱心:
const isNotHeartEmoji = /^[\p{RGI_Emoji_Tag_Sequence}--\q{💜💚♥️💙🖤💛🧡🤍🤎}]$/v;
isNotHeartEmoji.test("💚");
// 输出 false
isNotHeartEmoji.test("🐨");
// 输出 true
你可以使用 && 查找两个模式的交集。例如,以下代码将查找希腊符号和字母的交集:
const GreekLetters = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;
GreekLetters.test('π');
// 输出 true
GreekLetters.test('𐆊');
// 输出 false
v 标志还解决了 u 标志在不区分大小写方面的一些问题,使其在几乎所有情况下都是更好的选择。
正则表达式的 v 标志在 2023 年期间达到了第 4 阶段,并在所有主流浏览器中实现,因此完全可以预期它将成为 ES2024 规范的一部分。
装饰器:更优雅地扩展类
装饰器提案 旨在使用装饰器来本地扩展 JavaScript 类。
装饰器在许多面向对象语言(如 Python)中已经很常见,并且已经 包含在 TypeScript 中。它们是一种标准的元编程抽象,允许你在不改变函数或类结构的情况下为其添加额外的功能。例如,你可能想为一个方法添加一些额外的验证,你可以通过创建一个验证装饰器来检查输入到表单中的数据来实现这一点。
虽然 JavaScript 允许你使用函数来实现这种设计模式,但大多数面向对象程序员更喜欢一种更简单、更本地的方式来实现这一点,只是为了让生活更轻松。
该提案添加了一些语法糖,让你可以轻松地在类中实现一个装饰器,而无需考虑将 this 绑定到类。它提供了一种更简洁的方法来扩展类元素,例如类字段、类方法或类访问器,甚至可以应用于整个类。
装饰器使用 @ 符号作为前缀,并且始终放在它们“装饰”的代码之前。
例如,类装饰器将紧接在类定义之前。在下面的例子中,validation 装饰器应用于整个 FormComponent 类:
@validation
class FormComponent {
// 代码
}
// 装饰器函数也需要定义
function validation(target) {
// 验证代码
}
类方法装饰器紧接在其装饰的方法之前。在下面的例子中,validation 装饰器应用于 submit 方法:
class FormComponent {
// 类代码
@validation
submit(data) {
// 方法代码
}
}
// 装饰器函数也需要定义
function validation(target) {
// 验证代码
}
装饰器函数定义接受两个参数:值和上下文。值参数指的是被装饰的值(例如类方法),上下文包含关于该值的元数据,例如它是否是函数、它的名称以及它是否是静态的或私有的。你还可以向上下文添加一个初始化器函数,该函数将在实例化类时运行。
装饰器提案目前处于第 3 阶段,并且已经 在 Babel 中实现,所以你已经可以试用它了。
总结
你觉得怎么样? 你希望今年规范中添加什么? 所有这些特性都将是 JavaScript 的伟大补充,所以让我们祈祷它们能在今年实现!