前端开发 · 2026年3月12日

JavaScript终于修好了Date:Temporal API实战,扔掉moment.js的时候到了

JavaScript的Date对象烂了30年,这不是新闻。2026年3月,Bloomberg的工程师发了一篇长文回顾Temporal提案的9年历程,而真正让我兴奋的是——Chrome 144、Firefox 139已经原生支持Temporal了。不用polyfill,不用装库,直接在浏览器里跑。moment.js、day.js、date-fns这些库每周合计超过1亿次下载,但它们解决的问题本质上是语言层面的缺陷。现在原生方案来了,该认真看看了。

Date到底烂在哪

老生常谈但还是得说,因为这直接影响你理解Temporal为什么这么设计。问题一:可变性

const date = new Date("2026-03-12T00:00:00Z");function addOneDay(d) {  d.setDate(d.getDate() + 1); // 直接改了原对象  return d;}addOneDay(date);console.log(date.toISOString());// "2026-03-13T00:00:00.000Z" — 原始date被改了

这个坑踩过的人应该不少。你以为自己在返回一个新的日期,实际上把原对象改了。在React里这种bug尤其恶心,因为state mutation不会触发重渲染。问题二:月份溢出

const billingDate = new Date("2026-01-31");billingDate.setMonth(billingDate.getMonth() + 1);console.log(billingDate.toDateString());// "Tue Mar 03 2026" — 不是2月28号,是3月3号

想算”下个月的同一天”?Date会把2月31号静默溢出到3月3号。没有任何报错。问题三:解析歧义

new Date("2026-06-25 15:15:00").toISOString();// Chrome: 当作本地时间// 某些引擎: Invalid Date// 老版本FF: 当作UTC

同一段代码在不同浏览器里得到不同结果,这在2026年依然存在。

Temporal核心类型一览

Temporal不是一个简单的Date替代品,它是一套完整的日期时间类型系统。搞清楚用哪个类型是第一步。

类型 包含信息 典型场景
Temporal.Instant 精确时间点(纳秒级) API时间戳、日志、事件记录
Temporal.ZonedDateTime 精确时间 + 时区 + 日历 用户可见的完整日期时间
Temporal.PlainDateTime 日期 + 时间,无时区 表单输入、本地事件
Temporal.PlainDate 仅日期 生日、纪念日
Temporal.PlainTime 仅时间 闹钟、营业时间
Temporal.PlainYearMonth 年+月 账单周期、月报
Temporal.PlainMonthDay 月+日 节日、每年重复事件
Temporal.Duration 时间段 倒计时、时间差计算

关键设计原则:所有Temporal对象都是不可变的。任何修改操作都返回新对象,原对象不变。这一条就干掉了Date最大的坑。

实战:用Temporal重写常见日期操作

别光看API文档,直接上手。以下代码全部可以在Chrome 144+的控制台里直接跑。获取当前时间

// 旧写法const now = new Date();// Temporal写法const now = Temporal.Now.zonedDateTimeISO();console.log(now.toString());// "2026-03-12T08:30:15.123456789+08:00[Asia/Shanghai]"// 时区信息直接带在值里,不用猜console.log(now.timeZoneId); // "Asia/Shanghai"

注意输出里直接包含时区标识[Asia/Shanghai],不是一个偏移量。这意味着夏令时切换时它知道该怎么处理。安全的日期加减

const date = Temporal.PlainDate.from("2026-01-31");// 加一个月 — 自动处理溢出const nextMonth = date.add({ months: 1 });console.log(nextMonth.toString()); // "2026-02-28"// 原对象不变console.log(date.toString()); // "2026-01-31"

Date的月份溢出问题,Temporal默认就修好了。1月31号加一个月得到2月28号(平年)而不是3月3号。夏令时安全的时间运算

// 伦敦夏令时:2026-03-29 凌晨1点跳到2点const zdt = Temporal.ZonedDateTime.from(  "2026-03-29T00:30:00+00:00[Europe/London]");const plus1h = zdt.add({ hours: 1 });console.log(plus1h.toString());// "2026-03-29T02:30:00+01:00[Europe/London]"// 跳过了不存在的01:30,直接到02:30

用Date做这个计算,你得自己查夏令时表然后手动处理。ZonedDateTime直接帮你搞定。跨时区转换

// 一个精确时间点const instant = Temporal.Instant.from("2026-03-12T10:00:00Z");// 转成不同时区的本地时间const shanghai = instant.toZonedDateTimeISO("Asia/Shanghai");const newYork = instant.toZonedDateTimeISO("America/New_York");const london = instant.toZonedDateTimeISO("Europe/London");console.log(shanghai.toPlainTime().toString()); // "18:00:00"console.log(newYork.toPlainTime().toString());  // "06:00:00"console.log(london.toPlainTime().toString());   // "10:00:00"

计算两个日期之间的差值

const start = Temporal.PlainDate.from("2026-01-15");const end = Temporal.PlainDate.from("2026-03-12");const diff = start.until(end);console.log(diff.toString()); // "P56D"console.log(diff.total({ unit: "day" })); // 56// 也可以用更大的单位const diff2 = start.until(end, { largestUnit: "month" });console.log(diff2.toString()); // "P1M25D" — 1个月25天

Duration运算

const duration = Temporal.Duration.from({  hours: 130,  minutes: 20});console.log(duration.total({ unit: "second" })); // 469200console.log(duration.total({ unit: "hour" }));   // 130.333...// 把散的单位整理成更大的单位const balanced = duration.round({  largestUnit: "day"});console.log(balanced.toString()); // "P5DT10H20M"

一个实际场景:会议时间排期

做过国际化项目的都知道,跨时区排会议是噩梦。用Temporal来实现:

function findMeetingTime(hostTz, guestTz, hostTime) {  // 创建主持人视角的时间  const hostZdt = Temporal.PlainDateTime    .from(hostTime)    .toZonedDateTime(hostTz);  // 转换成客人视角  const guestZdt = hostZdt.withTimeZone(guestTz);  // 检查是否在工作时间内(9:00-18:00)  const guestHour = guestZdt.hour;  const isWorkHours = guestHour >= 9 && guestHour < 18;  return {    host: hostZdt.toPlainTime().toString(),    guest: guestZdt.toPlainTime().toString(),    guestTz: guestTz,    workHours: isWorkHours  };}const result = findMeetingTime(  "Asia/Shanghai",  "America/New_York",  "2026-03-12T10:00");console.log(result);// { host: "10:00:00", guest: "22:00:00",//   guestTz: "America/New_York", workHours: false }// 上海早上10点 = 纽约晚上10点,不在工作时间

这种代码用Date写至少要引入moment-timezone,而且还容易在夏令时切换的日子出bug。

浏览器支持现状

截至2026年3月:

浏览器 最低支持版本 状态
Chrome 144 ✅ 已发布
Edge 144 ✅ 已发布
Firefox 139 ✅ 已发布
Safari ❌ Technology Preview中,默认关闭
Chrome Android 145 ✅ 支持
Firefox Android 147 ✅ 支持
Safari iOS ❌ 不支持

Safari是最大的问题。WebKit那边还在实现中,短期内不会有原生支持。如果你的项目需要覆盖Safari用户(几乎所有面向C端的项目都需要),目前有两个选择:方案一:官方Polyfill

npm install @js-temporal/polyfill
import { Temporal } from '@js-temporal/polyfill';// 用法和原生完全一致

polyfill包体积不小(gzip后约40KB),但好处是API跟原生完全一致,等Safari支持后可以无缝切换。方案二:渐进式加载

async function getTemporal() {  if (typeof Temporal !== 'undefined') {    return Temporal; // 原生支持,直接用  }  const { Temporal } = await import('@js-temporal/polyfill');  return Temporal;}

支持原生的浏览器零额外开销,不支持的按需加载polyfill。

从moment.js/day.js迁移

如果你的项目还在用moment.js或day.js,迁移路径大致是:

moment.js / day.js Temporal
moment() Temporal.Now.zonedDateTimeISO()
moment.utc() Temporal.Now.instant()
moment("2026-03-12") Temporal.PlainDate.from("2026-03-12")
.add(1, "month") .add({ months: 1 })
.subtract(7, "days") .subtract({ days: 7 })
.diff(other, "days") .until(other).total({ unit: "day" })
.format("YYYY-MM-DD") .toPlainDate().toString()
.tz("Asia/Shanghai") .withTimeZone("Asia/Shanghai")

需要注意的是Temporal没有内置format方法来做自定义格式化。你需要用Intl.DateTimeFormat或者手动拼字符串。这是一个有意的设计决策——格式化属于i18n层,不属于日期时间库。

const zdt = Temporal.Now.zonedDateTimeISO();// 用Intl格式化const formatted = zdt.toLocaleString("zh-CN", {  year: "numeric",  month: "2-digit",  day: "2-digit",  hour: "2-digit",  minute: "2-digit"});console.log(formatted); // "2026/03/12 08:30"

踩坑记录

实际用下来遇到的几个问题:1. PlainDateTime不能直接比较时间先后因为PlainDateTime不带时区,两个PlainDateTime之间没有绝对的先后关系。上海的下午3点和纽约的下午3点,谁先谁后?不知道。要比较先后,用Instant或ZonedDateTime。2. from()的解析比Date严格得多

Temporal.PlainDate.from("2026/03/12"); // RangeError!Temporal.PlainDate.from("2026-03-12"); // 正确,必须ISO 8601格式

Date那种"什么格式都试试看"的行为,Temporal直接砍掉了。传错格式就报错,没有静默的歧义解析。这是好事。3. Safari用户需要polyfill,包体积是个问题@js-temporal/polyfill gzip后约40KB。如果你的应用对包体积敏感,需要权衡。好消息是可以用dynamic import按需加载。4. 和后端交互时注意序列化格式Temporal.ZonedDateTime的toString()输出类似2026-03-12T08:30:00+08:00[Asia/Shanghai],尾部的[Asia/Shanghai]是RFC 9557格式。如果后端只接受标准ISO 8601,需要用.toInstant().toString()转成UTC格式。

该不该现在切换

实话说:还不是全面替换的时候。Safari不支持是硬伤。面向消费者的Web应用,短期内还得靠polyfill或继续用day.js。但以下场景可以开始用了:立刻可以用的:- Node.js后端服务(v22+已支持,不需要考虑Safari)- 内部工具、管理后台(用户群体确定,可以限定浏览器)- Electron应用- Chrome扩展等Safari支持后再切的:- 面向C端的Web应用- 需要覆盖iOS用户的移动端网页从moment.js时代折腾到day.js到date-fns,日期时间库换了一轮又一轮。Temporal的意义在于,这次是语言层面的修复,不会再有下一个替代库了。9年磨一剑,Bloomberg、Google、Igalia、一堆TC39 champion推了这么久,总算进浏览器了。对于新项目,polyfill+原生的渐进式方案是最稳妥的选择。对于老项目,不急,等Safari跟上再动手也不迟。但API可以先学起来了——这是JavaScript未来30年的日期时间标准。