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年的日期时间标准。