Temporal

可用性有限

此特性不是基线特性,因为它在一些最广泛使用的浏览器中不起作用。

实验性: 这是一项实验性技术
在生产中使用此技术之前,请仔细检查浏览器兼容性表格

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 对象提供了读取和修改这些组件的方法。

时区是大量日期相关 bug 的根本原因。当通过“组件组合”模型与 Date 交互时,时间只能是两个时区之一:UTC 和本地(设备),并且无法指定任意时区。此外还缺少“无时区”的概念:这被称为日历日期(对于日期)或挂钟时间(对于时间),它表示你“从日历或时钟上读取的时间”。例如,如果你设置每日起床闹钟,你希望将其设置为“上午 8:00”,无论是否夏令时,无论你是否旅行到不同的时区等等。

Date 缺乏的第二个功能是日历系统。大多数人可能熟悉公历,它有两个时代,公元前和公元;有 12 个月;每个月有不同的天数;大约每 4 年有一个闰年;等等。然而,当你使用其他日历系统时,例如希伯来历、农历、日本历等,其中一些概念可能不适用。使用 Date,你只能使用公历模型。

Date 还有许多其他不受欢迎的遗留问题,例如所有设置器都是变异的(这通常会导致不必要的副作用),日期时间字符串格式无法以一致的方式解析等。最终,最好的解决方案是从头开始构建一个新的 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

inLeapYear
monthsInYear
daysInYear
era
eraYear

inLeapYear
monthsInYear
daysInYear
era
eraYear

inLeapYear
monthsInYear
daysInYear
N/A era
eraYear

inLeapYear
monthsInYear
daysInYear
N/A
月份相关 N/A 月份
monthCode
daysInMonth
月份
monthCode
daysInMonth
月份
monthCode
daysInMonth
N/A 月份
monthCode
daysInMonth
monthCode
周相关 N/A weekOfYear
yearOfWeek
daysInWeek
weekOfYear
yearOfWeek
daysInWeek
weekOfYear
yearOfWeek
daysInWeek
N/A N/A N/A
日期相关 N/A
dayOfWeek
dayOfYear

dayOfWeek
dayOfYear

dayOfWeek
dayOfYear
N/A N/A
时间组件 N/A 小时
minute

millisecond
microsecond
nanosecond
小时
minute

millisecond
microsecond
nanosecond
N/A 小时
minute

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 年增加一个额外的一天,即闰日。然后,一年可以进一步划分为称为月份的部分。这些日历被称为阳历。公历和伊朗太阳历都是阳历。
  • 有些日历主要将一个月定义为平均 29.5 天,通过将月份在 29 天和 30 天之间交替定义。然后,12 个月可以组合成一个 354 天的年份。这些日历被称为阴历。伊斯兰历就是一种阴历。由于阴历年份是人为的,与季节周期无关,因此阴历通常较少见。
  • 有些日历也主要根据月球周期定义月份,类似于阴历。然后,为了弥补与阳历 11 天的差异,大约每 3 年增加一个额外的月份,即闰月。这些日历被称为阴阳历。希伯来历和农历都是阴阳历。

Temporal 中,在一个日历系统下的每个日期都由三个组件唯一标识:yearmonthday。虽然 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 是一个任意整数,可以是零或负数,甚至可能随着时间减少(通常用于最古老的时代)。

注意:始终成对使用 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()

规范

规范
Temporal
# sec-temporal-objects

浏览器兼容性

另见