Node.js 26 来了:Temporal API 默认启用,Date 终于可以退休了

如果你还在写 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 开始

js 复制代码
const d = new Date(2026, 4, 6);  // 4 = 五月?
console.log(d.getMonth());        // 4
// 实际是 5 月 6 日,不是 4 月

这是 off-by-one 错误的万恶之源。1 月是 0,12 月是 11,无数 bug 因此诞生。

罪二:可变性------对象会被悄悄改掉

js 复制代码
const meeting = new Date('2026-06-15T14:00:00');
scheduleMeeting(meeting);
// meeting 对象可能在函数内部被 setMonth/setDate 修改了
// 你完全不知道原始值还在不在

Date 的所有 setter 都是原地修改。多个地方引用同一个对象时,一处修改全局影响。

罪三:1 月 31 号加一个月,变成了 3 月 3 号

js 复制代码
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 最阴险的坑,你以为的"下个月"变成了"下下个月"。

罪四:时区------全靠运气

js 复制代码
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 打补丁,而是彻底重新设计。核心思想只有两条:

  1. 不可变:所有操作返回新对象,原对象永远不会变
  2. 用途分型:不同场景用不同的类型,类型即约束

Temporal 提供了以下核心类型:

类型 用途 典型场景
Temporal.Instant UTC 绝对时刻 服务器日志、时间戳
Temporal.ZonedDateTime 带时区的完整时刻 跨时区会议、航班
Temporal.PlainDate 日历日期(无时间) 生日、合同签署日
Temporal.PlainTime 钟表时间(无日期) 营业时间、闹钟
Temporal.PlainDateTime 日期+时间(无时区) 用户输入的日期时间
Temporal.Duration 时间段 "2 小时 30 分钟"
Temporal.PlainYearMonth 年月 信用卡有效期
Temporal.PlainMonthDay 月日 每年重复的纪念日

Temporal 是全局命名空间对象,和 MathPromise 同级,不能 new Temporal()

js 复制代码
// Node.js 26 直接可用,无需任何标志
console.log(typeof Temporal); // "object"

四、实战对比:Date vs Temporal

4.1 获取今天的日期

js 复制代码
// ❌ 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 日期加减

js 复制代码
// ❌ 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 计算两个日期之间的差

js 复制代码
// ❌ 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 日期排序

js 复制代码
// ❌ 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 创建带时区的时间

js 复制代码
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 时区转换:一个方法搞定

js 复制代码
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 小时:

js 复制代码
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 解决了这个问题:

js 复制代码
// 创建时间段
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() 方法,可以把时间段换算成任意单位的总数值:

js 复制代码
const duration = Temporal.Duration.from({ hours: 2, minutes: 30 });
console.log(duration.total('minutes')); // 150

七、DayOfWeek、DaysInMonth------那些你曾经手写工具函数的属性

Temporal 对象自带丰富的日历属性,告别手写工具函数:

js 复制代码
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:

js 复制代码
// 前端项目: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/getOrInsertComputedIterator.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(),感受一下什么叫"该有的能力,标准就该给"。


参考来源


本内容由AI辅助生成

相关推荐
雨季mo浅忆1 小时前
记录前端内网开发之新入职篇
前端·内网开发
杨运交1 小时前
[025][Web模块]基于 Spring Boot 的请求日志过滤器设计与实现
前端·spring boot·后端
孟陬1 小时前
首次上榜新项目 HyperFrames(22k Star):HTML → MP4 一句话生成视频
react.js·node.js·html
IT_陈寒1 小时前
React的useEffect里设状态?我又踩雷了
前端·人工智能·后端
恋猫de小郭1 小时前
GSY 史上最全跨平台/架构/语言的项目,七大项目召唤「神龙」
android·前端·flutter
wgc2k2 小时前
Nest.js基础-5:关于Docker的简单概述
docker·typescript·node.js
范什么特西2 小时前
狂神Vue
前端·javascript·vue.js
时寒的笔记2 小时前
LF11期_day19~20 补环境(三)案例
爬虫·webpack·node.js
怕浪猫2 小时前
Electron 开发实战(六):系统交互与原生功能实战全解
前端·javascript·electron