Temporal API、管道运算符、Record & Tuple、using 语法。但查完 TC39 官方记录后发现,四个里只有两个真的进了 ES2026。我看了不少文章,基本都是把四个混在一起写。实际上有的上线了,有的没有通过。
一、先搞清楚:ES2026 到底包含什么?
ES2026 是 ECMAScript 的 2026 年版本,由 TC39 委员会制定。一个特性要进 ES2026,必须在 2026 年的 TC39 会议上达到 Stage 4(最终阶段)。
简单说下 TC39 的流程:
Stage 0 → 提出想法
Stage 1 → 值得进一步探索
Stage 2 → 初步规范,草案阶段
Stage 3 → 规范完整,等待实现和反馈
Stage 4 → 最终确认,进入下一年度 ECMAScript
一个特性从 Stage 0 到 Stage 4,短则一两年,长则近十年。Temporal API 花了整整 9 年。
2026 年 3 月的 TC39 会议上,一批提案达到 Stage 4,正式进入 ES2026。其中跟前端日常开发关系最密切的是这两个:
| 特性 | Stage 4 时间 | 核心价值 |
|---|---|---|
| Temporal API | 2026 年 3 月 | 原生日期时间处理,彻底替代 Date |
| using(显式资源管理) | 2025 年 5 月 | 自动资源清理,告别 try/finally 样板代码 |
而另外两个经常被提到的特性:
| 特性 | 当前状态 | 说明 |
|---|---|---|
| Record & Tuple | ❌ 2025 年 4 月被撤回 | 停在 Stage 2,被毙了 |
| 管道运算符 |> | ❌ 仍在 Stage 2 | 官方明确说"今年不可能到 Stage 4" |
下面逐个讲。
二、Temporal API:Date 的继任者,等了 9 年
2.1 为什么 Date 非被替代不可
如果你写过前端,大概率被 Date 折磨过。举个最经典的坑:
js
const date = new Date(2026, 6, 22);
// 你以为创建的是 7 月 22 日?
// ✅ 没错,确实是 7 月 22 日。但月份是从 0 开始的------6 代表 7 月
// 第一次遇到这个设计的人,多半会骂一句
月份从 0 开始这个设计,源自 Java 1.0 时代的 java.util.Date,JS 原样抄了过来。这个"历史包袱"导致无数 off-by-one bug。
但月份 0-index 只是冰山一角。Date 真正的问题是缺少类型区分------它把"日期""时间""时刻""时段"全部混在一个对象里:
js
// 你想表示"2026年7月22日"这个纯日期(不含时区)
const d1 = new Date(2026, 6, 22);
// 但 d1 内部存的是 UTC 毫秒时间戳,一旦跨时区,显示就不一样了
// 在东八区显示 2026-07-22 00:00:00
// 在零时区显示 2026-07-21 16:00:00 ← 差了一天!
// 你想表示"下午3点"这个纯时间
// Date 做不到,只能 new Date() 然后设置小时
const d2 = new Date();
d2.setHours(15, 0, 0);
// 但它仍然带了一个日期和时区
再比如日期运算:
js
// "30天后是几号?"------用 Date 写,得这样
const now = new Date();
const future = new Date(now);
future.setDate(now.getDate() + 30);
// 如果跨了月底或年底,setDate 会自动进位...大部分时候
// 但你确定它能处理闰年、闰秒、夏令时吗?每次写都得查一遍
// 两个日期相差多少天?
const diff = (d2 - d1) / (1000 * 60 * 60 * 24);
// 这个除法能记住吗?毫秒→秒→分→时→天,四层转换
说实话,每次写日期相关的代码,我都要对着文档确认一遍。而 Temporal API 就是为了彻底解决这些问题而生的。
2.2 Temporal 的核心设计:类型分离
Temporal 最关键的设计理念是类型分离------不同概念用不同类型表示,不混在一起。
自己先看一眼这张全景图,对整体有个感知:
scss
┌─────────────────────────────────────────────────────────────────┐
│ Temporal API 类型全景 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Instant │ │ ZonedDateTime│ │ │ │
│ │ 时间线上的 │ │ 带时区的 │ │ Duration │ │
│ │ 精确时刻 │ │ 日期时间 │ │ 时间长度 │ │
│ │ (UTC纳秒) │ │ (如东八区) │ │ (如 "2天3小时") │ │
│ └──────────────┘ └──────────────┘ └───────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Plain 类型族(不含时区信息) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │ │
│ │ │ PlainDate │ │ PlainTime │ │ PlainDateTime │ │ │
│ │ │ 2026-07-22 │ │ 15:30:00 │ │ 2026-07-22 15:30 │ │ │
│ │ │ 纯日期 │ │ 纯时间 │ │ 日期+时间(无时区) │ │ │
│ │ └────────────┘ └────────────┘ └────────────────────┘ │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ PlainYearMonth │ PlainMonthDay │ │ │
│ │ │ 2026-07 │ 07-22 │ │ │
│ │ │ 年月 │ 月日(无年份) │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
核心逻辑就是**"含时区"用 ZonedDateTime,"不含时区"用 Plain 系列**。
下面逐个类型的介绍。
2.3 PlainDate 和 PlainTime:纯日期和纯时间
最常用的两个类型。当你只关心"几月几号"或"几点几分",不关心时区时,用这俩。
js
// 纯日期:2026年7月22日
const date = Temporal.PlainDate.from({ year: 2026, month: 7, day: 22 });
console.log(date.toString()); // "2026-07-22"
// 👆 月份是从 1 开始的!终于正常了
// 纯时间:下午3点30分
const time = Temporal.PlainTime.from({ hour: 15, minute: 30 });
console.log(time.toString()); // "15:30:00"
// 也可以用字符串创建
// 效果等价于上方代码效果
const date2 = Temporal.PlainDate.from("2026-07-22");
const time2 = Temporal.PlainTime.from("15:30:00");
注意第一个细节:Temporal 的月份从 1 开始。就这一条,就能少出无数 bug。
2.4 Instant:时间线上的精确时刻
Instant 表示 UTC 时间线上的一个精确时刻,和 Unix 时间戳是同一个概念,只是精度更高(纳秒级)。
js
// 当前时刻
const now = Temporal.Now.instant();
console.log(now.epochMilliseconds); // 1721630400000(和 Date.now() 一样)
// 从时间戳创建
const instant = Temporal.Instant.fromEpochMilliseconds(1721630400000);
// 从 ISO 8601 字符串创建
const instant2 = Temporal.Instant.from("2026-07-22T08:00:00Z");
// 👆 末尾的 Z 表示 UTC
Instant 的核心特点:它只有一个值,不受时区影响。无论你在地球哪个角落,同一个 Instant 表示的是同一个时刻。时区只影响"显示",不影响"值"。
2.5 ZonedDateTime:带时区的完整日期时间
这是 Temporal 中功能最全的类型------日期、时间、时区全都有。
js
// 从字符串创建(带时区)
const meeting = Temporal.ZonedDateTime.from(
"2026-07-22T15:00:00+08:00[Asia/Shanghai]"
);
console.log(meeting.year); // 2026
console.log(meeting.month); // 7
console.log(meeting.day); // 22
console.log(meeting.hour); // 15
console.log(meeting.timeZoneId); // "Asia/Shanghai"
// 转换到其他时区看看
const tokyo = meeting.withTimeZone("Asia/Tokyo");
console.log(tokyo.hour); // 16(东京比上海快1小时)
const ny = meeting.withTimeZone("America/New_York");
console.log(ny.hour); // 3(纽约和上海差12小时,夏令时期间差12小时)
举个实际场景:你在上海,要和一个在纽约的同事开会,约好了"北京时间下午 3 点"。用 Date 写时区转换代码很痛苦,用 Temporal 就很自然:
js
// 北京时间 7月22日下午3点
const shanghaiTime = Temporal.ZonedDateTime.from(
"2026-07-22T15:00:00+08:00[Asia/Shanghai]"
);
// 纽约同事看到的是几点?
const nyTime = shanghaiTime.withTimeZone("America/New_York");
console.log(`纽约时间: ${nyTime.year}-${nyTime.month}-${nyTime.day} ${nyTime.hour}:${nyTime.minute}`);
// 纽约时间: 2026-7-22 3:0
// 👆 凌晨3点... 纽约同事可能不太开心
2.6 Duration:时间长度
Duration 表示一段时间长度,比如"2 天 3 小时"。它和 Instant/PlainDate 不同------Duration 不是某个时间点,而是时间点之间的"距离"。
js
// 创建一个 Duration
const duration = Temporal.Duration.from({ days: 2, hours: 3 });
console.log(duration.toString()); // "P2DT3H"
// 👆 ISO 8601 持续时间格式:P 开头,T 分隔日期和时间
// 日期运算:30天后是几号?
const today = Temporal.Now.plainDateISO();
const future = today.add(Temporal.Duration.from({ days: 30 }));
console.log(future.toString()); // 自动处理月底、年底进位
// 两个日期相差多少天?
const d1 = Temporal.PlainDate.from("2026-07-01");
const d2 = Temporal.PlainDate.from("2026-07-22");
const diff = d1.until(d2);
console.log(diff.toString()); // "P21D"(21天)
// 反过来也行
const diff2 = d2.since(d1);
console.log(diff2.toString()); // "P21D"
还记得前面用 Date 写日期运算时那个 / (1000 * 60 * 60 * 24) 吗?Temporal 直接 until() / since() 就完事了。
2.7 日期比较
Temporal 的比较也很直观:
js
const d1 = Temporal.PlainDate.from("2026-07-01");
const d2 = Temporal.PlainDate.from("2026-07-22");
console.log(d1.equals(d2)); // false
console.log(d1.compare(d2)); // -1(d1 < d2)
// compare 返回 -1 / 0 / 1,和 Array.prototype.sort 的比较器一致
// 实用场景:判断是否在某个范围内
const start = Temporal.PlainDate.from("2026-07-01");
const end = Temporal.PlainDate.from("2026-07-31");
const target = Temporal.PlainDate.from("2026-07-22");
const isInRange = start.compare(target) <= 0 && target.compare(end) <= 0;
console.log(isInRange); // true
2.8 格式化和解析
Temporal 支持 ISO 8601 格式,也可以用 toLocaleString 做本地化:
js
const zdt = Temporal.ZonedDateTime.from(
"2026-07-22T15:30:00+08:00[Asia/Shanghai]"
);
// ISO 8601 格式
console.log(zdt.toString()); // "2026-07-22T15:30:00+08:00[Asia/Shanghai]"
// 本地化格式
console.log(zdt.toLocaleString("zh-CN", {
dateStyle: "full",
timeStyle: "long"
}));
// "2026年7月22日星期三 中国标准时间 下午3:30:00"
// 只取日期部分
console.log(zdt.toPlainDate().toLocaleString("zh-CN"));
// "2026/7/22"
2.9 浏览器支持情况
截至 2026 年 7 月:
| 运行时 | 支持情况 |
|---|---|
| Chrome 144+ | ✅ 原生支持 |
| Firefox 139+ | ✅ 原生支持 |
| Safari | ⚠️ Technology Preview 中,正式版未支持 |
| Node.js 24+ | ⚠️ 需 --harmony-temporal flag |
| Node.js 22- | ❌ 不支持 |
过渡期可以使用 polyfill:
bash
npm install @js-temporal/polyfill
js
// 在不支持原生 Temporal 的环境中
import { Temporal } from "@js-temporal/polyfill";
// 用法和原生 API 完全一致
现在就可以在项目中用起来。可以使用 polyfill 兜底,等浏览器全面支持后去掉 polyfill 就行。API 不会变,代码不用改。
三、using:自动资源清理,告别 try/finally
3.1 它解决了什么问题
先看一段熟悉的代码。你在读一个文件流,读到一半出错了:
js
async function readFile(url) {
const response = await fetch(url);
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 处理数据...
// 😱 如果这里抛错了?
}
} finally {
reader.releaseLock(); // 必须在 finally 里手动释放
}
}
这个模式的问题不在于"难写",而在于容易忘 。finally 里的清理代码,平时没人注意它,直到线上出事------文件句柄泄漏、数据库连接打满、定时器没清除------才发现忘了写。
C# 有 using,Python 有 with,Java 有 try-with-resources,都是解决同一个问题。现在 JS 也有了。
3.2 using 的基本用法
using 声明一个变量,在作用域结束时自动调用它的 [Symbol.dispose]() 方法:
js
{
using resource = {
name: "数据库连接",
[Symbol.dispose]() {
console.log(`清理: ${this.name}`);
}
};
console.log("使用资源中...");
// 即便这里抛错了,dispose 也会被调用
}
// 输出:
// 使用资源中...
// 清理: 数据库连接
注意:using 只能在有花括号 {} 的作用域内使用 ------代码块、函数体、for 循环里都行,但顶层不行。
3.3 自定义可释放对象
任何对象只要有 [Symbol.dispose]() 方法,就能用 using 管理。举个实际例子------数据库连接:
js
class DBConnection {
constructor(url) {
this.url = url;
console.log(`连接数据库: ${url}`);
}
query(sql) {
console.log(`执行查询: ${sql}`);
return { rows: [] };
}
[Symbol.dispose]() {
console.log(`关闭数据库连接: ${this.url}`);
// 这里放真正的关闭逻辑
}
}
// 使用
{
using db = new DBConnection("mysql://localhost:3306/mydb");
db.query("SELECT * FROM users");
db.query("SELECT * FROM orders");
// 离开作用域时,自动调用 [Symbol.dispose]()
}
// 输出:
// 连接数据库: mysql://localhost:3306/mydb
// 执行查询: SELECT * FROM users
// 执行查询: SELECT * FROM orders
// 关闭数据库连接: mysql://localhost:3306/mydb
不用写 try/finally,不用记着调 close()------离开花括号就自动清理。
3.4 await using:异步资源清理
有些资源的清理是异步的(比如关闭网络连接、刷新缓冲区)。这时候用 await using:
js
class FileHandle {
constructor(path) {
this.path = path;
}
async write(data) {
console.log(`写入 ${data} 到 ${this.path}`);
}
async [Symbol.asyncDispose]() {
console.log(`异步关闭文件: ${this.path}`);
await flushBuffer(this.path); // 假设有个刷新缓冲区的异步操作
}
}
async function processFile() {
await using file = new FileHandle("/tmp/data.txt");
await file.write("hello");
await file.write("world");
// 离开作用域时,自动 await [Symbol.asyncDispose]()
}
// 输出:
// 写入 hello 到 /tmp/data.txt
// 写入 world 到 /tmp/data.txt
// 异步关闭文件: /tmp/data.txt
await using 和 using 的区别就一个:前者等 [Symbol.asyncDispose]() 执行完再继续,后者同步调 [Symbol.dispose]()。
3.5 DisposableStack:批量管理多个资源
当多个资源需要一起清理时,用 DisposableStack:
js
{
using stack = new DisposableStack();
const db = stack.use(new DBConnection("mysql://localhost/mydb"));
const cache = stack.use(new RedisConnection("redis://localhost"));
const file = stack.use(new FileHandle("/tmp/output.txt"));
// 正常使用
db.query("SELECT 1");
cache.set("key", "value");
file.write("result");
// 离开作用域时,按入栈的逆序释放:
// 1. 关闭文件
// 2. 关闭 Redis
// 3. 关闭数据库
}
逆序释放是个重要设计------资源之间可能有依赖关系,后创建的依赖先创建的,所以释放时要反过来。
DisposableStack 还有几个方法:
js
{
using stack = new DisposableStack();
// use():添加一个已有 [Symbol.dispose] 的资源
stack.use(resourceWithDispose);
// adopt():给一个没有 [Symbol.dispose] 的对象加上清理逻辑
stack.adopt(reader, (r) => r.releaseLock());
// defer():添加一个纯清理动作(不需要关联资源)
stack.defer(() => console.log("收尾工作"));
// move():把当前 stack 的资源转移到新 stack
const newStack = stack.move();
}
3.6 一个实战场景:SSE 流式请求
举个前端常见的场景------SSE(Server-Sent Events)流式请求。用 using 管理连接的关闭:
js
async function streamChat(url, onMessage) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
// 用 DisposableStack 管理 reader
using stack = new DisposableStack();
stack.adopt(reader, (r) => r.cancel());
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
onMessage(text);
}
} finally {
// stack 离开作用域时自动 cancel reader
// 这里 finally 其实都不用写,因为 using 已经兜底了
}
}
// 调用
await streamChat("/api/chat", (msg) => {
console.log("收到:", msg);
});
// 函数结束后,reader 自动被 cancel,连接关闭
3.7 SuppressedError:清理时的错误处理
如果资源清理本身抛了错,而代码块内也有错误,using 会用 SuppressedError 把两个错误都包起来:
js
{
using resource = {
[Symbol.dispose]() {
throw new Error("清理时出错了");
}
};
throw new Error("业务代码出错了");
}
// 抛出: SuppressedError
// - error: "清理时出错了"(最近抛出的)
// - suppressed: "业务代码出错了"(被掩盖的)
这个设计确保你不会丢失任何一个错误信息。
3.8 浏览器支持情况
| 运行时 | 支持情况 |
|---|---|
| Chrome 134+ | ✅ 原生支持 |
| Firefox 134+ | ✅ 原生支持 |
| Safari | ❌ 暂不支持 |
| Node.js 24+ | ✅ 支持 |
| Babel | ✅ 可编译 |
和 Temporal 不同,using 在 Node.js 24+ 上已经原生支持,不需要 flag。
3.9 哪些场景适合用 using
说实话,using 不像 Temporal 那样"每个项目都要用"。它更适合有明确生命周期管理的场景:
| 场景 | 适合度 | 说明 |
|---|---|---|
| 文件/流操作 | ⭐⭐⭐⭐⭐ | reader、writer、file handle |
| 数据库连接 | ⭐⭐⭐⭐⭐ | 连接池、事务 |
| WebSocket / EventSource | ⭐⭐⭐⭐ | 需要手动关闭的连接 |
| 定时器 | ⭐⭐⭐ | setTimeout/setInterval 可包装 |
| 性能计时 | ⭐⭐⭐ | performance.mark/measure 可包装 |
| 普通业务逻辑 | ⭐ | 没有清理需求就别硬用 |
不要为了用而用。如果你的代码里没有"打开→使用→关闭"的模式,using 对你没有价值。
四、被毙的 Record & Tuple:为什么被撤回?
4.1 它原本要解决什么问题
JS 有个经典"痛点"------对象和数组是引用类型,{} !== {}:
js
// 两个"看起来一样"的对象,不相等
const a = { x: 1, y: 2 };
const b = { x: 1, y: 2 };
console.log(a === b); // false
// 数组也一样
console.log([1, 2, 3] === [1, 2, 3]); // false
这导致用对象做 Map 的 key 时很麻烦,深比较需要手动遍历或用 lodash 的 isEqual。
Record & Tuple 提案想引入值类型 的数据结构------#{x: 1, y: 2} 和 #[1, 2, 3],它们按值比较:
js
// 提案中的语法(已撤回,无法使用)
const a = #{ x: 1, y: 2 };
const b = #{ x: 1, y: 2 };
console.log(a === b); // true(如果提案通过的话)
4.2 为什么被撤回
2025 年 4 月 14 日的 TC39 会议上,经过讨论后达成共识:撤回该提案。核心原因是委员会无法就"是否给 JS 添加新的原始类型"达成一致。
具体争议点包括:
- 引擎实现成本高------新的原始类型意味着所有 JS 引擎(V8、SpiderMonkey、JSC)都要修改 GC、内存布局、性能优化路径,工程量巨大
- 与现有生态的兼容问题 ------
JSON.parse返回的是普通对象,不是 Record;第三方库的 API 都是基于引用类型设计的 Object.freeze已经存在------虽然不能实现值比较,但能保证不可变性,部分场景够用- 深度相等比较的替代方案已经够多 ------
structuredClone+ 自定义比较函数、lodash 的isEqual、immutable.js 等
4.3 现在用什么替代
Record & Tuple 被毙了,但"值比较"的需求还在。目前可用的方案:
方案一:Object.freeze(简单场景)
js
// 冻结对象,防止修改
const config = Object.freeze({ x: 1, y: 2 });
// config.x = 100; // 静默失败(严格模式抛错)
// 但 freeze 不解决比较问题
const a = Object.freeze({ x: 1 });
const b = Object.freeze({ x: 1 });
console.log(a === b); // 仍然 false
方案二:JSON 序列化做 key(需要值比较时)
js
// 用 JSON 字符串作为 Map 的 key
const cache = new Map();
const key = JSON.stringify({ x: 1, y: 2 });
cache.set(key, "result");
// 取的时候也序列化
const key2 = JSON.stringify({ x: 1, y: 2 });
console.log(cache.get(key2)); // "result" ✅
方案三:第三方库(大型项目)
js
// Immutable.js
import { Map } from "immutable";
const a = Map({ x: 1, y: 2 });
const b = Map({ x: 1, y: 2 });
console.log(a.equals(b)); // true ✅
// 或用 Ramda
import { equals } from "ramda";
console.log(equals({ x: 1 }, { x: 1 })); // true ✅
方案四:关注 Composites 提案(未来)
Record & Tuple 撤回后,社区在讨论一个替代方案------Composites 提案。它不引入新的原始类型,而是给现有对象提供"值语义"的比较能力。目前还在非常早期的讨论阶段,没有任何规范文本,可以关注但别等。
五、还没到的管道运算符
5.1 它要解决什么问题
函数嵌套多了,可读性就很差:
js
// 一堆函数嵌套,从内往外读
const result = format(
sanitize(
parse(
readFile("data.txt")
)
)
);
管道运算符 |> 把嵌套变成链式调用,从左往右读:
js
// 提案中的语法(仍是 Stage 2,未进入 ES2026)
const result = readFile("data.txt")
|> parse(%)
|> sanitize(%)
|> format(%);
// % 是占位符,表示前一步的结果
这个语法借鉴自 Elixir、F#、Elm 等语言,在这些语言里已经验证了可读性很好。
5.2 为什么还没进 ES2026
管道运算符从 2017 年就提出了,至今 8 年仍停在 Stage 2。核心争议是占位符的语法:
- Hack 风格 (当前草案):
x |> f(%),用%做占位符 - F# 风格 (早期方案):
x |> f,默认传给第一个参数 - Smart Mix 风格:不用占位符,但只支持单参数
委员会在几种风格之间来回讨论,一直没能达成共识。2024 年的 issue 里官方说了一句:"管道运算符今年不可能到 Stage 4"。
5.3 现在用什么替代
方案一:链式调用(如果函数支持)
js
// 很多库已经支持链式调用
const result = _
.chain(readFile("data.txt"))
.thru(parse)
.thru(sanitize)
.thru(format)
.value();
// lodash 的 chain 模式
方案二:中间变量(最朴实但最有效)
js
const content = readFile("data.txt");
const parsed = parse(content);
const sanitized = sanitize(parsed);
const result = format(sanitized);
// 多几行代码,但谁都能看懂
方案三:自定义 pipe 函数
js
// 一个极简的 pipe 实现
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), 0);
const transform = pipe(parse, sanitize, format);
const result = transform(readFile("data.txt"));
说实话,方案二和方案三已经能覆盖 90% 的场景。管道运算符的"语法糖"价值大于"功能"价值------它不会让你做到之前做不到的事,只是让代码更好读。
六、总结:ES2026 速查
来一张知识图谱,把全文串起来:
vbnet
┌─────────────────────────────── ES2026 ─────────────────────────────┐
│ │
│ ✅ 真上线的 │
│ ┌───────────────────────┐ ┌───────────────────────┐ │
│ │ Temporal API │ │ using (ERM) │ │
│ │ │ │ │ │
│ │ · PlainDate/Time │ │ · using + dispose │ │
│ │ · Instant │ │ · await using │ │
│ │ · ZonedDateTime │ │ · DisposableStack │ │
│ │ · Duration │ │ · SuppressedError │ │
│ │ · 月份从 1 开始 ✅ │ │ · 替代 try/finally │ │
│ │ · 替代 Date │ │ │ │
│ │ · Chrome 144+, FF 139+│ │ · Chrome 134+, FF 134+│ │
│ │ · Node 24+ (flag) │ │ · Node 24+ (原生) │ │
│ └───────────────────────┘ └───────────────────────┘ │
│ │
│ ❌ 没进的 │
│ ┌───────────────────────┐ ┌───────────────────────┐ │
│ │ Record & Tuple │ │ 管道运算符 |> │ │
│ │ │ │ │ │
│ │ · 2025/04 被撤回 │ │ · 仍在 Stage 2 │ │
│ │ · 原因:新原始类型 │ │ · 争议:占位符语法 │ │
│ │ 争议太大 │ │ · 替代:中间变量 / │ │
│ │ · 替代:freeze / │ │ 自定义 pipe 函数 │ │
│ │ JSON.stringify / │ │ │ │
│ │ Immutable.js │ │ │ │
│ └───────────────────────┘ └───────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
总之就是:
- Temporal API 是 ES2026 最重要的特性------Date 终于有继任者了,月份从 1 开始,类型分离,时区处理不再痛苦。现在就可以用 polyfill 上项目。
- using 是锦上添花------有资源管理的场景用起来很爽,没有也别硬用。Record & Tuple 被毙了,管道运算符还在路上,别被错误信息误导。
七、面试题尝鲜
Q1:Temporal API 和 Date 的主要区别是什么?
答: 主要区别有三点:
- 类型分离------Date 把日期、时间、时刻混在一个对象里,Temporal 拆成 PlainDate(纯日期)、PlainTime(纯时间)、Instant(精确时刻)、ZonedDateTime(带时区日期时间)等
- 月份从 1 开始------Date 的月份是 0-indexed(0-11),Temporal 是 1-indexed(1-12)
- 不可变 ------Date 的方法会修改原对象(如
setHours()),Temporal 的所有操作都返回新对象
Q2:using 和 try/finally 有什么区别?
答: using 是 try/finally 清理模式的语法糖,但有三点优势:
- 声明即管理 ------
using声明的同时就绑定了清理逻辑,不需要单独写finally块 - 不会忘 ------
using在作用域结束时自动调用[Symbol.dispose](),不需要手动调用清理方法 - 错误不丢失 ------如果清理时也抛错,
using会用SuppressedError保存被掩盖的错误,而try/finally中后抛的错会覆盖先抛的
Q3:Record & Tuple 提案为什么被撤回?
答: 2025 年 4 月 TC39 会议上达成共识撤回,核心原因是无法就"是否给 JS 添加新的原始类型"达成一致。主要争议包括:
- 引擎实现成本高------所有 JS 引擎需修改 GC 和内存布局
- 与现有生态不兼容------
JSON.parse返回普通对象而非 Record Object.freeze和第三方库(Immutable.js)已部分覆盖需求- 社区在讨论替代方案 Composites 提案,但仍在早期阶段
Q4:管道运算符 |> 解决什么问题?为什么还没进 ES2026?
答: 管道运算符解决函数嵌套的可读性问题------把 f(g(h(x))) 变成 x |> h(%) |> g(%) |> f(%),从左往右读更自然。仍未进入 ES2026 的原因是 TC39 对占位符语法存在分歧(Hack 风格用 % vs F# 风格不用占位符),2017 年提出至今仍在 Stage 2。
Q5:用 Temporal 实现"计算两个日期之间相差多少天"。
答:
js
const d1 = Temporal.PlainDate.from("2026-07-01");
const d2 = Temporal.PlainDate.from("2026-07-22");
const diff = d1.until(d2);
console.log(diff.days); // 21
// 或用 since
const diff2 = d2.since(d1);
console.log(diff2.days); // 21
相比用 Date:const diff = (d2 - d1) / (1000 * 60 * 60 * 24),Temporal 不需要手算毫秒转换,且能正确处理跨月、跨年、夏令时等边界情况。