掌握 JavaScript 日期和时间:从 Moment.js 到 Temporal
JavaScript 的 `Date` API 长期以来一直是开发人员的苦恼之源,因为它存在历史设计缺陷,包括:
为了克服这些问题和限制,开发人员开始使用 Moment.js 等库来处理更可靠、功能更丰富的日期和时间。现在,JavaScript 即将推出一种新的内置解决方案:Temporal API,它为日期和时间操作提供了一种现代而直观的方法。
在本文中,我们将研究 JavaScript 的“Date”API 的局限性,讨论 Moment.js 等流行库的优缺点,并深入研究 Temporal API。
JavaScript 日期 API 简介
Brendan Eich 于 1995 年在 10 天内编写了 JavaScript。在匆忙的开发过程中,`Date` API 是通过复制 Java 的 `Date` 对象的实现创建的,无意中继承了它的几个问题。近 30 年后,这些问题仍然让开发人员感到沮丧。
`Date` API 的主要缺陷之一是其可变性。对 `Date` 对象的任何更改都会影响原始实例,从而可能导致意外行为。此外,该 API 缺乏用于常见日期和时间操作的直观方法,迫使开发人员编写笨拙且容易出错的代码。
在下面的示例中,我们创建了两个 `Date` 对象,`today` 和 `tomorrow`。将 `tomorrow` 设置为 `today` 的后一天后,`today` 和 `tomorrow` 最终都指向同一个日期。如果处理不当,此行为可能会导致意外结果或错误:
const today = new Date(); const tomorrow = today; // Both variables reference the same object // Modify tomorrow's date tomorrow.setDate(tomorrow.getDate() + 1); // Since today and tomorrow reference the same object, both will be affected console.log("Today:", today); console.log("Tomorrow:", tomorrow);
上述示例中显示的另一个问题是缺少用于添加或减去日期的内置方法。要添加日期,我们必须使用 `getDate()` 提取日期,然后添加日期,最后使用 `setDate()` 更新日期。这种方法使代码更难阅读和维护:
// What we have to do using the Date API tomorrow.setDate(tomorrow.getDate() + 1); // What we expect tomorrow.addDays(1)
`Date` API 的其他值得注意的问题包括:
JavaScript 的 Moment.js 和 date-fns 库
由于原生 JavaScript `Date` API 难以使用,因此出现了各种 JavaScript 库来简化日期和时间操作,其中之一就是 Moment.js。
与原生的“Date”API 相比,Moment.js 提供了更直观的日期和时间处理 API。以下是示例:
// use Date API to add one day for tomorrow const date = new Date(); date.setDate(date.getDate() + 1); // use moment.js to add one day for tomorrow const momentDate = moment().add(1, "day");
可以看出,Moment.js 提供了一种更简洁易读的方式来操作日期。它还提供了许多附加功能,例如:
有关更多详细信息,请参阅使用 Moment.js 操作数据和时间的指南。
Moment.js 虽然功能丰富,但易变且繁重。如果不小心使用,易变的特性可能会导致意外行为。此外,它在 2020 年被弃用,这意味着它不再得到积极维护。
幸运的是,像 date-fns 这样的替代库提供了类似的功能。date-fns 是不可变的,并且具有摇树功能,这使我们能够只加载必要的组件。
介绍 Temporal API

上图说明了时间日期对象字符串表示;它的结构支持纯日期、时间、时间偏移、区域日期/时间值以及所有类型的日历。
Temporal API 的一些主要功能包括:
Temporal API、Moment.js 和 date-fns 之间的比较
Temporal API 的基本用法
由于 Temporal API 仍处于第 3 阶段,在大多数 JavaScript 环境中尚不可用,因此在撰写本文时,您需要安装 polyfill 才能使用它。要安装 `js-temporal/polyfill`,请运行以下命令:
npm install @js-temporal/polyfill
然后,将其导入 JavaScript 项目以开始使用时间特征:
import { Temporal } from '@js-temporal/polyfill';
Temporal API 引入了几种日期类型,包括:
其他类型,例如“Temporal.Duration”,可以精确处理时间间隔,使 Temporal 能够适用于各种日期和时间场景。
纯日期/时间
我们可以使用普通的日期对象来表示没有时区的日期,这对于生日或截止日期等场景非常理想。
我们可以使用 `Temporal.Now.plainDateISO()` 或 `Temporal.PlainDate.from()` 创建纯日期。类似地,纯日期时间对象表示没有时区的日期和时间,纯时间仅表示时间:
// plain date const today = Temporal.Now.plainDateISO(); const newYear = Temporal.PlainDate.from("2024-01-01"); console.log(newYear.toString()); //2024-01-01 console.log(today.toString()); //2024-11-06 // plain date time const now = Temporal.Now.plainDateTimeISO(); console.log(now.toString()); //2024-11-06T20:24:57.927697925 // plain time const currentTime = Temporal.Now.plainTimeISO(); console.log(currentTime.toString()); //20:48:04.025084025
构造简单日期的另一种方法是从指定年、月、日的对象开始:
const firstDateNov = Temporal.PlainDate.from({ year: 2024, month: 11, day: 1 }); console.log(firstDateNov.toString()); // 2024-11-01
分区日期/时间
分区日期时间表示带有时区详细信息的日期时间对象。我们可以使用它来跨时区转换日期/时间,或计算考虑夏令时的日期时间。
Temporal API 使用 IANA 时区数据库,该数据库以“区域/位置”形式定义具有唯一名称的时区,例如“America/New_York”:
// Zoned date: with time zone info const today = Temporal.Now.zonedDateTimeISO(); console.log(today.toString()); //2024-11-06T21:04:23.019063018+10:00[Australia/Sydney] const zonedDateTime = Temporal.ZonedDateTime.from("2024-10-30T10:00[Australia/Sydney]"); console.log(zonedDateTime.toString());//2024-10-30T10:00:00+11:00[Australia/Sydney]
立即的
`Temporal.Instant` 表示一个精确的时间点。与限制为毫秒的传统 JavaScript `Date` 对象不同,`Temporal.Instant` 提供纳秒精度。瞬间始终采用 UTC,因此适用于记录来自不同时区的事件等用例,而无需担心本地偏移:
const currentInstant = Temporal.Now.instant(); console.log('current instant:',currentInstant.toString()); //current instant: 2024-11-10T00:17:06.085826084Z const instantFromString = Temporal.Instant.from('2024-11-10T10:41:51Z'); console.log('from string:', instantFromString.toString()); //from string: 2024-11-10T10:41:51Z const sydneyTime = instantFromString.toString({ timeZone: 'Australia/Sydney' }); console.log('sydney time:',sydneyTime); //sydney time: 2024-11-10T21:41:51+11:00
期间
在 Temporal API 中,持续时间表示时间间隔,例如天、小时甚至年。它简化了日期和时间计算,例如在特定日期中添加或减去时间,或者在定义的时间间隔内安排事件。
以下是向日期添加持续时间的示例:
// Create a duration of 1 year, 1 months, and 10 days const duration = Temporal.Duration.from({ years: 1, months: 1, days: 10 }); console.log(duration.toString()); // "P1Y1M10D" // Add the duration to a date const startDate = Temporal.PlainDate.from("2024-10-30"); const newDate = startDate.add(duration); console.log(newDate.toString()); // 2025-12-10
类似地,我们可以使用持续时间来计算时间:
const timeDuration = Temporal.Duration.from({ hours: 3, minutes: 45 }); console.log(timeDuration.toString()); // "PT3H45M" // Subtracting time duration from a specific time const time = Temporal.PlainTime.from("12:00"); const newTime = time.subtract(timeDuration); console.log(newTime.toString()); // 08:15:00
计算日期和时间:加、减、四舍五入以及
Temporal API 允许我们执行诸如添加或减去时间、四舍五入到特定单位以及使用“with”替换日期时间组件等操作。“round”方法将日期和时间值四舍五入到最接近的指定单位。“with”方法可以替换特定组件(例如年、月或小时)而不更改其他部分:
const initialDateTime = Temporal.PlainDateTime.from("2024-11-01T10:23:45.678"); // Add 2 days and 5 hours const addedDateTime = initialDateTime.add({ days: 2, hours: 5 }); console.log("After Adding:", addedDateTime.toString()); // "2024-11-03T15:23:45.678" // Subtract 1 hour and 30 minutes const subtractedDateTime = addedDateTime.subtract({ hours: 1, minutes: 30 }); console.log("After Subtracting:", subtractedDateTime.toString()); // "2024-11-03T13:53:45.678" // Round to the nearest minute const roundedDateTime = subtractedDateTime.round({ smallestUnit: "minute" }); console.log("After Rounding:", roundedDateTime.toString()); // "2024-11-03T13:54:00" // Use 'with' to modify specific components (change only the month) const finalDateTime = roundedDateTime.with({ month: 12 }); console.log("Final Date-Time:", finalDateTime.toString()); // "2024-12-03T13:54:00"
Temporal API 的主要优势:不变性和性能
Temporal API 的一个主要优势是其专注于不变性。这意味着一旦创建了 Temporal 对象,其值就无法修改。这种不变性可确保日期和时间计算是可预测的,并避免意外的副作用:
const now = Temporal.Now.plainDateISO(); const futureDate = now.add({ days: 5 }); // Modifying futureDate won't affect now futureDate.add({ hours: 2 }); console.log("Now:", now.toString()); // Now: 2024-11-07 console.log("Future Date:", futureDate.toString()); //Future Date: 2024-11-12
与 Moment.js 和 date-fns 等库相比,Temporal API 的性能显著提升,这主要归功于其原生实现以及对日期和时间操作的有效处理。
与庞大且可变的 Moment.js 不同,Temporal API 不会增加包大小,其不可变设计可最大限度地减少内存使用量。此外,由于它内置于 JavaScript(无需额外解析或开销),Temporal 速度更快,尤其适用于精确、大量日期时间任务,如调度和时区管理。
高级用法:时区、日程安排和非公历
使用 `temporal.zonedDatetime`,我们可以轻松处理时区和调度。它允许我们使用本地时间并管理跨时区的事件。
使用时区
使用“Temporal.ZonedDateTime”,我们可以在选定的时区内创建特定的日期和时间,确保诸如加减时间之类的操作自动遵守时区规则,包括夏令时调整:
// Create a ZonedDateTime in the "Australia/Sydney" time zone const sydneyTime = Temporal.ZonedDateTime.from( '2024-10-30T15:00:00[Australia/Sydney]' ); console.log(sydneyTime.toString()); // 2024-10-30T15:00:00+11:00[Australia/Sydney] // Convert to a different time zone const londonTime = sydneyTime.withTimeZone('Europe/London'); console.log(londonTime.toString()); // 2024-10-30T04:00:00+00:00[Europe/London]
夏令时 (DST) 开始时,时钟会向前调整一小时。这不会改变时间,但会改变时间偏移,使其看起来像是跳过了一个小时。Temporal 会自动处理这些变化,确保计算在 DST 转换期间保持准确:
// Adding 24 hours during a daylight saving change const beforeDST = Temporal.ZonedDateTime.from("2024-10-05T12:00:00[Australia/Sydney]"); const afterDST = beforeDST.add({ days: 1 }); // Automatically adjusts for DST console.log(beforeDST.toString());// 2024-10-05T12:00:00+10:00[Australia/Sydney] console.log(afterDST.toString()); // 2024-10-06T12:00:00+11:00[Australia/Sydney]
安排活动
Temporal API 还使得跨时区安排和计算事件时间变得更加容易。
在下面的例子中,我们使用 `withTimeZone` 将安排在伦敦时间的活动转换为悉尼当地时间。然后,我们使用 `since` 和 `until` 计算从圣诞节到活动的时间。`since` 测量从较晚日期到较早日期的时间,而 `until` 计算从较早日期到较晚日期的时间:
const scheduledTime = Temporal.ZonedDateTime.from({ year: 2025, month: 1, day: 4, hour: 10, minute: 30, timeZone: 'Europe/London', }); console.log(scheduledTime.toString()); // 2025-01-04T10:30:00+00:00[Europe/London] const localEventTime = scheduledTime.withTimeZone('Australia/Sydney'); console.log(localEventTime.toString()); // 2025-01-04T21:30:00+11:00[Australia/Sydney] const christmasDate = Temporal.ZonedDateTime.from({ year: 2024, month: 12, day: 25 ,timeZone: 'Australia/Sydney'}); const timeUntilScheduled = christmasDate.until(scheduledTime, { largestUnit: 'hours' }); const timeSinceChristmas = scheduledTime.since(christmasDate, { largestUnit: 'hours' }); console.log(`Duration until scheduled: ${timeUntilScheduled.hours} hours, from Christmas ${timeSinceChristmas.hours} hours`); //Duration until scheduled: 261 hours, from Christmas 261 hours
我们还可以使用 Temporal API 中的“compare”辅助方法对事件时间进行排序以进行调度:
const sessionA = Temporal.PlainDateTime.from({ year: 2024, day: 20, month: 11, hour: 8, minute: 45, }); const lunch = Temporal.PlainDateTime.from({ year: 2024, day: 20, month: 11, hour: 11, minute: 30, }); const sessionB = Temporal.PlainDateTime.from({ year: 2024, day: 20, month: 11, hour: 10, minute: 0, }); const sessionC = Temporal.PlainDateTime.from({ year: 2024, day: 20, month: 11, hour: 13, minute: 0, }); // The events can be sorted chronologically or in reverse order const sorted = Array.from([sessionC, lunch, sessionA, sessionB]).sort( Temporal.PlainDateTime.compare ); console.log('sorted:', sorted); console.log('sorted reverse:', sorted.reverse()); // sorted: ["2024-11-20T08:45:00","2024-11-20T10:00:00","2024-11-20T11:30:00","2024-11-20T13:00:00"] // sorted reverse: ["2024-11-20T13:00:00","2024-11-20T11:30:00","2024-11-20T10:00:00","2024-11-20T08:45:00"]
非公历
虽然公历是最广泛使用的日历,但有时也需要使用希伯来历、伊斯兰历、佛教历、印度历、中国历和日本历来表示具有文化或宗教意义的日期。Temporal API 支持多种日历系统:
const eventDate = Temporal.PlainDate.from({ year: 2024, month: 12, day: 15 }); console.log(`Japanese calendar:${eventDate.withCalendar("japanese").toLocaleString('en-US', { calendar: 'japanese' })}`); //Japanese calendar:12/15/6 R console.log(`Hebrew calendar:${eventDate.withCalendar("hebrew").toLocaleString('en-US', { calendar: 'hebrew' })}`); //Hebrew calendar:14 Kislev 5785 console.log(`Islamic calendar:${eventDate.withCalendar("islamic").toLocaleString('en-US', { calendar: 'islamic' })}`); //Islamic calendar:6/14/1446 AH console.log(`Buddhist calendar:${eventDate.withCalendar("buddhist").toLocaleString('en-US', { calendar: 'buddhist' })}`); //Buddhist calendar:12/15/2567 BE
在上面的例子中,我们使用 `withCalendar` 将日期转换为各种日历系统。它有助于以不同地区用户的本地格式显示日期。
从 Moment.js 或 date-fns 迁移到 Temporal
如前所述,在撰写本文时,Temporal API 仍处于第 3 阶段提案,这意味着它可能会发生更改,并且尚未得到所有平台的支持。但是,您现在可以开始尝试它,为将来的采用做好准备。从 Moment.js 或 date-fns 迁移到 Temporal API 时,请考虑以下步骤:
结论
JavaScript 的 Temporal API 提供对不变性、时区管理、精确持续时间和丰富辅助方法的原生支持,无需额外依赖。一旦 Temporal 成为 JavaScript 的核心部分,采用它将使您的代码库保持现代、可扩展,并为长期可靠的日期处理做好准备。
希望本文对您有所帮助。您可以在此处找到代码片段。
LogRocket:通过理解上下文更轻松地调试 JavaScript 错误
调试代码总是一项繁琐的任务。但是你对错误的了解越多,修复它们就越容易。
LogRocket 可让您以新颖独特的方式了解这些错误。我们的前端监控解决方案可跟踪用户与您的 JavaScript 前端的互动情况,让您能够准确了解用户的操作导致错误。

LogRocket 记录控制台日志、页面加载时间、堆栈跟踪、带有标头 + 正文的慢速网络请求/响应、浏览器元数据和自定义日志。了解 JavaScript 代码的影响从未如此简单!
免费试用。