编程 Node.js 26.0 深度解析:Temporal API 正式默认启用——JavaScript 日期时间处理的 8 年长征终于到达终点

2026-07-12 07:18:16 +0800 CST views 10

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(提案)
2019Stage 2(起草),开始实现
2020Stage 3(候选),开始测试
2022Polyfill 发布,进入 Stage 4 冲刺
2024Chrome 122+ 默认启用
2025Safari 17.4+ 支持,Firefox 跟进
2026-05Node.js 26.0 默认启用

这 8 年主要花在:

  1. 设计稳定性:日期时间的语义在全球化和本地化场景下极其复杂
  2. 时区数据库维护:IANA 时区数据库每季度更新,Temporal 需要同步跟进
  3. 互操作性:要和 Intl API、Date、日历系统等现有 API 共存
  4. 实现正确性:纳秒级精度的时间计算需要严谨的数学基础

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 客户端(fetchhttp.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 前端工程化 性能优化

推荐文章

Vue3中的v-model指令有什么变化?
2024-11-18 20:00:17 +0800 CST
支付宝批量转账
2024-11-18 20:26:17 +0800 CST
Go 开发中的热加载指南
2024-11-18 23:01:27 +0800 CST
程序员茄子在线接单