首页/版本发布/Temporal崛起,Dayjs落幕

过去这些年,Dayjs 之所以能火,很大程度上不是因为它做了什么惊天动地的新能力,而是因为原生 Date 实在太难用了。

解析不稳,时区容易错,setMonth 这类 API 还带副作用,稍不注意就会掉坑里。

所以前端这些年才会长期维持一个奇怪局面:明明“时间”是最基础的能力之一,但大家写项目时,几乎都默认要额外装一个日期库。

Temporal 的出现,本质上就是标准库亲自下场,把这件事重新做一遍。

如果你今天主要用 Dayjs 来做这些事:

  • 解析时间
  • 格式化时间
  • 做加减运算
  • 比较时间先后
  • 处理时区
  • 表达“只有日期”或“只有时间”的业务语义

那么 Temporal 确实已经可以覆盖绝大部分核心场景。

但要先把一句话说清楚:

Temporal 不是 Dayjs 的同名替身,它不是把 dayjs().add().format() 原样搬进标准库,而是换了一套更清晰的对象模型。

也就是说,想通过 Temporal 完全替换 Dayjs,关键不是“找一个一模一样的 API”,而是把你的时间代码按语义重新拆开。

先说结论

如果你只想先知道结论,可以直接看这几条:

  1. Temporal 可以替掉 Dayjs 的大部分核心能力,尤其是解析、运算、比较、时区和不可变数据这几块。
  2. Temporal 并不是单对象链式 API,而是 PlainDatePlainDateTimeInstantZonedDateTimeDuration 等一组对象协同工作。
  3. Dayjs 最常用的 format('YYYY-MM-DD HH:mm:ss') 这种 token 格式化,Temporal 没有直接内置同款 API,通常要改成 Intl 或自己封一层格式化函数。
  4. Dayjs 的自定义格式解析插件很方便,但 Temporal 不提供任意 token 字符串解析,复杂输入需要你自己写解析函数。
  5. 到了 2026 年,Temporal 在新版 Chrome、Edge、Firefox 和 Deno 上已经可用,但 Node.jsBunSafariiOS Safari 侧仍然不能裸用,想“完全替换”就得配 polyfill

第一步:先解决运行时支持

如果你跑在新版浏览器里,可能已经能直接用 Temporal

但如果你的代码跑在下面这些环境里:

  • Node.js
  • Bun
  • Safari
  • iOS Safari

那就不要直接上原生 Temporal,而是先接上 polyfill。

pnpm add @js-temporal/polyfill
import { Temporal as TemporalPolyfill } from "@js-temporal/polyfill";

export const TemporalX = globalThis.Temporal ?? TemporalPolyfill;

后面所有示例里的 Temporal,你都可以理解成:

const Temporal = globalThis.Temporal ?? TemporalPolyfill;

这一层很重要。

因为很多人以为自己是在“迁移 API”,实际上第一个坑就死在运行时兼容上了。

第二步:先换思维,不要急着换 API

Dayjs 的思路很统一:

  • 基本上什么都先变成一个 dayjs() 对象
  • 然后在这个对象上链式操作

Temporal 的思路不是这样。

它把不同语义的“时间”拆开了。

业务语义Dayjs 常见写法Temporal 推荐对象
只有日期,比如生日、账单日、签到日dayjs('2026-05-06')Temporal.PlainDate
只有时间,比如每天 09:30dayjs(...).format('HH:mm')Temporal.PlainTime
日期 + 时间,但还没绑定时区dayjs('2026-05-06 14:30:00')Temporal.PlainDateTime
绝对时间戳,比如数据库 created_atdayjs(value) / dayjs.utc(value)Temporal.Instant,前提是输入本身就是带偏移量的绝对时间
带时区的真实业务时间,比如上海时间 9 点开会dayjs.tz(...)Temporal.ZonedDateTime
时间差dayjs.duration(...)Temporal.Duration

这一步是迁移成功的关键。

很多人一上来就想把所有 dayjs() 都换成 Temporal.ZonedDateTime,结果代码越写越乱。

正确姿势是:

  • 先问自己这段值到底是“日期”还是“瞬时点”
  • 再选合适的 Temporal 类型

当前时间、固定时间、UTC 时间怎么换

这是最常见的一批替换。

1. 当前本地时间

// Dayjs
const now = dayjs();

// Temporal
const now = Temporal.Now.zonedDateTimeISO();

如果你只是要“当前日期”,那就不要拿一个完整日期时间对象硬扛:

// Dayjs
const today = dayjs().format("YYYY-MM-DD");

// Temporal
const today = Temporal.Now.plainDateISO();

2. 解析一个纯日期

// Dayjs
const date = dayjs("2026-05-06");

// Temporal
const date = Temporal.PlainDate.from("2026-05-06");

3. 解析一个不带时区的日期时间

// Dayjs
const dateTime = dayjs("2026-05-06 14:30:00");

// Temporal
const dateTime = Temporal.PlainDateTime.from("2026-05-06T14:30:00");

注意这里的区别。

Temporal.PlainDateTime 表达的是“墙上时钟看到的时间”,它本身不带时区,不等于某个绝对瞬时点。

4. 处理 UTC / 绝对时间

// Dayjs
const utcTime = dayjs.utc("2026-05-06T06:30:00Z");

// Temporal
const instant = Temporal.Instant.from("2026-05-06T06:30:00Z");

如果你要把它展示成上海时区时间:

const shanghaiTime = instant.toZonedDateTimeISO("Asia/Shanghai");

console.log(shanghaiTime.toString());
// 2026-05-06T14:30:00+08:00[Asia/Shanghai]

5. Unix 时间戳

// Dayjs
const t1 = dayjs.unix(1714977000);
const unix = dayjs().unix();

// Temporal
const t1 = Temporal.Instant.fromEpochMilliseconds(1714977000 * 1000);
const unix = Math.floor(Temporal.Now.instant().epochMilliseconds / 1000);

格式化怎么替换

这是 Dayjs 用户迁移到 Temporal 时最别扭的地方。

因为 Dayjs 最顺手的能力之一就是:

dayjs().format("YYYY-MM-DD HH:mm:ss");

Temporal 不提供一套一模一样的 token 格式化器。

这不代表它不能格式化,而是思路变了。

方案一:固定机器格式,直接用 toString() 或自己拼

如果你要的是固定格式,例如 YYYY-MM-DDYYYY-MM-DD HH:mm:ss,那自己拼往往最稳。

function pad(value, length = 2) {
    return String(value).padStart(length, "0");
}

function formatPlainDate(date) {
    return `${date.year}-${pad(date.month)}-${pad(date.day)}`;
}

function formatPlainDateTime(dateTime) {
    return [`${dateTime.year}-${pad(dateTime.month)}-${pad(dateTime.day)}`, `${pad(dateTime.hour)}:${pad(dateTime.minute)}:${pad(dateTime.second)}`].join(" ");
}

const date = Temporal.PlainDate.from("2026-05-06");
const dateTime = Temporal.PlainDateTime.from("2026-05-06T14:30:45");

console.log(formatPlainDate(date));
// 2026-05-06

console.log(formatPlainDateTime(dateTime));
// 2026-05-06 14:30:45

对应的 Dayjs 写法:

console.log(dayjs("2026-05-06").format("YYYY-MM-DD"));
console.log(dayjs("2026-05-06 14:30:45").format("YYYY-MM-DD HH:mm:ss"));

方案二:面向用户显示,改用 Intl

如果你要的是“给用户看”的本地化格式,那 IntlDayjs 那套 token 更合理。

const meeting = Temporal.PlainDateTime.from("2026-05-06T14:30:00");

console.log(
    meeting.toLocaleString("zh-CN", {
        dateStyle: "full",
        timeStyle: "medium"
    })
);

如果是带时区对象:

const meeting = Temporal.PlainDateTime.from("2026-05-06T14:30:00").toZonedDateTime("Asia/Shanghai");

console.log(
    meeting.toLocaleString("zh-CN", {
        dateStyle: "full",
        timeStyle: "medium"
    })
);

对应的 Dayjs 写法往往是:

dayjs.extend(localizedFormat);

console.log(dayjs("2026-05-06 14:30:00").format("LLLL"));

方案三:封一层最常用 token 兼容函数

如果你项目里满地都是:

  • YYYY-MM-DD
  • YYYY-MM-DD HH:mm:ss
  • HH:mm:ss

那与其全项目手工改字符串,不如自己封一层有限 token formatter。

function formatTemporal(value, pattern = "YYYY-MM-DD HH:mm:ss") {
    const source = value instanceof Temporal.ZonedDateTime ? value : value instanceof Temporal.PlainDateTime ? value : value instanceof Temporal.PlainDate ? value.toPlainDateTime(Temporal.PlainTime.from("00:00:00")) : value.toZonedDateTimeISO("UTC");

    const year = String(source.year);
    const month = String(source.month).padStart(2, "0");
    const day = String(source.day).padStart(2, "0");
    const hour = String(source.hour ?? 0).padStart(2, "0");
    const minute = String(source.minute ?? 0).padStart(2, "0");
    const second = String(source.second ?? 0).padStart(2, "0");

    return pattern.replaceAll("YYYY", year).replaceAll("MM", month).replaceAll("DD", day).replaceAll("HH", hour).replaceAll("mm", minute).replaceAll("ss", second);
}

const value = Temporal.PlainDateTime.from("2026-05-06T14:30:45");

console.log(formatTemporal(value, "YYYY-MM-DD"));
console.log(formatTemporal(value, "YYYY-MM-DD HH:mm:ss"));

这不是说你要重写一个完整的 Dayjs,而是说你可以把业务里最常见的 3 到 5 种格式先吃掉,迁移成本会立刻下降很多。

加减时间怎么替换

这一块 Temporal 其实很顺。

1. 加减天、月、年

// Dayjs
const a = dayjs("2026-05-06").add(7, "day");
const b = dayjs("2026-05-06").subtract(1, "month");

// Temporal
const a = Temporal.PlainDate.from("2026-05-06").add({ days: 7 });
const b = Temporal.PlainDate.from("2026-05-06").subtract({ months: 1 });

2. 处理月底这种边界

// Dayjs
console.log(dayjs("2024-01-31").add(1, "month").format("YYYY-MM-DD"));
// 2024-02-29

// Temporal
console.log(Temporal.PlainDate.from("2024-01-31").add({ months: 1 }).toString());
// 2024-02-29

3. 日期时间加减

// Dayjs
const result = dayjs("2026-05-06 14:30:00").add(2, "hour").subtract(15, "minute");

// Temporal
const result = Temporal.PlainDateTime.from("2026-05-06T14:30:00").add({ hours: 2 }).subtract({ minutes: 15 });

比较、判等、diff 怎么替换

1. 比较大小

// Dayjs
dayjs(a).isBefore(b);
dayjs(a).isAfter(b);

// Temporal
Temporal.Instant.compare(a, b) < 0;
Temporal.Instant.compare(a, b) > 0;

如果你比较的是纯日期:

Temporal.PlainDate.compare(dateA, dateB) < 0;

2. 判断是否相等

// Dayjs
dayjs(a).isSame(b);

// Temporal
a.equals(b);

3. 判断“是否同一天”

// Dayjs
dayjs(a).isSame(b, "day");

// Temporal
const sameDay = a.toPlainDate().equals(b.toPlainDate());

如果 ab 本身就是 PlainDate,那直接 a.equals(b) 即可。

4. 时间差

// Dayjs
const days = dayjs("2026-05-10").diff(dayjs("2026-05-06"), "day");

// Temporal
const days = Temporal.PlainDate.from("2026-05-06").until(Temporal.PlainDate.from("2026-05-10"), {
    largestUnit: "day"
}).days;

如果你比较的是带时区的绝对时间:

const start = Temporal.Instant.from("2026-05-06T00:00:00Z");
const end = Temporal.Instant.from("2026-05-06T01:30:00Z");

const diff = start.until(end, { largestUnit: "hour" });

console.log(diff.hours); // 1
console.log(diff.minutes); // 30

startOf / endOf 怎么替换

DayjsstartOf() / endOf() 很顺手,但 Temporal 没有照着抄一遍。

不过这不难,自己封一下反而更清楚。

1. 一天的开始和结束

// Dayjs
const start = dayjs(value).startOf("day");
const end = dayjs(value).endOf("day");

// Temporal
const zdt = Temporal.PlainDateTime.from("2026-05-06T14:30:00").toZonedDateTime("Asia/Shanghai");

const start = zdt.startOfDay();
const end = zdt.startOfDay().add({ days: 1 }).subtract({ nanoseconds: 1 });

2. 一个月的开始和结束

function startOfMonth(zdt) {
    return zdt.with({
        day: 1,
        hour: 0,
        minute: 0,
        second: 0,
        millisecond: 0,
        microsecond: 0,
        nanosecond: 0
    });
}

function endOfMonth(zdt) {
    return startOfMonth(zdt).add({ months: 1 }).subtract({ nanoseconds: 1 });
}

const source = Temporal.PlainDateTime.from("2026-05-06T14:30:00").toZonedDateTime("Asia/Shanghai");

console.log(startOfMonth(source).toString());
console.log(endOfMonth(source).toString());

时区处理怎么替换

这部分是 Temporal 最大的价值之一。

因为 Date 和很多旧代码最大的问题就在于:

  • 到底当前值是本地时间,还是 UTC,还是某个业务时区时间
  • 到底是在“解释一个墙上时间”,还是在“转换一个绝对瞬时点”

Temporal 把这两件事分得很清楚。

场景一:把“上海时间 2026-05-06 09:00”解释成一个带时区的业务时间

// Dayjs
dayjs.extend(utc);
dayjs.extend(timezone);

const meeting = dayjs.tz("2026-05-06 09:00:00", "Asia/Shanghai");

// Temporal
const meeting = Temporal.PlainDateTime.from("2026-05-06T09:00:00").toZonedDateTime("Asia/Shanghai");

这一步表示的是:

“2026-05-06 09:00:00” 这串墙上时间,属于上海时区。

场景二:把同一个瞬时点换个时区展示

// Dayjs
const shanghai = dayjs.tz("2026-05-06 09:00:00", "Asia/Shanghai");
const newYork = shanghai.tz("America/New_York");

// Temporal
const shanghai = Temporal.PlainDateTime.from("2026-05-06T09:00:00").toZonedDateTime("Asia/Shanghai");

const newYork = shanghai.withTimeZone("America/New_York");

场景三:后端给你 UTC 时间戳,前端按用户时区展示

// Dayjs
const result = dayjs.utc("2026-05-06T01:00:00Z").tz("Asia/Shanghai");

// Temporal
const result = Temporal.Instant.from("2026-05-06T01:00:00Z").toZonedDateTimeISO("Asia/Shanghai");

这类代码迁完后,可读性会高很多。

因为你一眼就能看出:

  • Instant 是绝对时间
  • ZonedDateTime 是带时区展示

相对时间、duration 怎么替换

1. Duration 对象

// Dayjs
dayjs.extend(duration);

const d = dayjs.duration({ hours: 2, minutes: 30 });

// Temporal
const d = Temporal.Duration.from({ hours: 2, minutes: 30 });

2. fromNow / toNow 这类相对时间

Temporal 本身不提供 fromNow() 这种文案函数,但配合 Intl.RelativeTimeFormat 就够用了。

// Dayjs
dayjs.extend(relativeTime);

const text = dayjs("2026-05-10").from(dayjs("2026-05-06"));

// Temporal
const rtf = new Intl.RelativeTimeFormat("zh-CN", { numeric: "auto" });

const start = Temporal.PlainDate.from("2026-05-06");
const end = Temporal.PlainDate.from("2026-05-10");
const diff = start.until(end, { largestUnit: "day" });

const text = rtf.format(diff.days, "day");

console.log(text);
// 4 天后

如果你要的是“2 小时前”“3 分钟后”这类文案,也可以按同样方式封装。

自定义格式解析怎么替换

这是 Dayjs 相比 Temporal 仍然很方便的一点。

比如你以前可能会这样写:

// Dayjs + customParseFormat
dayjs("20260506 143045", "YYYYMMDD HHmmss");

Temporal 不提供这种“任意 token 输入直接解析”的能力。

它更偏向:

  • 解析标准格式
  • 或者你先把输入处理干净,再构造对象

比如:

function parseCompactDateTime(input) {
    const match = input.match(/^(\d{4})(\d{2})(\d{2})\s(\d{2})(\d{2})(\d{2})$/);

    if (!match) {
        throw new Error("Invalid datetime format");
    }

    const [, year, month, day, hour, minute, second] = match;

    return Temporal.PlainDateTime.from({
        year: Number(year),
        month: Number(month),
        day: Number(day),
        hour: Number(hour),
        minute: Number(minute),
        second: Number(second)
    });
}

const value = parseCompactDateTime("20260506 143045");

console.log(value.toString());
// 2026-05-06T14:30:45

这看起来比 Dayjs 多写了几行,但好处是:

  • 解析规则完全透明
  • 不依赖插件黑盒
  • 失败条件更明确

给你一套最实用的迁移对照

如果你要从 Dayjs 批量迁 Temporal,下面这张对照表最有用。

Dayjs 写法Temporal 写法
dayjs()Temporal.Now.zonedDateTimeISO()
dayjs('2026-05-06')Temporal.PlainDate.from('2026-05-06')
dayjs('2026-05-06 14:30:00')Temporal.PlainDateTime.from('2026-05-06T14:30:00')
dayjs.utc(value)Temporal.Instant.from(value),前提是 value 本身带 Z 或偏移量
dayjs.tz(value, 'Asia/Shanghai')Temporal.PlainDateTime.from(value).toZonedDateTime('Asia/Shanghai')
value.format('YYYY-MM-DD')plainDate.toString() 或自定义 formatter
value.format('LLLL')value.toLocaleString('zh-CN', options)
value.add(1, 'day')value.add({ days: 1 })
value.subtract(1, 'month')value.subtract({ months: 1 })
value.diff(other, 'day')value.until(other, { largestUnit: 'day' }).days
value.isBefore(other)Temporal.PlainDate.compare(a, b) < 0Temporal.Instant.compare(a, b) < 0
value.isSame(other)value.equals(other)
value.startOf('day')zdt.startOfDay()
value.endOf('day')zdt.startOfDay().add({ days: 1 }).subtract({ nanoseconds: 1 })
dayjs.duration(...)Temporal.Duration.from(...)
value.unix()Math.floor(instant.epochMilliseconds / 1000)

如果你真想“完全替掉 Dayjs”,建议按这个顺序迁

不要全局搜 dayjs( 然后暴力替换。

那样你很快就会发现:

  • 有的是纯日期
  • 有的是带时区时间
  • 有的是 UTC 时间戳
  • 有的是只拿来格式化
  • 有的是自定义格式输入

更稳的迁法是:

第一批先迁最容易的

  • add
  • subtract
  • diff
  • isBefore
  • isAfter
  • isSame
  • unix

这些最容易替换,而且改完收益立刻可见。

第二批迁语义最清晰的

  • 生日
  • 签到日期
  • 账单日期
  • 日历选择器结果

这类值优先改成 Temporal.PlainDate,比继续塞在 dayjs() 里清爽得多。

第三批处理时区

  • 国际化业务
  • 预约系统
  • 会议时间
  • 日志展示

这批应该统一改成 Instant + ZonedDateTime 的组合。

最后再处理格式化和自定义解析

这两块最容易卡住,也是 Dayjs 生态最舒服的部分。

我的建议很直接:

  • 面向用户显示,用 Intl
  • 面向固定模板输出,自己封一个很小的 formatter
  • 面向自定义输入,自己写解析函数

这样迁出来的代码会比“继续模拟 Dayjs 整套 token 体系”更干净。

最后一句话

Temporal 真正厉害的地方,并不只是它能不能替掉 Dayjs,而是它第一次让 JavaScript 标准库把“日期”“时间”“时区”“瞬时点”“时长”这些概念彻底拆开了。

这意味着以后很多以前必须靠第三方库硬补的能力,终于可以在标准层面被清晰表达了。

所以更准确的说法不是:

Temporal 有一个 API 可以 1:1 替掉 Dayjs

而是:

Temporal 提供了一套更底层、更明确、更适合长期维护的时间模型,让你不再必须依赖 Dayjs 这类库来给 Date 打补丁。

如果你的目标是“彻底摆脱 DateDayjs 的历史包袱”,那从今天开始用 Temporal,方向是对的。

如果你的项目目前还 heavily 依赖:

  • 自定义 token 解析
  • 大量 format('YYYY/MM/DD HH:mm:ss') 模板
  • 旧运行时兼容

那就不要一步到位,先 polyfill,再按场景逐批迁移,才是最稳的方案。