处理日期和时间一直是 JavaScript 开发者的痛点。原生的 Date 对象设计于上世纪九十年代,API 笨拙、时区支持薄弱、月份从零开始、解析行为不一致,更不用说无法区分本地时间和带时区的绝对时刻。每做一个日历组件、排期系统、跨国协作工具,都要借助 moment.js、date-fns 这类库,否则寸步难行。
好消息是,TC39 已经将 Temporal 提案推进到了 Stage 3,这是即将进入 JavaScript 标准的全新日期时间 API。Temporal 采用了模块化设计,将日期、时间、时区、持续时间等概念拆分为独立的类,提供了不可变对象和清晰的计算接口。Chrome、Firefox、Safari 的最新版本已经内置了实验性支持,通过 @js-temporal/polyfill 也能在旧环境中使用。本文借一个跨时区会议调度系统的完整案例,把 Temporal 的常用类和方法串起来,展示它如何让时间处理变得可预测且安全。
传统 Date 的旧账
先回想一下用 Date 时踩过的坑。创建一个指定日期的对象:
const meetingDate = new Date(2025, 2, 10, 14, 0); // 月份从0开始,实际是3月10日
获取时间戳、计算两天之间的差值,你不得不自己拼凑毫秒数:
const diff = date2.getTime() - date1.getTime();
const days = diff / (1000 * 60 * 60 * 24);
更头疼的是时区。同一个时间戳在不同地区会显示出不同的本地时间,Date 只能依赖运行环境的本地时区,想固定用东京时间来表示一个事件,要么手动做偏移计算,要么借助第三方库。跨时区会议排期简直就是噩梦。
Temporal 则从根子上解决了这些混乱,引入了一系列不可变对象和明确的类型:PlainDate、PlainTime、PlainDateTime、ZonedDateTime、Duration、Instant 等。每个类型表示特定的时间概念,绝对不会混淆。
基础类型速览
在动手写代码前,先认识几个核心类:
- Temporal.PlainDate:纯日期,无时间和时区,例如 2025-03-10。
- Temporal.PlainTime:纯时间,如 14:30:00。
- Temporal.PlainDateTime:日期加时间,无时区。
- Temporal.ZonedDateTime:带时区的日期时间,表示宇宙中某个绝对时刻。
- Temporal.Instant:纳秒精度的时间戳,类似 Unix 纪元。
- Temporal.Duration:一段时间间隔,可以精确到纳秒。
这些对象都是不可变的,每次操作返回新实例,不会意外修改原值。
完整案例:构建一个会议调度器
我们假设要开发一个面向全球团队的会议排期功能。用户可以创建一个会议,设定一个基准时间(如“北京时间 3 月 15 日下午 2 点”),然后系统自动计算每位参会者本地时区下的时间,并支持设置提前 15 分钟的提醒。
我们将用 Temporal 来实现:
- 用
ZonedDateTime存储会议的绝对时刻 - 用
PlainDateTime解析用户输入的本地时间 - 用
Duration计算提醒时刻 - 用
withTimeZone转换到不同时区
假设用户在东京(时区 Asia/Tokyo)输入一个会议时间:2025 年 5 月 20 日 14:00。我们把它转换为一个固定时刻,然后再显示在伦敦和纽约的参会者界面中。
首先,用 Tokyo 时区构建 ZonedDateTime:
import { Temporal } from '@js-temporal/polyfill'; // 如果浏览器尚未原生支持
// 用户输入:东京时间 2025-05-20 14:00
const tokyoDateTime = Temporal.PlainDateTime.from('2025-05-20T14:00');
const tokyoTimeZone = Temporal.TimeZone.from('Asia/Tokyo');
const meetingInstant = tokyoDateTime.toZonedDateTime(tokyoTimeZone);
// 或者直接一行:
// const meetingInstant = Temporal.ZonedDateTime.from('2025-05-20T14:00[Asia/Tokyo]');
toZonedDateTime 并不改变时间数值,它只是给这个“本地时间”附上了时区解释,然后内部计算出对应的绝对时刻。这是 Temporal 最核心的思维转变:把用户看到的时间(PlainDateTime)和全球统一的瞬间(Instant)明确分开。
接下来,我们把这一刻转换成其他参会者的本地时间:
const londonZone = Temporal.TimeZone.from('Europe/London');
const newYorkZone = Temporal.TimeZone.from('America/New_York');
const meetingInLondon = meetingInstant.withTimeZone(londonZone);
const meetingInNewYork = meetingInstant.withTimeZone(newYorkZone);
console.log(meetingInLondon.toString());
// 输出类似:2025-05-20T06:00:00+01:00[Europe/London]
console.log(meetingInNewYork.toString());
// 输出类似:2025-05-20T01:00:00-04:00[America/New_York]
东京下午两点,伦敦是早上六点,纽约是凌晨一点。如果不用 Temporal,手动算时差、处理夏令时,代码可能要多出几十行,而且容易算错。
设置提醒:用 Duration 计算时间偏移
会议开始前 15 分钟需要发送提醒。过去我们用毫秒数加减,Temporal 提供了 subtract 方法和 Duration 对象,单位可以是小时、分钟等,语义清晰:
const reminderOffset = Temporal.Duration.from({ minutes: 15 });
const reminderInstant = meetingInstant.subtract(reminderOffset);
// 再将提醒时刻分别转到参会者时区
const reminderTokyo = reminderInstant.withTimeZone(tokyoTimeZone);
const reminderLondon = reminderInstant.withTimeZone(londonZone);
console.log(`东京提醒:${reminderTokyo.toPlainDateTime().toString()}`);
console.log(`伦敦提醒:${reminderLondon.toPlainDateTime().toString()}`);
减法操作返回一个新的 ZonedDateTime,保证了绝对时刻的准确性。如果恰巧某一天有夏令时切换,Duration 也会正确计算,不会出现偏移错误,因为 Temporal 底层依赖 IANA 时区数据库。
循环会议与日期运算
有时候会议是周期性举行的,比如每月 10 号。我们可以在 PlainDate 基础上用 add 方法:
const baseDate = Temporal.PlainDate.from('2025-05-10');
const nextMeeting = baseDate.add({ months: 1 });
console.log(nextMeeting.toString()); // 2025-06-10
甚至可以直接跳过月底的瀑布问题,Temporal 默认使用 ISO 日历,months: 1 会正确处理诸如 1 月 31 日加一个月变成 2 月 28 日(或闰年 29 日)的情况。
如果用户想要“下一个工作日”的会议,可以用 withCalendar 配合自定义工作日计算,或者自己封装一个函数,基于 dayOfWeek 属性。
序列化与持久化
ZonedDateTime 可以序列化为 ISO 字符串,方便存到数据库或通过 API 传输:
const isoString = meetingInstant.toString();
// 保存并重建
const recovered = Temporal.ZonedDateTime.from(isoString);
这种字符串包含了时区信息,完全自描述,不会因为系统切换时区而丢失原始语义。
兼容性与当下使用方式
截止 2025 年初,Chrome (117+)、Edge (117+)、Firefox (133+) 和 Safari (17+) 均以实验性标志支持 Temporal。生产环境中建议暂时使用 @js-temporal/polyfill,它完全遵循规范,未来浏览器原生支持后移除即可,业务代码无需修改。
安装 polyfill:
npm install @js-temporal/polyfill
导入后全局可用:
import { Temporal } from '@js-temporal/polyfill';
Date.prototype.toTemporalInstant = function() {
return Temporal.Instant.fromEpochMilliseconds(this.getTime());
};
关于第三行,为了更好地与遗留代码交互,polyfill 建议给 Date 加上 toTemporalInstant 方法,这样你可以从旧 Date 对象平滑过渡到 Temporal。
更多细节:避免常见陷阱
不要混用 Plain 和 Zoned。 如果你有一个 PlainDateTime,在没有时区信息的情况下,它不能直接被当作一个时刻来比较。一定要通过 toZonedDateTime 指定时区后再进行跨时区运算。Temporal 严格禁止隐式转换,这能帮你提前发现很多 bug。
Duration 的精确度。 Duration 可以表示纳秒级别的间隔,但并不是所有操作都需要纳秒精度。在进行加法和减法时,注意 Duration 会保留所有单位,极端情况下可能产生非规格化结果,可以通过 round() 方法规整。
日历支持。 Temporal 内置了 ISO 日历,也支持其他日历系统,如 Chinese、Islamic 等,可以通过 withCalendar 转换,在全球化场景中非常有用。
小结
Temporal API 不是为了取代 dayjs 或 date-fns 这些生态库而设计的,它是在语言底层提供一套可靠、安全的时间处理基石。跨时区会议调度这个案例只是冰山一角,但已经涵盖了 ZonedDateTime、PlainDateTime、Duration 和时区转换这几个最常用的场景。一旦开始在项目中使用,你会发现自己不再需要记住月份偏移量,不再担心夏令时,也不再为“这个时间到底是哪个时区的”而困惑。时间处理终于变成了它该有的样子——清晰、明确、不可变。

