如果你还在写
new Date(2026, 4, 6)然后疑惑为什么是 5 月——这篇文章就是写给你的。
一、引言:一个等了 30 年的修正
JavaScript 的 Date 对象诞生于 1995 年。那一年,Brendan Eich 用 10 天赶出了 JavaScript 语言,日期处理直接从 Java 的 java.util.Date 抄了过来。讽刺的是,Java 自己在 1.1 版本就弃用了这个实现,但 JavaScript 一用就是 30 年。
这 30 年里,开发者前赴后继地用 moment.js、date-fns、dayjs、Luxon 给 Date 打补丁。本质都是在给一个有缺陷的标准 API 缝缝补补。
2026 年 5 月 5 日,Node.js 26.0.0 发布,Temporal API 默认启用。
这意味着在 Node.js 中,你不再需要 --experimental-temporal 标志,直接 Temporal.Now.plainDateISO() 就能用。这是 JavaScript 日期处理的标志性事件——不是又一个第三方库,而是语言层面的重新设计。
二、Date 的五宗罪:为什么我们需要 Temporal
在认识新 API 之前,先看看旧的到底烂在哪。MDN 官方列举了 Date 的设计缺陷,这里用代码说话:
罪一:月份从 0 开始
const d = new Date(2026, 4, 6); // 4 = 五月?
console.log(d.getMonth()); // 4
// 实际是 5 月 6 日,不是 4 月
这是 off-by-one 错误的万恶之源。1 月是 0,12 月是 11,无数 bug 因此诞生。
罪二:可变性——对象会被悄悄改掉
const meeting = new Date('2026-06-15T14:00:00');
scheduleMeeting(meeting);
// meeting 对象可能在函数内部被 setMonth/setDate 修改了
// 你完全不知道原始值还在不在
Date 的所有 setter 都是原地修改。多个地方引用同一个对象时,一处修改全局影响。
罪三:1 月 31 号加一个月,变成了 3 月 3 号
const today = new Date('2026-01-31');
const nextMonth = new Date(today);
nextMonth.setMonth(today.getMonth() + 1);
console.log(nextMonth.toISOString().slice(0, 10));
// "2026-03-03" —— 2 月没有 31 号,溢出到 3 月了
日期溢出是 Date 最阴险的坑,你以为的"下个月"变成了"下下个月"。
罪四:时区——全靠运气
const d1 = new Date('2026-01-01'); // 字符串解析,可能是 UTC
const d2 = new Date(2026, 0, 1); // 数字参数,一定是本地时间
// 同一天,不同结果,取决于运行环境
Date 没有原生 IANA 时区支持,getTimezoneOffset() 只返回偏移量,不知道 "Asia/Shanghai" 还是 "America/New_York"。夏令时?自己算。
罪五:所有概念塞进一个类型
生日、会议时间、营业时间、信用卡有效期——全都是 Date。你无法在类型层面区分"一个日期"和"一个时间戳",bug 就在概念混淆中滋生。
三、Temporal 的核心思想:用途分型,不可变设计
Temporal 不是给 Date 打补丁,而是彻底重新设计。核心思想只有两条:
- 不可变:所有操作返回新对象,原对象永远不会变
- 用途分型:不同场景用不同的类型,类型即约束
Temporal 提供了以下核心类型:
| 类型 | 用途 | 典型场景 |
|---|---|---|
Temporal.Instant | UTC 绝对时刻 | 服务器日志、时间戳 |
Temporal.ZonedDateTime | 带时区的完整时刻 | 跨时区会议、航班 |
Temporal.PlainDate | 日历日期(无时间) | 生日、合同签署日 |
Temporal.PlainTime | 钟表时间(无日期) | 营业时间、闹钟 |
Temporal.PlainDateTime | 日期+时间(无时区) | 用户输入的日期时间 |
Temporal.Duration | 时间段 | "2 小时 30 分钟" |
Temporal.PlainYearMonth | 年月 | 信用卡有效期 |
Temporal.PlainMonthDay | 月日 | 每年重复的纪念日 |
Temporal 是全局命名空间对象,和 Math、Promise 同级,不能 new Temporal()。
// Node.js 26 直接可用,无需任何标志
console.log(typeof Temporal); // "object"
四、实战对比:Date vs Temporal
4.1 获取今天的日期
// ❌ Date:月份从 0 开始,还要手动拼
const today = new Date();
const y = today.getFullYear();
const m = String(today.getMonth() + 1).padStart(2, '0');
const d = String(today.getDate()).padStart(2, '0');
console.log(`${y}-${m}-${d}`);
// ✅ Temporal:一步到位
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // "2026-05-31"
4.2 日期加减
// ❌ Date:1月31日加1个月,溢出到3月
const date = new Date('2026-01-31');
date.setMonth(date.getMonth() + 1);
console.log(date.toISOString().slice(0, 10)); // "2026-03-03"
// ✅ Temporal:自动约束到2月底
const date = Temporal.PlainDate.from('2026-01-31');
const next = date.add({ months: 1 });
console.log(next.toString()); // "2026-02-28"
add({ months: 1 }) 在跨月边界时会自动"钳制"到目标月的最后一天,不会溢出。这才是人类直觉中的"下个月"。
4.3 计算两个日期之间的差
// ❌ Date:手动算毫秒差,然后换算
const start = new Date('2026-05-08');
const end = new Date('2026-12-15');
const diffMs = end - start;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
console.log(diffDays); // 221,但你不知道是几个月几天
// ✅ Temporal:直接得到结构化结果
const start = Temporal.PlainDate.from('2026-05-08');
const end = Temporal.PlainDate.from('2026-12-15');
const duration = start.until(end, { largestUnit: 'months' });
console.log(`${duration.months} 个月 ${duration.days} 天`); // "7 个月 7 天"
4.4 日期排序
// ❌ Date:转时间戳再排序
const dates = [new Date('2026-12-15'), new Date('2026-05-08'), new Date('2026-08-01')];
dates.sort((a, b) => a - b);
// ✅ Temporal:专用比较方法
const dates = ['2026-12-15', '2026-05-08', '2026-08-01']
.map(s => Temporal.PlainDate.from(s));
dates.sort(Temporal.PlainDate.compare);
// [ '2026-05-08', '2026-08-01', '2026-12-15' ]
五、深入:ZonedDateTime——时区问题终于被正经对待了
时区是 Date 最无力的领域,也是 Temporal 最大的亮点。
5.1 创建带时区的时间
const meeting = Temporal.ZonedDateTime.from({
year: 2026,
month: 10,
day: 24,
hour: 9,
minute: 0,
timeZone: 'Europe/Dublin',
});
console.log(meeting.toString());
// "2026-10-24T09:00:00+01:00[Europe/Dublin]"
IANA 时区标识符是值的一部分,不是运行环境的附属品。
5.2 时区转换:一个方法搞定
const tokyo = meeting.withTimeZone('Asia/Tokyo');
const ny = meeting.withTimeZone('America/New_York');
console.log(tokyo.toPlainDateTime().toString()); // "2026-10-24T17:00:00"
console.log(ny.toPlainDateTime().toString()); // "2026-10-24T04:00:00"
不需要手动查 UTC 偏移、不需要第三方库、不需要担心夏令时——Temporal 内置了 IANA 时区数据库。
5.3 夏令时:1 天 ≠ 24 小时
这是最容易出 bug 的地方。在夏令时切换日,一天可能是 23 小时或 25 小时:
const before = Temporal.ZonedDateTime.from('2026-03-28T20:00[Europe/Dublin]');
const plus24Hours = before.add({ hours: 24 });
const plus1Day = before.add({ days: 1 });
console.log(plus24Hours.toPlainDateTime().toString()); // "2026-03-29T21:00" (时钟快了1小时)
console.log(plus1Day.toPlainDateTime().toString()); // "2026-03-29T20:00" (同一钟表时间)
在夏令时切换日,"加 24 小时"和"加 1 天"结果不同。Date 根本无法表达这个区别,你只能靠运气选对了方法。Temporal 让你显式声明意图,这才是正确的设计。
六、Duration:时间段有了自己的类型
Date 时代,时间段只能用毫秒数表示,遇到"1 个月"这种不确定长度的区间就无能为力。Temporal.Duration 解决了这个问题:
// 创建时间段
const workDay = Temporal.Duration.from({ hours: 8, minutes: 30 });
// 日期加时间段
const start = Temporal.PlainTime.from('09:00:00');
const end = start.add(workDay);
console.log(end.toString()); // "17:30:00"
// 两个日期之间的差就是 Duration
const projectStart = Temporal.PlainDate.from('2026-01-15');
const projectEnd = Temporal.PlainDate.from('2026-06-30');
const timeline = projectStart.until(projectEnd, { largestUnit: 'months' });
console.log(`${timeline.months} 个月 ${timeline.days} 天`); // "5 个月 15 天"
Duration 还支持 total() 方法,可以把时间段换算成任意单位的总数值:
const duration = Temporal.Duration.from({ hours: 2, minutes: 30 });
console.log(duration.total('minutes')); // 150
七、DayOfWeek、DaysInMonth——那些你曾经手写工具函数的属性
Temporal 对象自带丰富的日历属性,告别手写工具函数:
const date = Temporal.PlainDate.from('2026-02-17');
date.year; // 2026
date.month; // 2 (不是 1!月份从 1 开始)
date.day; // 17
date.dayOfWeek; // 2 (周一=1, 周日=7, ISO 标准)
date.daysInMonth; // 28 (自动判断闰年)
date.daysInYear; // 365
date.monthsInYear; // 12
date.inLeapYear; // false
还记得你写过的 getDaysInMonth 函数吗?不需要了。
八、PlainDate vs Instant vs ZonedDateTime:怎么选?
选型其实很简单,一张表搞定:
| 你要表达什么 | 用哪个类型 | 为什么 |
|---|---|---|
| 服务器日志时间戳 | Temporal.Instant | 纯 UTC,精度纳秒,无需时区 |
| "每周一上午 9 点开会" | Temporal.ZonedDateTime | 需要时区,需处理夏令时 |
| 生日、合同日期 | Temporal.PlainDate | 不涉及时刻,只关心日历日期 |
| 营业时间 "9:00-18:00" | Temporal.PlainTime | 不需要日期 |
| 用户输入的日期时间(时区未确定) | Temporal.PlainDateTime | 确定前用这个 |
| 信用卡有效期 | Temporal.PlainYearMonth | 只需年月 |
| 每年纪念日 | Temporal.PlainMonthDay | 只需月日 |
核心原则:能不用时区就不用。PlainDate 足够的场景,不要升级到 ZonedDateTime。
九、迁移指南:从 dayjs/date-fns 到 Temporal
9.1 常见操作的对照表
| 操作 | dayjs | Temporal |
|---|---|---|
| 今天 | dayjs() | Temporal.Now.plainDateISO() |
| 解析日期 | dayjs('2026-05-31') | Temporal.PlainDate.from('2026-05-31') |
| 加 7 天 | dayjs().add(7, 'day') | date.add({ days: 7 }) |
| 格式化 | dayjs().format('YYYY-MM-DD') | date.toString() |
| 两个日期差 | dayjs(end).diff(start, 'day') | start.until(end).total('day') |
| 判断之前 | dayjs(a).isBefore(b) | Temporal.PlainDate.compare(a, b) < 0 |
| 月末 | dayjs().endOf('month') | date.with({ day: date.daysInMonth }) |
9.2 不需要换的部分
如果你正在使用 polyfill(@js-temporal/polyfill),可以直接删掉依赖,Node.js 26 的原生 Temporal API 与 polyfill 完全兼容。
9.3 需要注意的部分
同构代码(Node + 浏览器)暂不能全面切换。 浏览器端的支持情况:
- Chrome 144+ ✅
- Edge 144+ ✅
- Firefox 139+ ✅
- Safari ❌(截至 2026 年 5 月仍未支持)
如果项目需要兼容 Safari,前端仍然需要 polyfill:
// 前端项目:Safari 兼容方案
import { Temporal } from '@js-temporal/polyfill'; // 45KB gzip,规范精确
// 或
import { Temporal } from 'temporal-polyfill'; // 20KB gzip,轻量选择
纯 Node.js 服务端项目没有这个顾虑,现在就可以全面切换。
十、与 Node.js 26 的其他变化一起看
Temporal 不是 Node.js 26 的唯一亮点,但它是最贴近日常开发的一个。其他值得关注的:
- V8 14.6:JIT 和 GC 优化,性能提升;新增
Map.getOrInsert/getOrInsertComputed、Iterator.concat()等 - Undici 8:内置
fetch()的底层引擎升级,HTTP 性能改善 - API 清理:
_stream_*私有模块移除、writeHeader()删除、module.register()运行时弃用
升级建议:生产环境等 2026 年 10 月 Node.js 26 进入 LTS 后再切。新项目和库作者现在就可以开始测试。
十一、总结
Temporal 的意义不只是"比 Date 好用"——它是 JavaScript 第一次在语言层面认真对待日期时间这个领域。不可变设计消除了隐蔽的可变性 bug,类型分离让概念不再混淆,原生时区支持终结了"时区靠运气"的时代。
如果你还在 Node.js 服务端用 dayjs 处理日期,现在有一个不依赖任何第三方库的替代方案了。试试把代码里的 new Date() 换成 Temporal.Now.plainDateISO(),感受一下什么叫"该有的能力,标准就该给"。
参考来源:
- Node.js 26.0.0 Release Announcement
- What's new in Node.js 26 - Node.js Design Patterns
- Node.js 26 发布:Temporal API 默认启用 - CSDN
- Temporal API Is Finally in ECMAScript 2026 - JSManifest
本内容由AI辅助生成