描述
与大多数全局对象不同,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.Duration
- 表示时间点
- 表示历史上的一个唯一时刻
- 作为时间戳:
Temporal.Instant
- 作为日期-时间组件组合与时区配对:
Temporal.ZonedDateTime
- 作为时间戳:
- 表示不感知时区的日期/时间(所有都以“Plain”为前缀)
- 日期(年、月、日)+ 时间(小时、分钟、秒、毫秒、微秒、纳秒):
Temporal.PlainDateTime
(注意:ZonedDateTime
等同于PlainDateTime
加上时区)- 日期(年、月、日):
Temporal.PlainDate
- 时间(小时、分钟、秒、毫秒、微秒、纳秒):
Temporal.PlainTime
- 日期(年、月、日):
- 日期(年、月、日)+ 时间(小时、分钟、秒、毫秒、微秒、纳秒):
- 表示历史上的一个唯一时刻
此外,还有一个实用命名空间 Temporal.Now
,它提供了以各种格式获取当前时间的方法。
共享类接口
Temporal
命名空间中有许多类,但它们共享许多相似的方法。下表列出了每个类的所有方法(除了转换方法)
下表总结了每个类可用的属性,让你大致了解每个类可以表示的信息。
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 | ||||
ZonedDateTime | toZonedDateTimeISO() | / | toZonedDateTime() | toZonedDateTime() | PlainDate#toZonedDateTime() (作为参数传递) | 首先转换为 PlainDate | ||
PlainDateTime | 首先转换为 ZonedDateTime | toPlainDateTime() | / | toPlainDateTime() | PlainDate#toPlainDateTime() (作为参数传递) | |||
PlainDate | toPlainDate() | toPlainDate() | / | 信息无重叠 | toPlainDate() | toPlainDate() | ||
PlainTime | toPlainTime() | toPlainTime() | 信息无重叠 | / | 信息无重叠 | |||
PlainYearMonth | 首先转换为 PlainDate | toPlainYearMonth() | 信息无重叠 | / | 首先转换为 PlainDate | |||
PlainMonthDay | toPlainMonthDay() | 首先转换为 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
中,在一个日历系统下的每个日期都由三个组件唯一标识:year
、month
和 day
。虽然 year
通常是正整数,但它也可以是零或负数,并且随着时间的推移单调递增。年份 1
(或 0
,如果存在)被称为日历纪元,对于每个日历都是任意的。month
是一个正整数,每次递增 1,从 1
开始,到 date.monthsInYear
结束,然后随着年份的推进重置回 1
。day
也是一个正整数,但它可能不从 1 开始,或者每次不递增 1,因为政治变化可能导致日期跳过或重复。但总的来说,day
单调递增并随着月份的推进而重置。
除了 year
,对于使用时代的日历,年份也可以通过 era
和 eraYear
的组合唯一标识。例如,公历使用“CE”(公元)和“BCE”(公元前)时代,年份 -1
与 { era: "bce", eraYear: 2 }
相同(请注意,年份 0
始终存在于所有日历中;对于公历,由于天文年编号,它对应于公元前 1 年)。era
是一个小写字符串,eraYear
是一个任意整数,可以是零或负数,甚至可能随着时间减少(通常用于最古老的时代)。
注意:始终成对使用 era
和 eraYear
;不要在没有使用另一个属性的情况下使用其中一个。此外,为避免冲突,在指定年份时不要将 year
与 era
/eraYear
组合使用。选择一种年份表示并始终如一地使用它。
请注意以下关于年份的错误假设
- 不要假设
era
和eraYear
总是存在;它们可能是undefined
。 - 不要假设
era
是用户友好的字符串;请使用toLocaleString()
来格式化你的日期。 - 不要假设来自不同日历的两个
year
值是可比较的;请使用compare()
静态方法。 - 不要假设年份有 365/366 天和 12 个月;请使用
daysInYear
和monthsInYear
。 - 不要假设闰年(
inLeapYear
为true
)多一天;它们可能多一个月。
除了 month
,一年中的月份也可以通过 monthCode
唯一标识。monthCode
通常映射到月份的名称,但 month
不映射。例如,在阴阳历的情况下,两个具有相同 monthCode
的月份,其中一个属于闰年而另一个不属于,如果它们在闰月之后,则由于插入了一个额外的月份,它们将具有不同的 month
值。
注意:为避免冲突,在指定月份时不要将 month
与 monthCode
组合使用。选择一种月份表示并始终如一地使用它。如果你需要一年中月份的顺序(例如,循环遍历月份时),month
更实用,而如果你需要月份的名称(例如,存储生日时),monthCode
更实用。
请注意以下关于月份的错误假设
- 不要假设
monthCode
和month
总是对应。 - 不要假设月份的天数;请使用
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 或更多天,甚至完全没有固定的天数。要获取日期一周中的具体天数,请使用日期的 daysInWeek
。Temporal
通过 weekOfYear
和 yearOfWeek
的组合来标识周。weekOfYear
是一个正整数,每次递增 1,从 1
开始,然后随着年份的推进重置回 1
。yearOfWeek
通常与 year
相同,但在每年的开始或结束时可能不同,因为一周可能跨越两年,并且 yearOfWeek
根据日历规则选择其中一年。
注意:始终成对使用 weekOfYear
和 yearOfWeek
;不要使用 weekOfYear
和 year
。
请注意以下关于周的错误假设
- 不要假设
weekOfYear
和yearOfWeek
总是存在;它们可能是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.Instant
和Temporal.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 |
浏览器兼容性
加载中…