Node.js 26.0 深度解析:Temporal API 正式默认启用——JavaScript 日期时间处理的 8 年长征终于到达终点
作者:程序员茄子 | 2026-07-12
2026 年 5 月,Node.js 26.0 正式发布,最大的变化只有一个词:Temporal API 默认启用。这个 ECMAScript 提案从提出到落地,整整走了 8 年。本文从 Date 对象的历史包袱讲起,深度剖析 Temporal API 的设计哲学、核心 API 用法、与 Date 的迁移路径,以及 V8 14.6 带来的 Map.getOrInsert() 和 Iterator.concat() 新玩具——配大量可运行代码,让你在生产环境中用起来。
一、从 Date 的原罪说起:为什么 JavaScript 日期处理如此糟糕?
1.1 JavaScript Date 对象的设计缺陷清单
如果你写过 JavaScript 日期处理代码,你一定被这些问题折磨过:
问题一:月份从 0 开始
// 这行代码折磨了无数 JS 开发者 8 年
const date = new Date(2026, 6, 12); // 6 代表 7 月,不是 6 月!
// Java 开发者看到这个会沉默
问题二:月份/天数计算需要手动处理闰年
// 想计算「两个月后的日期」?自己写吧
function addMonths(date, months) {
const result = new Date(date);
result.setMonth(result.getMonth() + months);
// 如果原始日期是 1月31日,setMonth(3) 会自动变成 3月31日
// 但如果原始日期是 1月31日,setMonth(result.getMonth() + 2) 会变成 3月3日(取决于实现)
return result;
}
// 还有一个经典的坑:
const d = new Date('2026-01-31');
d.setMonth(d.getMonth() + 1);
console.log(d.toISOString()); // 2026-03-03T00:00:00.000Z ❌ 跨月日期飘移!
问题三:时间戳精度丢失
// Date.now() 返回毫秒,但很多场景需要纳秒级精度
// JavaScript 的 number 类型只能精确表示 ±2^53 - 1 范围内的整数
// 超过这个范围的毫秒时间戳会产生精度误差
const bigTimestamp = 9007199254741994; // 超过 Number.MAX_SAFE_INTEGER
console.log(bigTimestamp + 1); // 9007199254741995 ❌ 应该 +1
console.log(bigTimestamp + 2); // 9007199254741995 ❌ 应该是 +2,精度丢失!
问题四:时区处理的噩梦
// Date 没有内置的时区感知能力
// 你永远无法确定一个 Date 对象究竟代表哪个时区的时间
const d = new Date('2026-07-12T10:00:00');
// 这个时间是北京时间?UTC 时间?洛杉矶时间?
// Date 对象本身不携带时区信息——它只是一个 UTC 毫秒时间戳
// 格式化时完全依赖运行环境
console.log(d.toString()); // "Sun Jul 12 2026 10:00:00 GMT+0800 (中国标准时间)"
console.log(d.toUTCString()); // "Sun, 12 Jul 2026 02:00:00 GMT"
console.log(d.toISOString()); // "2026-07-12T02:00:00.000Z"
// 同一个 Date,三个不同的输出——你需要同时记住这些 API 才能不踩坑
问题五:字符串解析的不一致性
// 同样的日期字符串,不同浏览器/Node 版本可能解析出不同结果
new Date('2026-07-12'); // Chrome: 2026-07-12T00:00:00.000Z (UTC)
// // Safari: 可能解析为本地时区 00:00
// // IE: 可能直接报错
// ISO 8601 格式带 T 的解析相对可靠,但:
// 1. 带时区偏移的格式支持不一致
// 2. 非 ISO 格式完全是浏览器的实现决定
new Date('2026/07/12'); // 部分环境支持,部分不支持
new Date('July 12, 2026'); // 大多数环境支持,但不是标准格式
问题六:不可变性为零
// Date 是可变的——这是并发场景下的定时炸弹
const bookingDate = new Date('2026-12-25T18:00:00');
// 某个库不小心调用了 bookingDate.setHours(20)
// 你的预订时间就被篡改了!
// 更糟糕的是:没有报错,没有警告,程序继续跑下去
// 直到用户在错误的餐厅等了 2 小时才发现
1.2 为什么 Date 这么难用却没人去修
JavaScript 的 Date 对象是 1995 年 Netscape 时代设计的,直接移植自 Java 的 java.util.Date——而 Java 的日期 API 本身在 2004 年就被 java.time (JSR-310) 彻底替换了。JavaScript 比 Java 还早 4 年「更新」了自己的日期 API(指删掉了部分糟糕的设计),但核心问题一直延续到 2026 年。
原因很简单:Date 的问题太多了,彻底替换的代价太高。
- 数十亿行代码依赖 Date 对象
- 所有库的日期处理逻辑都基于 Date
- 浏览器 API(setTimeout, performance.now())都返回 Date 或时间戳
- TC39(ECMAScript 标准委员会)用了 8 年才把 Temporal 提案推到 Stage 4(完成)
二、Temporal:TC39 八年磨一剑的解决方案
2.1 Temporal 的设计哲学
Temporal API 的设计哲学可以用一句话概括:让每一种日期时间概念都有自己独立的类型。
这个设计思想来自一个简单的问题:「2026年7月12日」和「2026年7月12日 10:00:00 北京时间」是同一个东西吗?
答案是:不是。
- 「2026年7月12日」描述的是一个日历日期,没有时区,没有具体时刻
- 「2026年7月12日 10:00:00 北京时间」描述的是一个具体的瞬间,可以转换为 UTC 或任何其他时区
Date 把这两个完全不同的概念混为一谈,Temporal 彻底把它们拆开了:
Temporal API 的类型体系
├── Instant — 绝对时间点(UTC 纳秒时间戳)
├── ZonedDateTime — 带时区的日期时间(最常用的「完整」时间)
├── PlainDateTime — 不带时区的日期时间(只有日期+时间)
├── PlainDate — 日历日期(只有年月日)
├── PlainTime — 时间(只有时分秒)
├── PlainYearMonth — 年月(只有年和月)
├── PlainMonthDay — 月日(只有月和日)
├── Duration — 时间段(天/时/分/秒/毫秒/纳秒)
├── Relative机关Time — 相对时间(用于定时器)
└── Calendar — 日历系统(支持非公历日历)
2.2 为什么等了 8 年
Temporal 的提案历程:
| 时间 | 里程碑 |
|---|---|
| 2017 | 开始提案,最初叫「Date-Time API」 |
| 2018 | 改名为「Temporal」,进入 TC39 Stage 1(提案) |
| 2019 | Stage 2(起草),开始实现 |
| 2020 | Stage 3(候选),开始测试 |
| 2022 | Polyfill 发布,进入 Stage 4 冲刺 |
| 2024 | Chrome 122+ 默认启用 |
| 2025 | Safari 17.4+ 支持,Firefox 跟进 |
| 2026-05 | Node.js 26.0 默认启用 |
这 8 年主要花在:
- 设计稳定性:日期时间的语义在全球化和本地化场景下极其复杂
- 时区数据库维护:IANA 时区数据库每季度更新,Temporal 需要同步跟进
- 互操作性:要和 Intl API、Date、日历系统等现有 API 共存
- 实现正确性:纳秒级精度的时间计算需要严谨的数学基础
2.3 在 Node.js 26.0 中启用 Temporal
// Node.js 26.0 之前:需要 experimental flag
// node --experimental-temporal index.js
// Node.js 26.0:默认启用,无需任何配置!
// 直接使用:
const { Temporal } = require('temporal-polyfill/global');
// 检查是否可用
console.log(typeof Temporal.Instant); // 'function' ✅
console.log(typeof Temporal.ZonedDateTime); // 'function' ✅
三、核心 API 深度实战
3.1 Instant:处理绝对时间点
Instant 是 Temporal API 中最接近传统 Date 的概念——它代表一个绝对的、与时区无关的时间点。
const { Temporal } = require('temporal-polyfill/global');
// ===== 获取当前时刻 =====
const now = Temporal.Now.instant();
console.log(now.toString());
// "2026-07-12T07:09:00.000000000Z" ← 纳秒精度!Date 只有毫秒
// ===== 从时间戳创建 =====
const instant = Temporal.Instant.fromEpochNanoseconds(1775617140000000000n);
// ↑ ↑
// 纳秒级精度(BigInt) BigInt 是必须的,因为纳秒数超过 Number.MAX_SAFE_INTEGER
console.log(instant.toString()); // "2026-05-05T00:59:00Z"
// ===== 从 ISO 字符串创建 =====
const fromString = Temporal.Instant.from('2026-07-12T10:00:00+08:00');
console.log(fromString.toString()); // "2026-07-12T02:00:00Z" ← 自动转换到 UTC
// ===== 转换为其他类型 =====
const epochMs = instant.epochMilliseconds; // 毫秒级整数(安全范围)
const epochNs = instant.epochNanoseconds; // BigInt 纳秒精度
console.log(`Epoch ms: ${epochMs}`);
// ===== 时间点之间的计算 =====
const start = Temporal.Instant.from('2026-07-01T00:00:00Z');
const end = Temporal.Instant.from('2026-07-12T00:00:00Z');
const diff = start.until(end); // 返回 Duration 对象
console.log(diff.total({ unit: 'days' })); // 11(精确到浮点数)
// ===== Instant 之间的比较 =====
const earlier = Temporal.Instant.from('2026-07-01T00:00:00Z');
const later = Temporal.Instant.from('2026-07-12T00:00:00Z');
console.log(earlier.until(later).total({ unit: 'hours' })); // 264 小时
console.log(earlier < later); // true,直接比较!
3.2 ZonedDateTime:处理带时区的日期时间(最常用)
ZonedDateTime 是大多数场景下你真正需要的类型——它同时包含日期、时间和时区信息。
const { Temporal } = require('temporal-polyfill/global');
// ===== 获取当前时间(带时区)=====
const nowBeijing = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
console.log(nowBeijing.toString());
// "2026-07-12T07:09:00.000000000+08:00[Asia/Shanghai]"
const nowNYC = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(nowNYC.toString());
// "2026-07-11T19:09:00.000000000-04:00[America/New_York]"
// ===== 自动使用系统时区 =====
const now = Temporal.Now.zonedDateTimeISO();
console.log(now.timeZoneId); // "Asia/Shanghai" ← 自动检测
// ===== 创建指定时间点 =====
const flightDeparture = Temporal.ZonedDateTime.from({
year: 2026,
month: 8,
day: 15,
hour: 10,
minute: 30,
timeZone: 'Asia/Shanghai',
});
console.log(flightDeparture.toString());
// "2026-08-15T10:30:00+08:00[Asia/Shanghai]"
// ===== 转换时区(最常见的操作)=====
// 同一个时间点,在不同时区的显示
const utcTime = Temporal.Instant.from('2026-07-12T10:30:00Z');
const inBeijing = utcTime.toZonedDateTimeISO('Asia/Shanghai');
const inNY = utcTime.toZonedDateTimeISO('America/New_York');
const inLondon = utcTime.toZonedDateTimeISO('Europe/London');
console.log(`北京: ${inBeijing.toLocaleString('zh-CN')}`); // "2026/7/12 18:30:00"
console.log(`纽约: ${inNY.toLocaleString('en-US')}`); // "7/12/2026, 6:30:00 AM"
console.log(`伦敦: ${inLondon.toLocaleString('en-GB')}`); // "12/07/2026, 11:30:00"
// 三个地点,同一个瞬间,自动处理 DST(夏令时)
// ===== 加减时间(Immutable!)=====
const deadline = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
const oneWeekLater = deadline.add({ days: 7 });
const threeHoursAgo = deadline.subtract({ hours: 3 });
console.log(deadline.toString()); // 原始值不变!Immutable!
console.log(oneWeekLater.toString()); // 7 天后的时间
console.log(threeHoursAgo.toString()); // 3 小时前的时间
// ===== 计算两个日期之间的差距 =====
const birthday = Temporal.ZonedDateTime.from({
year: 1990, month: 1, day: 1,
timeZone: 'Asia/Shanghai'
});
const today = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
const age = birthday.until(today);
console.log(`年龄: ${age.years}岁 ${age.months}月 ${age.days}天`);
// "年龄: 36岁 6月 11天" ← 自动处理闰年,精确计算
// ===== 提取各个字段 =====
// 在 Date 中需要写一堆 getMonth()/getDate()/getHours()...
const zdt = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
console.log(`
年: ${zdt.year}
月: ${zdt.month}
日: ${zdt.day}
时: ${zdt.hour}
分: ${zdt.minute}
秒: ${zdt.second}
毫秒: ${zdt.millisecond}
纳秒: ${zdt.nanosecond}
星期: ${zdt.dayOfWeek} (1=周一, 7=周日)
年内第几天: ${zdt.dayOfYear} (1-366)
是否闰年: ${zdt.inLeapYear}
时区: ${zdt.timeZoneId}
`);
// 输出结构清晰,每个字段都有对应的 getter,再也不用记 getDate() vs getDay() 的区别了!
3.3 PlainDate / PlainTime / PlainDateTime:处理日历日期
当你只需要日期或时间,不需要时区时:
const { Temporal } = require('temporal-polyfill/global');
// ===== PlainDate:只有年月日 =====
// 适合:生日、节假日、到期日
const christmas = Temporal.PlainDate.from({ year: 2026, month: 12, day: 25 });
const today = Temporal.PlainDate.from('2026-07-12');
console.log(christmas.dayOfWeek); // 5(周五)
console.log(christmas.inLeapYear); // false
// 计算倒计时(比 Date 简单 100 倍)
const daysUntilChristmas = today.until(christmas).days;
console.log(`距圣诞还有 ${daysUntilChristmas} 天`);
// ===== PlainTime:只有时分秒 =====
// 适合:营业时间、闹钟、每日提醒
const businessStart = Temporal.PlainTime.from({ hour: 9, minute: 0 });
const businessEnd = Temporal.PlainTime.from({ hour: 18, minute: 30 });
const now = Temporal.Now.plainTimeISO();
const inBusinessHours = now >= businessStart && now < businessEnd;
console.log(`当前${inBusinessHours ? '在' : '不在'}营业时间`);
// ===== PlainDateTime:日期+时间,无时区 =====
// 适合:日程、预约、事件
const meeting = Temporal.PlainDateTime.from({
year: 2026, month: 7, day: 15,
hour: 14, minute: 30, second: 0
});
console.log(meeting.toString()); // "2026-07-15T14:30:00"
// ===== 类型之间的互相转换 =====
const date = Temporal.PlainDate.from('2026-07-12');
const time = Temporal.PlainTime.from('10:30:00');
// PlainDate + PlainTime → PlainDateTime
const dateTime = date.toPlainDateTime(time);
console.log(dateTime.toString()); // "2026-07-12T10:30:00"
// PlainDateTime + 时区 → ZonedDateTime
const zonedDateTime = dateTime.toZonedDateTime('Asia/Shanghai');
console.log(zonedDateTime.toString()); // "2026-07-12T10:30:00+08:00[Asia/Shanghai]"
3.4 Duration:处理时间段
const { Temporal } = require('temporal-polyfill/global');
// ===== Duration 的构造 =====
const duration = Temporal.Duration.from({ days: 30, hours: 12, minutes: 45 });
console.log(duration.total({ unit: 'hours' })); // 732.75
// ===== 时间段计算 =====
const start = Temporal.Now.instant();
await doSomeWork();
const end = Temporal.Now.instant();
const elapsed = start.until(end);
console.log(`耗时: ${elapsed.total({ unit: 'milliseconds' })} ms`);
console.log(`耗时: ${elapsed.total({ unit: 'seconds' })} s`);
console.log(`耗时: ${elapsed.toLocaleString('en')}`); // "0 hours, 4 minutes, 30.123 seconds"
// ===== 人类可读的时长输出 =====
// Date 计算「2小时30分」需要手动算
// Duration 帮你自动搞定
function formatDuration(duration) {
const parts = [];
if (duration.days > 0) parts.push(`${duration.days}天`);
if (duration.hours > 0) parts.push(`${duration.hours}小时`);
if (duration.minutes > 0) parts.push(`${duration.minutes}分`);
if (duration.seconds > 0 || parts.length === 0) parts.push(`${duration.seconds}秒`);
return parts.join('');
}
const d = Temporal.Duration.from({ days: 2, hours: 5, minutes: 30, seconds: 15 });
console.log(formatDuration(d)); // "2天5小时30分15秒"
// ===== 倒计时/正计时 =====
function countdown(targetDate) {
const now = Temporal.Now.zonedDateTimeISO();
const remaining = now.until(targetDate);
if (remaining.negated.total({ unit: 'seconds' }) > 0) {
return `已过期 ${Math.abs(remaining.negated.total({ unit: 'days' })).toFixed(1)} 天`;
}
return `${remaining.days}天 ${remaining.hours}时 ${remaining.minutes}分`;
}
3.5 Calendar:非公历日历支持
这是 Temporal 最强大的特性之一——它原生支持非公历日历系统:
const { Temporal } = require('temporal-polyfill/global');
// ===== 公历(默认)=====
const gregorian = Temporal.PlainDate.from('2026-07-12');
console.log(gregorian.toLocaleString('en-US')); // "7/12/2026"
// ===== 伊斯兰历 =====
const islamicDate = Temporal.PlainDate.from({
era: 'islamic',
year: 1448,
month: 1,
day: 1,
calendar: 'islamic'
});
console.log(islamicDate.toLocaleString('en-US', { calendar: 'islamic' })); // "6/30/1448 AH"
// ===== 农历 =====
// Temporal 支持 Hebrew, Buddhist, Indian, Islamic, Japanese 等日历
const japaneseDate = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
console.log(japaneseDate.toLocaleString('ja-JP', { calendar: 'japanese' }));
// "2026/7/12" (Reiwa 8)
// ===== 农历转换 =====
// 使用 calendar-system 库可以处理农历等更复杂的日历系统
// Temporal.Instant.fromEpochSeconds(...).toZonedDateTimeISO({ calendar: 'chinese' })
四、迁移实战:从 Date 到 Temporal 的完整指南
4.1 最常见的迁移场景
场景 1:获取当前时间
// ❌ 旧写法
const now = new Date();
const ts = Date.now();
// ✅ 新写法
const now = Temporal.Now.zonedDateTimeISO();
const ts = Temporal.Now.instant().epochMilliseconds;
场景 2:日期字符串解析
// ❌ 旧写法(不可靠)
const d1 = new Date('2026-07-12');
const d2 = new Date('2026/07/12');
const d3 = new Date('July 12, 2026');
// ✅ 新写法(确定性)
const d1 = Temporal.PlainDate.from('2026-07-12'); // ISO 格式
const d2 = Temporal.PlainDate.from({ year: 2026, month: 7, day: 12 }); // 明确参数
// Temporal.Instant.from() 支持更严格的解析,格式不对直接报错,不会有歧义
场景 3:日期计算(加/减)
// ❌ 旧写法(可变,有坑)
const deadline = new Date('2026-07-15');
deadline.setDate(deadline.getDate() + 7); // 可变!
// 如果中间有其他代码也改了 deadline,就出 bug
// ✅ 新写法(不可变,链式)
const deadline = Temporal.PlainDate.from('2026-07-15')
.add({ days: 7 }); // 返回新的 PlainDate,原始值不变
场景 4:日期比较
// ❌ 旧写法
const d1 = new Date('2026-07-12');
const d2 = new Date('2026-07-15');
console.log(d1 < d2); // 可以,但不够直观
// ✅ 新写法
const d1 = Temporal.PlainDate.from('2026-07-12');
const d2 = Temporal.PlainDate.from('2026-07-15');
console.log(d1.equals(d2)); // false(最清晰的比较方式)
console.log(d1.since(d2).days); // -3(d1 比 d2 早 3 天)
console.log(d1.until(d2).days); // 3(d1 到 d2 还有 3 天)
场景 5:格式化输出
// ❌ 旧写法(依赖运行环境)
const d = new Date('2026-07-12T10:30:00+08:00');
console.log(d.toLocaleString('zh-CN')); // 依赖系统时区和 locale
// ✅ 新写法(确定性 + 灵活)
const zdt = Temporal.ZonedDateTime.from({
timeZone: 'Asia/Shanghai',
year: 2026, month: 7, day: 12,
hour: 10, minute: 30, second: 0
});
// Intl API 配合 Temporal,格式化完全确定
console.log(zdt.toLocaleString('zh-CN', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}));
// "2026年7月12日星期日 上午10:30" ← 完全确定,不依赖运行环境
// 格式化特定字段
console.log(zdt.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
}));
// "Jul 12, 2026"
4.2 与第三方库集成
与 Luxon/Moment.js 的对比
// Luxon 的 DateTime 概念上最接近 ZonedDateTime
// Luxon: DateTime.fromISO('2026-07-12T10:30:00+08:00')
// Temporal: Temporal.ZonedDateTime.from('2026-07-12T10:30:00+08:00')
// 主要区别:
// 1. Temporal 是 TC39 标准,原生内置,零依赖
// 2. Temporal 纳秒精度,Luxon 只有毫秒
// 3. Temporal 是 Immutable,Luxon 部分可变
// 4. Temporal 支持更多日历系统
适配层设计(生产环境推荐)
// dateUtils.js:统一的日期时间工具层
const { Temporal } = require('temporal-polyfill/global');
// 工厂函数:统一获取当前时间
function now() {
return Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
}
// 工厂函数:解析用户输入
function parseDate(input) {
if (typeof input === 'string') {
// ISO 格式优先
if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
return Temporal.PlainDate.from(input);
}
// 带时间的 ISO 格式
if (/^\d{4}-\d{2}-\d{2}T/.test(input)) {
return Temporal.ZonedDateTime.from(input);
}
}
if (input instanceof Date) {
// 从 Date 迁移过来的兼容处理
return Temporal.Instant.fromEpochMilliseconds(input.getTime())
.toZonedDateTimeISO('Asia/Shanghai');
}
throw new Error(`无法解析日期: ${input}`);
}
// 格式化输出
function formatDate(date, fmt = 'yyyy-MM-dd HH:mm:ss') {
if (!date) return '';
const zdt = date instanceof Temporal.ZonedDateTime
? date
: date.toZonedDateTimeISO('Asia/Shanghai');
// 手动格式化(比依赖库更可控)
const p = n => String(n).padStart(2, '0');
return fmt
.replace('yyyy', zdt.year)
.replace('MM', p(zdt.month))
.replace('dd', p(zdt.day))
.replace('HH', p(zdt.hour))
.replace('mm', p(zdt.minute))
.replace('ss', p(zdt.second));
}
// 对外暴露统一接口
module.exports = { now, parseDate, formatDate, Temporal };
// 使用示例
const { now, parseDate, formatDate } = require('./dateUtils');
// 设置过期时间(订单等场景)
const orderExpiry = now().add({ days: 7, hours: 23, minutes: 59 });
console.log(formatDate(orderExpiry)); // "2026-07-19 23:59:00"
// 计算用户年龄
const userBirthday = parseDate('1990-07-01');
const userAge = userBirthday.until(now()).years;
console.log(`用户年龄: ${userAge}`); // 精确到月日
4.3 渐进式迁移策略
// 策略:双轨运行,逐步替换
// 第一阶段:Temporal 作为内部计算引擎,输出仍是 Date(兼容现有代码)
function computeWithTemporal(inputDate) {
const temporal = Temporal.PlainDate.from(inputDate);
const result = temporal.add({ days: 30 });
// 输出兼容老代码
return new Date(result.year, result.month - 1, result.day);
}
// 第二阶段:完全迁移到 Temporal,Date 只用于边界(API 输入输出)
async function handleRequest(input) {
// 输入:JSON 中的日期字符串 → Temporal
const startDate = Temporal.PlainDateTime.from(input.startDate);
const endDate = Temporal.PlainDateTime.from(input.endDate);
// 内部:纯 Temporal 计算
const days = startDate.until(endDate).days;
const midpoint = startDate.add({ days: Math.floor(days / 2) });
// 输出:转为 API 需要的格式
return {
startDate: startDate.toString(), // "2026-07-01T00:00:00"
endDate: endDate.toString(),
midpointDate: midpoint.toString(),
totalDays: days,
};
}
// 第三阶段:全面 Temporal 化,包括持久化层
// 使用 Temporal 的 epoch 秒/纳秒存储到数据库
async function saveToDatabase(temporalDate) {
const epochNs = temporalDate.toInstant().epochNanoseconds;
// 存 BigInt,精度不丢失
await db.query('INSERT INTO events (occurred_at_ns) VALUES (?)', [epochNs]);
}
async function loadFromDatabase(row) {
const instant = Temporal.Instant.fromEpochNanoseconds(BigInt(row.occurred_at_ns));
return instant.toZonedDateTimeISO('Asia/Shanghai');
}
五、V8 14.6 新玩具:Map.getOrInsert 和 Iterator.concat
5.1 Map.prototype.getOrInsert(Map Upsert)
这是 ECMAScript 提案 「Upsert」 的核心 API,解决了「查询-不存在则插入」这个经典并发问题:
// ===== 老写法:先查后写(两次操作,有竞态条件)=====
const cache = new Map();
function getUser(id) {
if (cache.has(id)) {
return cache.get(id); // 查
}
// 查不到,从数据库加载
const user = db.loadUser(id);
cache.set(id, user); // 写
return user;
}
// 问题:如果两个并发请求同时查询同一个不存在的 id
// 两个请求都会触发数据库查询,浪费资源
// 更严重的是:如果两次写入之间有其他逻辑,可能产生不一致
// ===== 新写法:getOrInsert(原子操作)=====
const cache = new Map();
function getUser(id) {
return cache.getOrInsert(
id, // 键
() => db.loadUser(id) // 工厂函数:只有键不存在时才调用
);
}
// 工厂函数只调用一次(即使多个并发请求同时到达)
// 底层保证原子性:只执行一次工厂函数
// ===== 更复杂的场景:带默认值的 upsert =====
const user = new Map();
// 记录访问次数(原子操作)
function recordAccess(userId) {
return user.getOrInsert(userId, 0); // 默认值(不是函数)
// 返回现有的值(如果存在)或 0(如果不存在)
}
// ===== getOrInsertComputed:基于键计算默认值 =====
const scoreBoard = new Map();
// 如果用户不存在,用用户名作为默认值
const score = scoreBoard.getOrInsertComputed(
userId, // 键
() => `User ${userId}`, // 键不存在时执行的函数
(key, existingValue) => existingValue // 键存在时执行的函数(可修改)
);
console.log(score); // 如果 userId 存在,返回现有值;否则返回 "User xxx"
// ===== WeakMap 版本 =====
const weakCache = new WeakMap();
const obj = { id: 1 };
// WeakMap 不支持 getOrInsert,但支持 setDefault(在某些实现中)
// 注意:WeakMap 的 getOrInsertComputed 在 V8 14.6 中暂不支持,需要手动处理
5.2 Iterator.concat(迭代器拼接)
// ===== 老写法:拼接多个迭代器 =====
function* range(start, end) {
for (let i = start; i < end; i++) yield i;
}
// 拼接两个迭代器,需要手动处理
function* concat(...iterators) {
for (const it of iterators) {
yield* it;
}
}
const combined = concat(range(1, 5), range(10, 15));
// [1, 2, 3, 4, 10, 11, 12, 13, 14]
// ===== 新写法:Iterator.concat =====
const combined = Iterator.concat(range(1, 5), range(10, 15));
console.log([...combined]); // [1, 2, 3, 4, 10, 11, 12, 13, 14]
// ===== 更实际的例子:分页数据流 =====
async function* fetchAllPages(apiEndpoint) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${apiEndpoint}?page=${page}`);
const data = await response.json();
if (data.items.length === 0) {
hasMore = false;
} else {
yield* data.items; // 生成器返回每个 item
page++;
}
}
}
// 将多个 API 的分页数据合并为一个流
async function* mergeAPIData() {
const apis = [
'https://api.example.com/users',
'https://api.example.com/admins',
];
const iterators = apis.map(url => fetchAllPages(url));
// Iterator.concat 优雅地合并所有迭代器
yield* Iterator.concat(...iterators);
}
// 使用
for await (const item of mergeAPIData()) {
processUser(item);
}
// ===== Iterator.concat 的性能优势 =====
// 1. 惰性求值:只有消费时才会真正获取数据
// 2. 内存友好:不需要先把所有数据加载到数组再拼接
// 3. 链式可组合:Iterator.concat(...arr.map(gen)).filter(...).map(...)
5.3 其他 V8 14.6 小改进
// Map.getOrInsertComputed 的第二个参数函数签名
const m = new Map([['key1', 'value1']]);
const result = m.getOrInsertComputed(
'key1',
(key) => `computed for ${key}`, // 如果键存在,这个函数不执行
(key, existingValue) => existingValue + ' (updated)' // 如果键存在,修改现有值
);
console.log(result); // "value1 (updated)" ← 现有值被更新
// Array.fromAsync 配合 Iterator.concat
// (ECMAScript 2024 已发布,Node.js 26.0 全面支持)
const fileData = await Array.fromAsync(
Iterator.concat(
await import.meta.glob('./data/*.json', { eager: true }),
await import.meta.glob('./data/*.csv', { eager: true })
)
);
六、Undici 8:Node.js 内置 HTTP 客户端的进化
Node.js 26.0 捆绑了 Undici 8——这是 Node.js 内置 HTTP 客户端(fetch、http.request 等)的核心依赖:
// Undici 8 的新特性:连接池自动调优
// 在 Node.js 26.0 中,这些都基于 Undici 8
const response = await fetch('https://api.example.com/data');
// Undici 8 的连接池现在可以自动根据并发请求数量调整连接数
// 之前需要手动配置的参数现在可以自动化:
// 以前:手动设置连接池参数
// const agent = new HttpAgent({
// maxSockets: 25,
// maxFreeSockets: 10,
// timeout: 60000
// });
// 现在:Undici 8 自动管理
// 自动根据活跃请求数增加连接,用完后释放
// 零配置高性能 HTTP 客户端
// Undici 8 新特性:流式请求(Streaming Request Bodies)
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
// 直接传可读流,不需要手动缓冲
body: fs.createReadStream('./large-file.zip'),
duplex: 'half', // 半双工模式,流式上传
});
// Undici 8 新特性:Request/Response 对象直接操作
import { Request, Response } from 'undici';
const req = new Request('https://api.example.com/data', {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' }),
});
const res = await fetch(req);
const data = await res.json();
七、Temporal 的 Node.js 内部支持
7.1 Console 时间戳输出
// Node.js 26.0 的 console.time/timeEnd/timeLog
// 现在可以正确显示纳秒级精度
console.time('operation');
await doSomething();
console.timeEnd('operation');
// operation: 12.345ms ← 毫秒精度(console 的限制)
// 但内部 Temporal API 已经处理了纳秒精度
// performance.now() + Temporal 无缝集成
const start = performance.now();
const startTemporal = Temporal.Now.instant();
await doSomething();
const end = performance.now();
const endTemporal = Temporal.Now.instant();
// 两者可以互相转换
const perfDiff = end - start;
const temporalDiff = startTemporal.until(endTemporal).total({ unit: 'milliseconds' });
console.log(`差异: ${Math.abs(perfDiff - temporalDiff).toFixed(6)}ms`); // 极小差异
7.2 在 Worker Threads 中使用
// worker.js
const { Temporal } = require('temporal-polyfill/global');
// Worker 线程中,Temporal 完全可用
// 但时区数据需要从主线程传递
parentPort.on('message', ({ timeZone }) => {
const result = Temporal.Now.zonedDateTimeISO(timeZone);
parentPort.postMessage({ result: result.toString() });
});
// main.js
const worker = new Worker('./worker.js');
worker.postMessage({ timeZone: 'America/New_York' });
八、生产环境注意事项与已知限制
8.1 性能考量
// Temporal 的性能通常优于手写的 Date 计算
// 但 Instant.from() 有额外的时区解析开销
// 高频场景优化:复用 Temporal 对象
const { Temporal } = require('temporal-polyfill/global');
// ❌ 每次调用都解析时区
for (let i = 0; i < 10000; i++) {
const now = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');
process(now);
}
// ✅ 复用时区对象(减少解析开销)
const tz = Temporal.TimeZone.from('Asia/Shanghai');
for (let i = 0; i < 10000; i++) {
const now = Temporal.Now.instant().toZonedDateTime(tz);
process(now);
}
8.2 序列化与存储
// Temporal 对象 → JSON(存储)
const zdt = Temporal.Now.zonedDateTimeISO();
const serialized = {
// 最安全的序列化方式:保留所有精度
epochNanoseconds: zdt.toInstant().epochNanoseconds.toString(),
timeZone: zdt.timeZoneId,
};
// JSON 存储(BigInt 需要转字符串)
await db.query(
'INSERT INTO logs (ts_ns, tz, message) VALUES (?, ?, ?)',
[serialized.epochNanoseconds, serialized.timeZone, 'some message']
);
// 反序列化
const row = await db.query('SELECT ts_ns, tz FROM logs WHERE id = ?', [id]);
const reconstructed = Temporal.Instant
.fromEpochNanoseconds(BigInt(row.ts_ns))
.toZonedDateTimeISO(row.tz);
// ✅ 另一种方式:直接存 ISO 字符串(简单,但精度可能丢失)
const simpleSerialized = zdt.toString();
// "2026-07-12T07:09:00.000000000+08:00[Asia/Shanghai]"
// 解析回去:
const restored = Temporal.ZonedDateTime.from(simpleSerialized);
// ISO 字符串有足够的精度,解析后 nanosecond 信息不丢失
8.3 已知限制
// 限制 1:Temporal 不能直接替换所有 Date 用法
// 以下场景 Date 仍然更合适:
// 1. 浏览器 DOM API(如 setTimeout, setInterval)
setTimeout(() => {}, 1000); // 只能用 Date/ms
// 2. WebSocket/Network 协议(时间戳字段)
// HTTP Date header 等
// 3. 遗留系统边界
// 如果 API 返回 Date.now() 格式的整数,需要从 Temporal 转回
const epochMs = zdt.toInstant().epochMilliseconds;
// 限制 2:时区数据需要定期更新
// Temporal 的时区数据来自 ICANN Time Zone Database
// 需要通过 Node.js 版本更新来获得最新的时区规则
// 如果有重大时区变更(如某国废除夏令时),需要升级 Node.js
// 限制 3:并非所有浏览器都支持
// Chrome 122+ ✅
// Safari 17.4+ ✅
// Firefox 129+ ✅
// Node.js 26.0+ ✅(主要运行环境)
// 如果要兼容旧版浏览器,需要使用 polyfill
8.4 Polyfill 策略
// 如果需要兼容不支持 Temporal 的环境
// 方案一:temporal-polyfill(Node.js 环境推荐)
const { Temporal } = require('temporal-polyfill/global');
// temporal-polyfill 是 TC39 Temporal 规范的参考实现
// 功能完整,但体积较大(约 200KB gzip)
// 方案二:@js-temporal/polyfill(浏览器环境推荐)
// 更轻量的 polyfill,适合浏览器端使用
// 配合打包工具(Rollup/esbuild)可以 tree-shaking
// 条件加载(最佳方案)
let Temporal;
if (typeof Temporal !== 'undefined' && Temporal.Instant) {
// 原生支持(Node 26+)
Temporal = globalThis.Temporal;
} else {
// 使用 polyfill
Temporal = require('@js-temporal/polyfill');
}
九、总结:前端时间处理的范式转移
Node.js 26.0 默认启用 Temporal API,是 JavaScript 生态系统的一个里程碑事件。这不是一个小功能更新,而是从根本上重新定义了 JavaScript 处理日期和时间的方式:
从混沌到有序:Date 对象把日期、时间、时区混为一谈,导致了无数 bug 和技术债务。Temporal 用类型系统解决了这个问题——每种语义不同的概念都有对应的类型。
从可变到不可变:Temporal 的所有对象都是不可变的。这在并发编程和函数式编程范式下意义重大——再也没有「某个库悄悄修改了你的日期」这种 bug。
从毫秒到纳秒:V8 14.6 的纳秒级时间精度为实时系统、高性能日志、审计追踪等场景提供了更好的支持。
从隐式到显式:Temporal 的 API 设计处处体现显式优于隐式的原则——时区必须显式指定,日历系统必须显式选择,精度必须显式声明。
向后兼容:TC39 和 Node 团队在 Temporal 的设计上花了大量精力确保平滑过渡。polyfill、渐进迁移、与 Date 的互操作——这些都为现有的数十亿行 Date 代码提供了保护。
最后,引用 Temporal 提案作者之一 Philipp Dunkel 的一句话:
"Temporal 不是为了让你写更少的代码,而是为了让你写正确的时间处理代码。"
对于在生产环境中处理日期时间的人来说,这句话的分量,只有踩过 Date 的坑的人才能真正体会。
Tags: Node.js Temporal API JavaScript V8 ES2026 Date ZonedDateTime 时区处理 Undici Map.getOrInsert Iterator.concat 前端工程化 性能优化