此页面由社区从英文翻译而来。了解更多并加入 MDN Web Docs 社区。

View in English Always switch to English

Temporal

Limited availability

This feature is not Baseline because it does not work in some of the most widely-used browsers.

Temporal 对象支持在各种场景下管理日期和时间,包括内置的时区和日历表示、墙钟时间转换、运算、格式化等功能。它被设计为 Date 对象的完全替代方案。

描述

与大多数全局对象不同,Temporal 不是构造函数。你不能将其与 new 运算符一起使用,也不能将 Temporal 对象作为函数调用。Temporal 的所有属性和方法都是静态的(正如 Math 对象)。

Temporal 有着复杂且强大的 API。它通过多个类暴露了超过 200 个实用方法,因此可能显得非常复杂。我们将提供一个高层次的概览,阐述这些 API 之间的关系。

背景和概念

JavaScript 在一开始就有 Date 对象来处理日期和时间。但是,Date API 的设计基于 Java 中的设计欠佳的 java.util.Date 类,而该类在 2010 年代初就已被替代;但是,出于 JavaScript 向后兼容的目标,Date 仍然保留在这门语言中。

在整个介绍开始之前,需要强调的是:日期处理是复杂的Date 的大多数问题可以通过增加更多方法来修复,但这始终存在一个根本性的设计缺陷:它在同一个对象上暴露了过多的方法,导致开发者因常常不清楚该使用哪一个而踩到意想不到的坑。一个精心设计的 API 不仅需要能完成更多任务,还应在每一抽象层中承担更少职责,因为避免误用与支持更多使用场景是同样重要的。

Date 对象同时承担着两种角色:

  • 作为时间戳:表示自一个固定时间点(称为纪元)以来经过的毫秒数或纳秒数。
  • 作为日期组件的组合体:年、月、日、时、分、秒、毫秒、纳秒。年、月、日这些标识只有在参照某种日历系统时才有意义。当与某个时区关联时,整个组合体会映射到历史中的一个唯一时间点。Date 对象提供了用于读取和修改这些组件的方法。

时区是大量与日期相关漏洞的根源。当通过“分量组合体”模型与 Date 交互时,时间只能处于两个时区之一:UTC 时区和本地(设备)时区,并且无法指定任意时区。此外还缺少“无时区”的概念:无时区被称为日历日期(对应日期)或墙钟时间(对应时间),即你“从日历或时钟上看到的”的时间。例如,如果你在设置每天的起床闹钟时,不管时下是否处于夏令时,也不管你是否旅行至不同的时区等情况,你都会希望它始终设为“上午 8:00”。

Date 还缺少的第二个特性是日历系统。大多数人可能熟悉公历(格里高利历),它有公元前(BC)和公元后(AD)两个纪元;有 12 个月;每个月的天数不同;每 4 年有一个闰年等等。然而,当你使用其他日历系统,比如希伯来日历、中国农历、日本日历等等,一些公历的概念就可能不适用了。使用 Date 时,你只能采用公历模型。

Date 还有许多不理想的历史遗留问题,例如所有 setter 都可变(这常常导致不必要的副作用),日期时间字符串格式无法以一致的方式解析等等。最终,最佳解决方案是重新构建一个 API,而这正是 Temporal

API 概览

Temporal 是一个与 Intl 类似的命名空间,包括若干类和命名空间,每个类和命名空间都旨在处理日期与时间管理的某个特定方面。类可以被归为以下几组:

此外,还有另一个实用命名空间 Temporal.Now,提供以不同格式获取当前时间的方法。

共享的类接口

Temporal 命名空间中包含许多类,但它们共享很多相似的方法。下表列出了每个类的所有方法(不包括类之间的转换方法):

Instant ZonedDateTime PlainDateTime PlainDate PlainTime PlainYearMonth PlainMonthDay
构造函数 Instant()
Instant.from()
Instant.fromEpochMilliseconds()
Instant.fromEpochNanoseconds()
ZonedDateTime()
ZonedDateTime.from()
PlainDateTime()
PlainDateTime.from()
PlainDate()
PlainDate.from()
PlainTime()
PlainTime.from()
PlainYearMonth()
PlainYearMonth.from()
PlainMonthDay()
PlainMonthDay.from()
更新器 N/A with()
withCalendar()
withTimeZone()
withPlainTime()
with()
withCalendar()
withPlainTime()
with()
withCalendar()
with() with() with()
运算 add()
subtract()
since()
until()
add()
subtract()
since()
until()
add()
subtract()
since()
until()
add()
subtract()
since()
until()
add()
subtract()
since()
until()
add()
subtract()
since()
until()
N/A
舍入 round() round() round() N/A round() N/A N/A
比较 equals()
Instant.compare()
equals()
ZonedDateTime.compare()
equals()
PlainDateTime.compare()
equals()
PlainDate.compare()
equals()
PlainTime.compare()
equals()
PlainYearMonth.compare()
equals()
序列化 toJSON()
toLocaleString()
toString()
valueOf()
toJSON()
toLocaleString()
toString()
valueOf()
toJSON()
toLocaleString()
toString()
valueOf()
toJSON()
toLocaleString()
toString()
valueOf()
toJSON()
toLocaleString()
toString()
valueOf()
toJSON()
toLocaleString()
toString()
valueOf()
toJSON()
toLocaleString()
toString()
valueOf()

下表总结了每个类可用的属性,让你了解每个类能表示哪些信息。

Instant ZonedDateTime PlainDateTime PlainDate PlainTime PlainYearMonth PlainMonthDay
日历 N/A calendarId calendarId calendarId N/A calendarId calendarId
年份相关 N/A era
eraYear
year
inLeapYear
monthsInYear
daysInYear
era
eraYear
year
inLeapYear
monthsInYear
daysInYear
era
eraYear
year
inLeapYear
monthsInYear
daysInYear
N/A era
eraYear
year
inLeapYear
monthsInYear
daysInYear
N/A
月份相关 N/A month
monthCode
daysInMonth
month
monthCode
daysInMonth
month
monthCode
daysInMonth
N/A month
monthCode
daysInMonth
monthCode
星期相关 N/A weekOfYear
yearOfWeek
daysInWeek
weekOfYear
yearOfWeek
daysInWeek
weekOfYear
yearOfWeek
daysInWeek
N/A N/A N/A
日相关 N/A day
dayOfWeek
dayOfYear
day
dayOfWeek
dayOfYear
day
dayOfWeek
dayOfYear
N/A N/A day
时间组件 N/A hour
minute
second
millisecond
microsecond
nanosecond
hour
minute
second
millisecond
microsecond
nanosecond
N/A hour
minute
second
millisecond
microsecond
nanosecond
N/A N/A
时区 N/A timeZoneId
offset
offsetNanoseconds
hoursInDay
getTimeZoneTransition()
startOfDay()
N/A N/A N/A N/A N/A
纪元时间 epochMilliseconds
epochNanoseconds
epochMilliseconds
epochNanoseconds
N/A N/A N/A N/A N/A

类之间的转换

下表总结了各类之间存在的所有转换方法。

转换自
Instant ZonedDateTime PlainDateTime PlainDate PlainTime PlainYearMonth PlainMonthDay
转换为Instant/toInstant()先转换为 ZonedDateTime
ZonedDateTimetoZonedDateTimeISO()/toZonedDateTime()toZonedDateTime()PlainDate#toZonedDateTime()(作为参数传入)先转换为 PlainDate
PlainDateTime先转换为 ZonedDateTimetoPlainDateTime()/toPlainDateTime()PlainDate#toPlainDateTime()(作为参数传入)
PlainDatetoPlainDate()toPlainDate()/信息不重叠toPlainDate()toPlainDate()
PlainTimetoPlainTime()toPlainTime()信息不重叠/信息不重叠
PlainYearMonth先转换为 PlainDatetoPlainYearMonth()信息不重叠/先转换为 PlainDate
PlainMonthDaytoPlainMonthDay()先转换为 PlainDate/

通过这些表格,你应该对如何使用 Temporal API 有了基本认识。

日历

日历是一种组织日期的方法,通常按周、月、年和纪元划分时期。世界上大多数地区使用公历,但也在使用许多其他的日历,尤其是在宗教与文化背景下。默认情况下,所有与日历相关的 Temporal 对象都使用 ISO 8601 日历系统,该系统基于公历并定义了额外的周编号规则。Intl.supportedValuesOf() 列出了浏览器可能支持的大多数日历。这里我们简要概述日历系统是如何形成的,帮助你理解哪些因素可能因日历而异。

地球上有三个显著的周期性事件:绕太阳公转(一次 365.242 天)、月球绕地球公转(从一次新月到下一次新月约 29.53 天)、地球自转(从日出到日出约 24 小时)。每种文化对“一天”的衡量都是 24 小时。偶然的变化(如夏令时)不在日历范畴,而是时区信息的一部分。

  • 有些日历主要以一年平均 365.242 天为基准,规定一年有 365 天,并大约每隔 4 年增加一天,即闰日。之后,一年会被进一步划分为“月”的部分。这些日历被称为阳历(solar calendar)。公历和希吉拉阳历(伊朗历)都属于阳历。
  • 有些日历主要以一月平均 29.5 天为基准,规定月在 29 天和 30 天之间交替。然后 12 个月组成一年,共 354 天。这些日历被称为阴历(lunar calendar)。伊斯兰历是阴历。由于阴历年的长度与季节周期不相关,阴历通常更少见。
  • 还有一些日历主要以月相周期定义月份,类似阴历。为了补偿与阳历年之间约 11 天的差距,它会大约每 3 年加入一个闰月。这些日历称为阴阳历(lunisolar calendar)。希伯来历和中国农历都是阴阳历。

Temporal 中,同一日历系统下的每个日期都由三个组件唯一标识:yearmonthdayyear 通常为正整数,但也可能为 0 或负数,并随时间单调递增。year 值为 1(或可能存在的 0)称为该日历的纪元,可由每种日历任意设定。month 为正整数,从 1 开始,每次递增 1,直到 date.monthsInYear,随后随着年份推进重置为 1day 也是正整数,但不一定从 1 开始或每次递增 1,因为政治变动可能导致日期被跳过或重复。总体而言,day 会随月份推进而单调增加并在月切换时重置。

除了 year 以外,对使用纪元的日历而言,年份还可以由 eraeraYear 的组合唯一标识。例如,公历使用纪元“CE”和纪元前“BCE”,并且年份 -1{ era: "bce", eraYear: 2 } 等价(注意所有日历都存在年份 0;在公历中,由于天文计年,它对应公元前 1 年)。era 是小写字符串,eraYear 是任意整数,可以为 0 或负数,甚至可能随时间递减(通常用于最早的纪元)。

备注:始终将 eraeraYear 成对使用;不要只用其中一个。此外,为避免冲突,在指定年份时不要把 yearera/eraYear 混用。选择一种年份表示方式并保持一致。

注意以下对年份的错误假设:

  • 不要假设 eraeraYear 总是存在;它们可能是 undefined
  • 不要假设 era 是用户友好的字符串;而是使用 toLocaleString() 以格式化日期。
  • 不要假设来自不同日历的两个 year 值可比较;而是使用静态方法 compare()
  • 不要假设一年有 365/366 天或 12 个月;而是使用 daysInYearmonthsInYear
  • 不要假设闰年(inLeapYeartrue)只多出一天;它可能会多出一个月。

除了 month 以外,一年中的月份还可以由 monthCode 唯一标识。monthCode 通常对应月份名称,但 month 不对应。例如在阴阳历中,若两个月份的 monthCode 相同,其中一个属于闰年而另一个不属于闰年,那么在闰月之后,它们的 month 值会因插入了一个额外的月份而不同。

备注:为避免冲突,在指定月份时不要混用 monthmonthCode。选择一种月份表示方式并保持一致。如果你需要一年中月份的顺序(例如循环遍历月份)时,month 更有用;如果你需要月份的名称(例如用于储存生日)时,monthCode 更有用。

注意以下对月份的错误假设:

  • 不要假设 monthCodemonth 总是对应。
  • 不要假设一个月的天数;而是使用 daysInMonth
  • 不要假设 monthCode 是用户友好的字符串;而是使用 toLocaleString() 以格式化日期。
  • 通常不要把月份名称缓存到数组或对象中。即使 monthCode 通常对应某一日历中的月份名称,我们仍建议始终通过例如 date.toLocaleString("en-US", { calendar: date.calendarId, month: "long" }) 以计算月份名称。

除了 day(基于月份的索引)以外,一年中的某一天还可以由 dayOfYear 唯一标识。dayOfYear 是正整数,从 1 开始,每次递增 1,直到 date.daysInYear

“周”的概念不关乎任何天文事件,而关乎文化建构。虽然最常见的长度是 7 天,但一周也可以是 4、5、6、8 或更多天——甚至没有固定天数。要获取某日期所处“周”的具体天数,使用该日期的 daysInWeekTemporal 通过 weekOfYearyearOfWeek 的组合以识别周。weekOfYear 是正整数,从 1 开始,每次递增 1,随后随着年份推进重置为 1yearOfWeek 通常与 year 相同,但可能会在每年开头或结尾不同,因为一周可能跨越两个年份,而 yearOfWeek 会根据日历规则选择其中一个年份。

备注:始终将 weekOfYearyearOfWeek 成对使用;不要混用 weekOfYearyear

注意以下对“周”的错误假设:

  • 不要假设 weekOfYearyearOfWeek 总是存在;它们可能是 undefined
  • 不要假设每周总是 7 天;而是使用 daysInWeek
  • 注意当前 Temporal API 不支持“年 - 周”日期,因此你不能用这些属性构造日期或将日期序列化为“年 - 周”表示。它们只是信息性属性。

RFC 9557 格式

所有 Temporal 类都可以使用 RFC 9557 指定的格式进行序列化和反序列化,该格式基于 ISO 8601 / RFC 3339。完整格式如下(空格仅为方便阅读,实际字符串中不应包含空格):

YYYY-MM-DD T HH:mm:ss.sssssssss Z/±HH:mm [time_zone_id] [u-ca=calendar_id]

不同的类对每个组件是否必须存在有不同要求,因此你会在每个类的文档中看到“RFC 9557 格式”一节,说明该类可识别的格式。

这与 Date 使用的日期时间字符串格式非常相似,同样基于 ISO 8601。主要的新增能力是可以指定微秒与纳秒组件,以及指定时区和日历系统。

可表示的日期

所有表示特定日历日期的 Temporal 对象都对可表示日期范围施加了类似的限制:以 Unix 纪元为中心的 ±108 天(含边界),即从 -271821-04-20T00:00:00+275760-09-13T00:00:00 的瞬间的范围。这与有效日期范围相同。详细规则如下:

  • Temporal.InstantTemporal.ZonedDateTime 会直接对其 epochNanoseconds 值施加该限制。
  • Temporal.PlainDateTime 以 UTC 时区解释其日期,并要求它距离 Unix 纪元为 ±(108 + 1) 天(不含边界),因此其有效范围为 -271821-04-19T00:00:00+275760-09-14T00:00:00(不含边界)。这保证任何 ZonedDateTime 都能转换为 PlainDateTime,无论其偏移量如何。
  • Temporal.PlainDate 对该日期的正午(12:00:00)应用与 PlainDateTime 相同的检查,因此其有效范围为 -271821-04-19+275760-09-13。这保证任何 PlainDateTime 都能转换为 PlainDate,无论其具体时间如何,反之亦然。
  • Temporal.PlainYearMonth 的有效范围为 -271821-04+275760-09。这保证任何 PlainDate 都能转换为 PlainYearMonth,无论其日期如何(除非非 ISO 日历月份的第一天落在 ISO 月份 -271821-03 中)。

Temporal 对象会拒绝构造超出其限制的日期/时间实例。这包括:

  • 使用构造函数或 from() 静态方法。
  • 使用 with() 方法更新日历字段。
  • 使用 add()subtract()round() 或其他方法派发新实例。

静态属性

Temporal.Duration

表示两个时间点之间的差值,可用于日期/时间运算。它基础表示为年、月、周、日、时、分、秒、毫秒、微秒和纳秒数值的组合。

Temporal.Instant

表示时间中的一个唯一点,具有纳秒级精度。它基础表示为自 Unix 纪元(1970 年 1 月 1 日 UTC 零点)以来的纳秒数,不考虑任何时区或日历系统。

Temporal.Now

提供以不同格式获取当前时间的方法。

Temporal.PlainDate

表示一个日历日期(不含时间或时区的日期);例如,日历上的一个持续一整天的事件,无论发生在哪个时区。它基础表示为一个 ISO 8601 日历日期,包含年、月、日字段,以及关联的日历系统。

Temporal.PlainDateTime

表示一个不含时区的日期(日历日期)和时间(墙钟时间)。它基础表示为一个日期(带有日历系统)与一个时间的组合。

Temporal.PlainMonthDay

表示一个日历日期的月与日,不包含年份或时区;例如,每年重复且持续一整天的日历事件。它基础表示为一个 ISO 8601 日历日期,包含年、月、日字段,以及关联的日历系统。年份用于在非 ISO 日历系统中消除“月—日”歧义。

Temporal.PlainTime

表示一个不含日期或时区的时间;例如,每天在同一时间发生的重复事件。它基础表示为由时、分、秒、毫秒、微秒和纳秒值组成的组合。

Temporal.PlainYearMonth

表示一个日历日期的年和月,不包含日或时区;例如,日历上持续整个月的事件。它基础表示为一个 ISO 8601 日历日期,包含年、月、日字段,以及关联的日历系统。日字段用于在非 ISO 日历系统中消除“年—月”歧义。

Temporal.ZonedDateTime

表示带时区的日期与时间。它基础表示为一个瞬间、一个时区以及一个日历系统的组合。

Temporal[Symbol.toStringTag]

[Symbol.toStringTag] 属性的初始值是字符串 "Temporal"。该属性用于 Object.prototype.toString()

规范

Specification
Temporal
# sec-temporal-objects

浏览器兼容性

参见