告别 Date | JavaScript Temporal API 使用教程

告别 Date | JavaScript Temporal API 使用教程

写在之前

JavaScript 终于有了靠谱的日期时间 API,但这个 API 似乎还没有比较全面的中文文档。因此我写了这篇入门教程抛砖引玉,待大佬们完善Temporal的中文文档。

如果你写过前端,大概率被 Date 折磨过:

  • 月份从 0 开始(1 月是 0,12 月是 11);
  • 没有原生时区支持,跨时区计算全靠手搓;
  • 对象可变,一不小心就改了原始值;
  • 想做"加一天"这种简单操作,还要自己算毫秒。

Temporal 就是为了彻底解决这些问题而设计的新一代日期时间 API。

本文会按这个顺序来:

  1. 先看 Date 到底有什么问题
  2. 快速上手 Temporal 核心类型
  3. 逐个讲解常用方法与实战
  4. 把原理放在靠后位置集中梳理
  5. 最后给你一个完整实例 + 逐段解析

⚠️ 注意 本文强调"先上手,再吃透"。 如果某个类型第一次没完全理解,先跟着写,后面原理章节会回收解释。
⚠️ 注意 本文封面源自于 MDN博客,如有侵权请联系作者删除。
🚫 警告 本内容首发伴莺の小窝,未经授权禁止搬运!
ℹ️ 提示 文章中包含部分AI内容,用于校正、完善文章,如有错误请向作者反馈。

一、Date 到底有什么问题?

在正式学 Temporal 之前,先快速过一遍 Date 的经典坑,这样你才会理解 Temporal 的设计动机。

1.1 月份从 0 开始

js 复制代码
const d = new Date(2026, 2, 6);
console.log(d.toISOString());
// 输出:2026-03-06T...  而不是 2026-02-06
// 第二个参数 2 代表三月,不是二月

这是几乎所有 JS 初学者都会踩的坑。

1.2 对象可变(Mutable)

js 复制代码
const start = new Date(2026, 0, 1);
const end = start; // 你以为复制了一份?
end.setMonth(11);  // 实际上 start 也被改了!

console.log(start.getMonth()); // 11,不是 0

Date 是引用类型且可变,赋值只是复制引用,不是复制值。

1.3 没有原生时区支持

想知道东京当前时间? Date 只能拿到本地时间UTC ,其它时区全靠手动偏移或第三方库。

1.4 日期运算靠手搓

js 复制代码
// "今天加 30 天"
const today = new Date();
today.setDate(today.getDate() + 30);
// 能用,但可读性差,且修改了原对象

ℹ️ 提示 正是这些问题,催生了 moment.jsdayjsdate-fns 等第三方库。 而 Temporal 的目标就是在语言层面一次性解决它们。

二、Temporal 是什么?

Temporal ( /ˈtempərəl/ ) 是 TC39(JavaScript 标准委员会)提出的新日期时间 API,目前处于 Stage 3 阶段,已被主流浏览器逐步实现。

它的核心设计原则:

  • 不可变(Immutable) :所有操作返回新对象,不修改原值
  • 类型明确:日期、时间、日期时间、时区时间各有专属类型
  • 原生时区支持:内置 IANA 时区数据库
  • 无歧义:月份从 1 开始,API 命名直观

⚠️ 注意 截至文档编辑时间,仅 Safari 暂不支持。 Chrome、Firefox、Safari 的预览版已支持 Temporal。 如果你的浏览器暂不支持,可以升级浏览器内核或使用 polyfill:@js-temporal/polyfill

Temporal 类型一览

先记住这张一图流,后面每个类型会逐一讲解:

类型 含义 示例
Temporal.PlainDate 纯日期(无时间、无时区) 2026-03-06
Temporal.PlainTime 纯时间(无日期、无时区) 14:30:00
Temporal.PlainDateTime 日期 + 时间(无时区) 2026-03-06T14:30:00
Temporal.ZonedDateTime 日期 + 时间 + 时区(完整) 2026-03-06T14:30:00+08:00[Asia/Shanghai]
Temporal.Instant 时间刻(UTC) YYYY-MM-DD T HH:mm:ss.sssssssss Z/±HH:mm[TimeZone]
Temporal.Duration 时间段/持续时间 2 小时 30 分
Temporal.PlainYearMonth 年月 2026-03
Temporal.PlainMonthDay 月日 03-06

一句话总结:需要时区就用 ZonedDateTime,不需要就用 Plain* 系列。

三、快速上手:核心类型逐个写

3.1 Temporal.PlainDate --- 纯日期

表示"某一天",不关心具体时间和时区。适用于生日、纪念日、排班日期等场景。

js 复制代码
// 方式一:from 字符串
const date1 = Temporal.PlainDate.from('2026-03-06');
console.log(date1.toString()); // "2026-03-06"

// 方式二:from 对象
const date2 = Temporal.PlainDate.from({ year: 2026, month: 3, day: 6 });
console.log(date2.year);  // 2026
console.log(date2.month); // 3  ← 终于是 3 了,不是 2!
console.log(date2.day);   // 6

// 方式三:获取今天的日期
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // 当前日期,如 "2026-03-06"

原理补充

  • PlainDate 没有时间信息,所以不会出现"跨时区日期变了"的问题。
  • from() 是 Temporal 所有类型的统一构造方式,支持字符串和对象两种入参。
  • 月份从 1 开始,这是 Temporal 最直观的改进之一。
日期运算
js 复制代码
const date = Temporal.PlainDate.from('2026-03-06');

// 加 10 天
const later = date.add({ days: 10 });
console.log(later.toString()); // "2026-03-16"

// 减 1 个月
const earlier = date.subtract({ months: 1 });
console.log(earlier.toString()); // "2026-02-06"

// 原始对象不变!
console.log(date.toString()); // "2026-03-06" ← 不可变

💡 技巧 Temporal 的运算方法(如 add()subtract())都返回新对象 ,原始值保持不变,详情请看 章节4.2。 因此,你可以放心地链式调用或传参,不用担心被意外修改。(它的便利程度不亚于map方法)

日期比较
js 复制代码
const a = Temporal.PlainDate.from('2026-01-01');
const b = Temporal.PlainDate.from('2026-12-31');

console.log(Temporal.PlainDate.compare(a, b)); // -1(a 在 b 之前)
console.log(Temporal.PlainDate.compare(b, a)); // 1(b 在 a 之后)
console.log(Temporal.PlainDate.compare(a, a)); // 0(相等)

// 更直观的方式:计算两个日期的差
const diff = a.until(b);
const diff2 = b.since(a);
console.log(diff.toString()); // "P365D"-> P 指示 Period(期间),365D 表示 365 天
console.log(diff2.toString()); // "P365D"

3.2 Temporal.PlainTime --- 纯时间

表示"某个时刻的时间",没有日期、没有时区。适合表达营业时间、闹钟等。

js 复制代码
const time = Temporal.PlainTime.from('14:30:00');
console.log(time.hour);   // 14
console.log(time.minute); // 30
console.log(time.second); // 0

// 加 2 小时 15 分
const later = time.add({ hours: 2, minutes: 15 });
console.log(later.toString()); // "16:45:00"

// 获取当前时间
const now = Temporal.Now.plainTimeISO();
console.log(now.toString()); // 如 "20:15:30.123456789"

💡 技巧 PlainTime 精度可达纳秒级(9 位小数),比 Date 的毫秒精度高得多。

3.3 Temporal.PlainDateTime --- 日期 + 时间

把日期和时间组合在一起,但仍然没有时区。适合表达"本地事件",比如会议安排。

js 复制代码
const dt = Temporal.PlainDateTime.from('2026-03-06T14:30:00');
console.log(dt.year);   // 2026
console.log(dt.hour);   // 14

// 也可以从对象创建
const dt2 = Temporal.PlainDateTime.from({
    year: 2026, month: 3, day: 6,
    hour: 14, minute: 30
});

// 运算:加 1 天 2 小时
const result = dt.add({ days: 1, hours: 2 });
console.log(result.toString()); // "2026-03-07T16:30:00"

原理补充

  • PlainDateTime 不包含时区信息,所以它表示的是一个"挂在墙上的钟看到的时间"。
  • 如果同一个会议需要让不同时区的人看到各自的本地时间,应该用 ZonedDateTime

3.4 Temporal.ZonedDateTime --- 完整时间(带时区)

这是最重量级 的类型:日期 + 时间 + 时区,三者缺一不可。 适合表达:全球统一的某个精确时刻在某个时区的表现。

js 复制代码
// 创建一个时区为上海的时间
const zdt = Temporal.ZonedDateTime.from({
    year: 2026, month: 3, day: 6,
    hour: 14, minute: 30,
    timeZone: 'Asia/Shanghai'
});
console.log(zdt.toString());
// "2026-03-06T14:30:00+08:00[Asia/Shanghai]"

// 获取当前时区的当前时间
const now = Temporal.Now.zonedDateTimeISO();
console.log(now.toString());

// 查看同一时刻在东京是几点
const tokyo = now.withTimeZone('Asia/Tokyo');
console.log(tokyo.toString());
时区转换实战
js 复制代码
// 场景:北京时间 2026-03-06 20:00 开播,纽约观众看几点?
const beijing = Temporal.ZonedDateTime.from({
    year: 2026, month: 3, day: 6,
    hour: 20, minute: 0,
    timeZone: 'Asia/Shanghai'
});

const newYork = beijing.withTimeZone('America/New_York');
console.log(newYork.toString());
// "2026-03-06T07:00:00-05:00[America/New_York]"
// 纽约是早上 7 点

ℹ️ 提示 withTimeZone() 不会改变"时间线上的那个点",只是换一种时区来表达同一个瞬间。 这和 Date 完全不同------Date 根本没有时区切换的概念。

3.5 Temporal.Instant --- 时间轴上的一刻

Instant 代表时间线上的一个精确点,不关心时区,类似 Unix 时间戳。

js 复制代码
// 当前瞬间
const now = Temporal.Now.instant();
console.log(now.toString()); // 如 "2026-03-06T12:30:00Z"(UTC)

// 从纪元毫秒数创建
const fromMs = Temporal.Instant.fromEpochMilliseconds(1772582400000);
console.log(fromMs.toString());

// 转换为特定时区的 ZonedDateTime
const zdt = now.toZonedDateTimeISO('Asia/Shanghai');
console.log(zdt.toString());

原理补充

  • Instant 只代表"时间线上的点",没有年月日时分秒的概念。
  • 想要年月日,必须给它一个时区,通过 toZonedDateTimeISO() 转换。
  • 它和 Date.now() 本质类似,但精度更高(纳秒级)。

3.6 Temporal.Duration --- 持续时间

表示一段时间长度,比如"2 小时 30 分"或"3 年 6 个月"。

js 复制代码
// 创建 Duration
const d1 = Temporal.Duration.from({ hours: 2, minutes: 30 });
console.log(d1.toString()); // "PT2H30M" -> T 表示分隔日期部分和时间部分

const d2 = Temporal.Duration.from('P1Y6M'); // 1 年 6 个月
console.log(d2.months); // 6

// 两个日期之间的差距
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-03-06');

const diff = start.until(end);
console.log(diff.toString()); // "P64D"(64 天)

// 指定更大单位
const diff2 = start.until(end, { largestUnit: 'month' });
console.log(diff2.toString()); // "P2M5D"(2 个月 5 天)
Duration 运算
js 复制代码
const meeting = Temporal.Duration.from({ hours: 1, minutes: 30 });
const breakTime = Temporal.Duration.from({ minutes: 15 });

// Duration 相加
const total = meeting.add(breakTime);
console.log(total.toString()); // "PT1H45M"

// 取绝对值(处理可能的负值)
const neg = Temporal.Duration.from({ hours: -3 });
console.log(neg.abs().toString()); // "PT3H"

3.7 Temporal.PlainYearMonth 与 PlainMonthDay

这两个是精简类型,分别表示"年月"和"月日"。

js 复制代码
// 年月:适合表示账单月、学期等
const ym = Temporal.PlainYearMonth.from('2026-03');
console.log(ym.daysInMonth); // 31
console.log(ym.inLeapYear);  // false

// 月日:适合表示生日(不含年份)、纪念日等
const md = Temporal.PlainMonthDay.from('03-06');
console.log(md.toString()); // "--03-06"(ISO 8601 格式)

四、从"会用"到"用对"

4.1 类型选择指南

不确定该用哪个类型?看这个决策流程:

  1. 需要时区吗?

    • 是 → ZonedDateTime
    • 否 → 继续
  2. 需要日期和时间都有吗?

    • 都要 → PlainDateTime
    • 只要日期 → PlainDate
    • 只要时间 → PlainTime
  3. 只需要年月或月日?

    • 年月 → PlainYearMonth
    • 月日 → PlainMonthDay
  4. 只关心时间轴上的精确时刻?

    • Instant

4.2 不可变性

Temporal 所有类型都是不可变的------告别"改着改着原值没了"。每次运算都返回新对象:

js 复制代码
const date = Temporal.PlainDate.from('2026-03-06');
const next = date.add({ days: 1 });

// date 完全不变
console.log(date.toString()); // "2026-03-06"
console.log(next.toString()); // "2026-03-07"

这意味着你可以放心传参、存储,不用担心被意外修改。

4.3 with() --- 修改部分字段

所有 Plain* 类型都支持 with() 方法,用于"只改一部分":

js 复制代码
const date = Temporal.PlainDate.from('2026-03-06');

// 只改月份
const july = date.with({ month: 7 });
console.log(july.toString()); // "2026-07-06"

// 只改年份
const nextYear = date.with({ year: 2027 });
console.log(nextYear.toString()); // "2027-03-06"

// PlainTime 同理
const time = Temporal.PlainTime.from('14:30:00');
const evening = time.with({ hour: 20 });
console.log(evening.toString()); // "20:30:00"

4.4 until() 与 since() --- 计算时间差

这两个方法互为反向:

js 复制代码
const start = Temporal.PlainDate.from('2026-01-01');
const end = Temporal.PlainDate.from('2026-03-06');

// until:从 start 到 end 的距离(正值)
console.log(start.until(end).toString()); // "P64D"

// since:从 end 回看 start 的距离(也是正值,方向相反)
console.log(end.since(start).toString()); // "P64D"

常用选项:

js 复制代码
// 指定最大单位为月
start.until(end, { largestUnit: 'month' });
// "P2M5D" → 2 个月零 5 天

// 指定最小单位为小时(PlainDateTime 场景)
const dt1 = Temporal.PlainDateTime.from('2026-03-06T08:00');
const dt2 = Temporal.PlainDateTime.from('2026-03-06T14:45');
dt1.until(dt2, { largestUnit: 'hour' });
// "PT6H45M" → 6 小时 45 分

⚠️ 注意 需要注意的是,until()since() 是互为反向的,以下是 MDN 的说明:

since() method does this - other. To do other - this, use the until() method.

until() method does other - this. To do this - other, use the since() method.

按照 MDN 建议,until() 作用的是其它时间点到当前时间的方法。按照上述 startend 的值举例,start.until(end) 建议改为 end.since(start),反之亦然。

4.5 比较与排序

Temporal 提供静态 compare() 方法,可直接配合 Array.sort()

js 复制代码
const dates = [
    Temporal.PlainDate.from('2026-12-25'),
    Temporal.PlainDate.from('2026-01-01'),
    Temporal.PlainDate.from('2026-07-04'),
];

dates.sort(Temporal.PlainDate.compare);
console.log(dates.map(d => d.toString()));
// ["2026-01-01", "2026-07-04", "2026-12-25"]

💡 技巧 compare(a, b) 返回 -1 / 0 / 1,和 Array.sort() 的比较函数签名完全兼容,直接传入即可。

五、核心原理

5.1 为什么需要这么多类型?

传统 Date 只有一个类型,试图同时表示"本地时间"和"UTC 时间",结果两头都做不好。

Temporal 的设计哲学是:

不同场景用不同类型,类型本身就是文档。

当你看到代码里用了 PlainDate,你立刻知道这里不涉及时区。 当你看到 ZonedDateTime,你知道这个时间是带时区的精确时刻。

这种"类型即文档"的思路,能在编码阶段就避免大量歧义。

⚠️ 注意 虽然各个方法之间单词几乎一致,但大小写会直接影响调用。因此这也是常踩的坑 之一。在写教程时就踩了很多次

5.2 ISO 8601 字符串格式

Temporal 遵循 ISO 8601 标准,常见格式:

格式 含义
2026-03-06 纯日期
14:30:00 纯时间
2026-03-06T14:30:00 日期时间(无时区)
2026-03-06T14:30:00+08:00[Asia/Shanghai] 日期时间 + 时区
2026-03-06T06:30:00Z UTC 时间
P1Y2M3DT4H5M6S 持续时间:1年2月3天4时5分6秒

Duration 格式说明:P 开头,T 分隔日期部分和时间部分。

5.3 Temporal.Now --- 获取当前时间的统一入口

js 复制代码
Temporal.Now.instant()            // 当前 Instant(UTC 精确时刻)
Temporal.Now.zonedDateTimeISO()   // 当前 ZonedDateTime(系统时区)
Temporal.Now.plainDateISO()       // 当前 PlainDate
Temporal.Now.plainTimeISO()       // 当前 PlainTime
Temporal.Now.plainDateTimeISO()   // 当前 PlainDateTime

所有方法都可以传入时区参数来获取特定时区的当前时间:

js 复制代码
// 获取东京当前日期
const tokyoDate = Temporal.Now.plainDateISO('Asia/Tokyo');

5.4 溢出处理(overflow)

当你创建一个不合法的日期(比如 2 月 30 日),Temporal 提供两种策略:

js 复制代码
// 默认:constrain(约束到合法范围)
Temporal.PlainDate.from({ year: 2026, month: 2, day: 30 }, { overflow: 'constrain' });
// → "2026-02-28"(约束到该月最后一天)

// 严格模式:reject(直接报错)
Temporal.PlainDate.from({ year: 2026, month: 2, day: 30 }, { overflow: 'reject' });
// → 抛出 RangeError

⚠️ 注意 默认行为是 constrain(静默纠正),不是报错。 如果你的业务需要严格校验输入,记得显式传入 { overflow: 'reject' }

5.5 和 Date 的互操作

在迁移过渡期,你可能需要在 DateTemporal 之间转换:

js 复制代码
// Date → Temporal.Instant
const legacy = new Date('2026-03-06T12:00:00Z');
const instant = Temporal.Instant.fromEpochMilliseconds(legacy.getTime());

// Temporal.Instant → Date
const back = new Date(instant.epochMilliseconds);

// Temporal.Instant → ZonedDateTime
const zdt = instant.toZonedDateTimeISO('Asia/Shanghai');
console.log(zdt.toString());
// "2026-03-06T20:00:00+08:00[Asia/Shanghai]"

六、模拟实战:倒计时组件

这个例子综合了你前面学到的核心类型和方法,实现一个简单的倒计时页面。

6.1 完整代码

js 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Temporal 倒计时</title>
    <style>
        * { box-sizing: border-box; }

        body {
            margin: 0;
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            gap: 20px;
            font-family: Arial, sans-serif;
            background: #0f172a;
            color: #e2e8f0;
        }

        h1 { font-size: 1.4rem; font-weight: 400; }

        /* 倒计时数字容器 */
        .countdown {
            display: flex;
            gap: 16px;
        }

        /* 每个时间块 */
        .block {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 6px;
        }

        /* 数字样式 */
        .number {
            font-size: 3rem;
            font-weight: 700;
            background: #1e293b;
            border-radius: 12px;
            padding: 12px 20px;
            min-width: 90px;
            text-align: center;
            color: #38bdf8;
        }

        /* 标签文字 */
        .label {
            font-size: 0.85rem;
            color: #94a3b8;
        }

        /* 状态提示 */
        .status {
            font-size: 1rem;
            color: #facc15;
        }
    </style>
</head>
<body>
    <h1>距离 2027 年元旦还有</h1>

    <div class="countdown">
        <div class="block">
            <span class="number" id="days">--</span>
            <span class="label">天</span>
        </div>
        <div class="block">
            <span class="number" id="hours">--</span>
            <span class="label">时</span>
        </div>
        <div class="block">
            <span class="number" id="mins">--</span>
            <span class="label">分</span>
        </div>
        <div class="block">
            <span class="number" id="secs">--</span>
            <span class="label">秒</span>
        </div>
    </div>

    <p class="status" id="status"></p>

    <script>
        // 1. 定义目标时间:2027-01-01 00:00:00 上海时区
        const target = Temporal.ZonedDateTime.from({
            year: 2027, month: 1, day: 1,
            hour: 0, minute: 0, second: 0,
            timeZone: 'Asia/Shanghai'
        });

        // 2. 获取 DOM 元素引用
        const elDays  = document.getElementById('days');
        const elHours = document.getElementById('hours');
        const elMins  = document.getElementById('mins');
        const elSecs  = document.getElementById('secs');
        const elStatus = document.getElementById('status');

        // 3. 核心更新函数
        function tick() {
            // 获取当前时区时间
            const now = Temporal.Now.zonedDateTimeISO('Asia/Shanghai');

            // 比较:如果已经过了目标时间
            if (Temporal.ZonedDateTime.compare(now, target) >= 0) {
                elDays.textContent  = '00';
                elHours.textContent = '00';
                elMins.textContent  = '00';
                elSecs.textContent  = '00';
                elStatus.textContent = '🎉 新年快乐!';
                return; // 停止计时
            }

            // 计算剩余时间,指定各单位
            const remaining = now.until(target, {
                largestUnit: 'day',
                smallestUnit: 'second',
                roundingMode: 'floor'
            });

            // 更新页面
            elDays.textContent  = String(remaining.days).padStart(2, '0');
            elHours.textContent = String(remaining.hours).padStart(2, '0');
            elMins.textContent  = String(remaining.minutes).padStart(2, '0');
            elSecs.textContent  = String(remaining.seconds).padStart(2, '0');
        }

        // 4. 启动:立即执行一次 + 每秒更新
        tick();
        setInterval(tick, 1000);
    </script>
</body>
</html>

6.2 逐段解析

  1. 目标时间ZonedDateTime 创建,明确指定了 Asia/Shanghai 时区,不会因为用户系统时区不同而出错。
  2. 当前时间 通过 Temporal.Now.zonedDateTimeISO('Asia/Shanghai') 获取,保证和目标在同一时区下比较。
  3. 时间差now.until(target, { largestUnit: 'day' }) 计算,直接得到天、时、分、秒各字段,不需要手动除以 86400000 再取余。
  4. 比较ZonedDateTime.compare(),语义清晰:>= 0 代表当前时间已到达或超过目标。
  5. 不可变性 的好处:每次 tick() 都重新获取 now,不存在累计误差或意外修改。

6.3 对比 Date 写法

如果用传统 Date 实现同样功能:

js 复制代码
// Date 版本(对比参考)
const target = new Date('2027-01-01T00:00:00+08:00').getTime();

function tick() {
    const diff = target - Date.now();
    if (diff <= 0) { /* ... */ return; }

    const days  = Math.floor(diff / 86400000);
    const hours = Math.floor((diff % 86400000) / 3600000);
    const mins  = Math.floor((diff % 3600000) / 60000);
    const secs  = Math.floor((diff % 60000) / 1000);
    // ...手动计算每一级单位
}

对比之下,Temporal 版本的优势很明显:

  • 不需要手动做毫秒除法
  • until() 直接返回结构化的时间差
  • 时区处理显式且可靠

七、真实实例:管理系统中时间模块

前面的倒计时是一个独立小页面,下面来看我的真实 React 项目中 Temporal 的用法------后台管理系统的布局组件。 虽然是用的 React 技术栈,但 Temporal 的核心用法和原理在任何框架中都是一样的。

👉 后台管理系统

这个管理系统的顶栏需要实时显示当前时间,同时根据时间自动切换暗黑模式。来看它是怎么用 Temporal 实现的。

7.1 自动暗黑模式:PlainDateTime 判断时段

js 复制代码
const [darkMode, setDarkMode] = useState(
    Temporal.Now.plainDateTimeISO().hour >= 18 ||
    Temporal.Now.plainDateTimeISO().hour < 6
);
// 晚上 6 点到早上 6 点默认开启暗黑模式

逐行解析

  • Temporal.Now.plainDateTimeISO() 获取当前本地日期时间(PlainDateTime 类型)。
  • .hour 直接取小时数(0--23),命名直观,不需要像 Date 那样调 getHours()
  • 这里用 PlainDateTime 而非 ZonedDateTime,因为"当前用户看到的时间"就是本地时间,不需要跨时区。

💡 技巧 这是一种典型的用对类型 场景: 判断当前是白天还是夜晚,只关心本地时间,所以用 PlainDateTime 就够了,不需要引入时区复杂度。

7.2 实时时钟:ZonedDateTime + 精准定时器

管理系统的顶栏和锁屏界面都需要一个秒级实时时钟

js 复制代码
const [time, setTime] = useState(Temporal.Now.zonedDateTimeISO());
const timerRef = useRef(null);

useEffect(() => {
    const nextTick = () => {
        // ① 每次获取最新的带时区时间
        const now = Temporal.Now.zonedDateTimeISO();
        setTime(now);

        // ② 计算"下一整秒"的时间点
        const nextSecond = now.add({ seconds: 1 })
            .round({
                smallestUnit: 'second',
                roundingMode: 'floor'
            });

        // ③ 用 until + total 精确算出到下一秒还剩多少毫秒
        const msUntilNextSecond = now.until(nextSecond)
            .total({ unit: 'millisecond' });

        // ④ setTimeout 递归,动态计算下次执行时间
        timerRef.current = setTimeout(nextTick, msUntilNextSecond);
    };

    nextTick(); // 立即执行第一次

    return () => {
        if (timerRef.current) {
            clearTimeout(timerRef.current);
            timerRef.current = null;
        }
    };
}, []);

逐行解析

这段代码的核心思路是:每次动态计算到下一整秒的精确延迟。

  • ① 获取当前时间Temporal.Now.zonedDateTimeISO() 返回带时区的完整时间,精度到纳秒。
  • ② 计算下一整秒 :先 add({ seconds: 1 }) 加一秒,再 round({ smallestUnit: 'second', roundingMode: 'floor' }) 向下取整到整秒------比如当前时间是 14:30:01.347,加 1 秒得到 14:30:02.347,向下取整得到 14:30:02.000
  • ③ 计算剩余毫秒数now.until(nextSecond) 得到一个 Duration 对象,.total({ unit: 'millisecond' }) 将它换算成毫秒总数。
  • ④ 递归 setTimeout :用精确的毫秒数设定下一次触发,避免 setInterval 的累积漂移问题。

ℹ️ 提示 为什么不用 setInterval(fn, 1000)

setInterval 每次固定延迟 1000ms,但实际执行可能因主线程繁忙而延迟。时间一长,显示时间会和真实时间越差越远。

上面的方案每次都重新获取真实时间 + 精确计算剩余毫秒,所以:

  • 秒跳变对齐整秒:用户看到的秒数切换不会有大幅的偏移
  • 无累积误差:即使某一次延迟了,下一次会自动修正,计算误差不超过毫秒级,保证时钟长期运行也能保持准确。
  • 不可变性保障 :每次循环都是全新的 now 对象,不存在引用共享问题

7.3 时间格式化显示

锁屏界面显示的大号时钟,直接用 toLocaleString 格式化为本地时间格式:

js 复制代码
<p style={{ fontSize: '10vh', fontWeight: 'bold' }}>
    {time.toLocaleString('zh-CN', {
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit'
    })}
</p>
// 显示效果:"20:15:30"

这里 time 就是前面 useState 中存的 ZonedDateTime 对象,因为它自带时区信息,toLocaleString 能正确地按时区渲染时间。

7.4 这个实例用到了哪些 Temporal API?

知识点 对应章节 实际用法
Temporal.Now.plainDateTimeISO() 章节 3.3 判断当前小时,切换暗黑模式
Temporal.Now.zonedDateTimeISO() 章节 3.4 获取带时区的实时时间
.add() + .round() 章节 3.1 计算下一个整秒时间点
.until().total() 章节 4.4 精确计算到下一秒的毫秒数
.toLocaleString() 章节 8.5 把时间格式化为 HH:mm:ss 显示
不可变性 章节 4.2 每次循环创建新对象,无引用共享风险

✅ 总结 从这个实例可以看出,Temporal 在真实项目中并不需要"全部都用"。 只需要根据场景选对类型(PlainDateTimeZonedDateTime),配合 add()round()until() 等少量方法,就能优雅地解决实际问题。

八、常见场景速查

8.1 获取某月有多少天

js 复制代码
const ym = Temporal.PlainYearMonth.from('2026-02');
console.log(ym.daysInMonth); // 28

const ym2 = Temporal.PlainYearMonth.from('2024-02');
console.log(ym2.daysInMonth); // 29(闰年)

8.2 判断闰年

js 复制代码
const date = Temporal.PlainDate.from('2024-06-15');
console.log(date.inLeapYear); // true

const date2 = Temporal.PlainDate.from('2026-06-15');
console.log(date2.inLeapYear); // false

8.3 获取星期几

ini 复制代码
const date = Temporal.PlainDate.from('2026-03-06');
console.log(date.dayOfWeek); // 5(周五,ISO 8601:周一=1,周日=7)

8.4 获取一年中的第几天

js 复制代码
const date = Temporal.PlainDate.from('2026-03-06');
console.log(date.dayOfYear); // 65

8.5 格式化显示

Temporal 本身不内置复杂的格式化方法,推荐配合 Intl.DateTimeFormat 使用:

js 复制代码
const date = Temporal.PlainDate.from('2026-03-06');

// 使用 toLocaleString
console.log(date.toLocaleString('zh-CN'));
// "2026/3/6"

console.log(date.toLocaleString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    weekday: 'long'
}));
// "2026年3月6日星期五"

九、排错清单

当 Temporal 代码"不对劲"时,按这个顺序排查:

  1. 类型选对了吗? 需要时区就用 ZonedDateTime,不要用 PlainDateTime"凑合"
  2. 时区写对了吗? 必须用 IANA 格式(如 Asia/Shanghai),不能写 +08:00(部分方法不支持)
  3. 溢出策略确认了吗? 默认是 constrain 静默修正,业务校验场景记得用 reject
  4. 单位指定了吗? until() / since() 默认最大单位可能不是你想要的,显式传 largestUnit
  5. 浏览器支持确认了吗? 不支持时需要 @js-temporal/polyfill

💡 技巧 调试时善用 .toString() 打印完整字符串,Temporal 的字符串格式自带类型信息,一眼就能看出问题。

下一步...

如果你已经读到这里,建议按照这个节奏继续:

  1. 把倒计时示例手写一遍(不要复制)
  2. 改造成"距离某个自定义事件"的倒计时
  3. 试试 PlainDate 写一个简单的日历网格
  4. ZonedDateTime 做一个"世界时钟"(同时显示北京、东京、纽约时间)
  5. 参考管理系统实例,在自己的项目中实现一个精准实时时钟

当你能"选对类型、算对时间、讲清原理",Temporal 就算入门了。

相关推荐
酉鬼女又兒2 小时前
HTML基础实例样式详解零基础快速入门Web开发(可备赛蓝桥杯Web应用开发赛道) 助力快速拿奖
前端·javascript·职场和发展·蓝桥杯·html·html5·web
Watermelo6172 小时前
【前端实战】构建 Vue 全局错误处理体系,实现业务与错误的清晰解耦
前端·javascript·vue.js·信息可视化·性能优化·前端框架·设计规范
进击的尘埃3 小时前
Vue 3 编译器宏的编译时魔法:defineModel、defineSlots 与 AST 转换的真相
javascript
不会敲代码13 小时前
使用 Mock.js 模拟 API 数据,实现前后端并行开发
前端·javascript
向上的车轮3 小时前
TypeScript 一日速通指南:数据类型全解析与转换指南
javascript·typescript
叫我一声阿雷吧3 小时前
【JS 实战案例】用 JS 实现页面滚动到指定位置(带动画)
javascript·页面交互·js实战案例·平滑滚动·前端零基础·锚点导航
We་ct3 小时前
React 更新触发原理详解
开发语言·前端·javascript·react.js·面试·前端框架·react
还是大剑师兰特4 小时前
Vue3 页面权限控制实战示例(路由守卫 + 权限判断)
开发语言·前端·javascript