摘要
时间处理是前端与全栈开发中最容易踩坑、却又无处不在的基础能力:显示时间、记录日志、性能计时、跨时区日程、报表统计、倒计时与定时任务……本文系统梳理 JavaScript 中与“时间”相关的概念、API、常见陷阱与工程实践,帮助你写出正确、稳定、可维护的时间处理代码。
目录
- 概念速览:UTC、本地时间、时区与 DST
- 核心 API:
Date、时间戳与Intl.* - 常见陷阱与避坑清单
- 实战技巧:显示、格式化、运算与计时
- 跨时区与国际化
- 计时与性能测量
- 测试中的时间与可控性
Temporal提案与未来- 工具与库选择
- 最佳实践清单
概念速览:UTC、本地时间、时区与 DST
- UTC(协调世界时):全球统一的基准时间,不受时区影响。
- 本地时间:根据用户机器所在区域(操作系统设置)计算出的时间。
- 时区(Time Zone):地理区域的时间偏移规则,如
Asia/Shanghai、America/New_York。 - 夏令时(DST):部分地区在一年中某些时段会+1 小时,导致一天可能是 23 或 25 小时。
理解上述概念有助于解释:为什么在不同地区显示同一时间会不同?为什么“加一天”可能不是 24 小时?为什么“某天的 02:30”在有些地方不存在?
核心 API:Date、时间戳与 Intl
Date 的构造与解析
// 当前时间(本地时区)
const now = new Date();
// 指定时间(本地时区解释)——注意:月份从 0 开始!
const d1 = new Date(2025, 8, 16, 14, 30); // 2025-09-16 14:30 本地
// 时间戳(毫秒)
const d2 = new Date(1737081600000);
// ISO 8601 字符串(推荐)
const d3 = new Date('2025-09-16T06:30:00Z'); // 明确为 UTC
// 避免依赖实现的模糊解析(如 '2025/09/16 06:30')
常见构造方式:
new Date():当前本地时间。new Date(ms):从 Unix Epoch(1970-01-01T00:00:00Z)起的毫秒数。new Date(isoString):推荐使用标准 ISO 8601;含Z代表 UTC,无Z则按本地时区解释或实现策略。new Date(y, m, d, h, M, s, ms):易错点是月份从 0 开始。
读取与设置:本地 vs UTC
const d = new Date('2025-09-16T06:30:00Z');
// 本地时区读写(受系统时区影响)
d.getFullYear(); d.getMonth(); d.getDate(); d.getHours();
d.setHours(10);
// UTC 读写(独立于系统时区)
d.getUTCFullYear(); d.getUTCMonth(); d.getUTCDate(); d.getUTCHours();
d.setUTCHours(10);
// 时间戳:毫秒
Date.now(); // 快捷
d.getTime(); // 等同于 Number(d)
静态辅助:Date.UTC
// 构造 UTC 下的时间戳(毫秒)
const ts = Date.UTC(2025, 8, 16, 6, 30); // 2025-09-16T06:30:00Z -> ms
格式化与国际化:Intl.DateTimeFormat
const date = new Date('2025-09-16T06:30:00Z');
// 按用户本地偏好
new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(date);
// 指定时区与语言
new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
}).format(date);
相对时间:Intl.RelativeTimeFormat
const rtf = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' });
rtf.format(-3, 'day'); // "3 天前"
rtf.format(2, 'hour'); // "2 小时后"
常见陷阱与避坑清单
- 月份从 0 开始:
0-11对应1-12 月。 Date.parse/字符串解析不一致:非 ISO 字符串行为依赖实现,避免使用。- 本地/UTC 混用:
getHoursvsgetUTCHours,跨时区逻辑要用 UTC 基准。 - DST 影响“加一天”:不要用“加 24 小时毫秒数”代替“加自然日”。
- 格式化时区遗漏:服务端/用户端显示不一致,务必显式指定
timeZone。 setInterval漂移:长期运行会累积误差,建议基于时间戳修正。- 序列化与反序列化:统一使用 ISO 字符串或 UTC 毫秒时间戳。
实战技巧:显示、格式化、运算与计时
安全的解析与序列化
// 序列化:推荐 ISO 或 UTC 时间戳
const iso = new Date().toISOString(); // 总是 UTC,如 2025-09-16T06:30:00.000Z
const ms = Date.now();
// 反序列化:优先 ISO;若为毫秒,直接 new Date(ms)
const dFromIso = new Date(iso);
const dFromMs = new Date(ms);
日期加减(自然日 vs 精确毫秒)
// 基于自然日:先取 UTC 日期,再设置 UTC 日期,避免 DST 干扰
function addUtcDays(date, days) {
const d = new Date(date.getTime());
d.setUTCDate(d.getUTCDate() + days);
return d;
}
// 基于毫秒(精确时长):适合倒计时、TTL 等
function addMs(date, ms) {
return new Date(date.getTime() + ms);
}
可靠的倒计时与定时
// 避免 setInterval 漂移:以时间戳为基准自校正
function startAccurateInterval(callback, intervalMs) {
let expected = Date.now() + intervalMs;
let timerId;
function tick() {
const drift = Date.now() - expected;
callback({ drift });
expected += intervalMs;
timerId = setTimeout(tick, Math.max(0, intervalMs - drift));
}
timerId = setTimeout(tick, intervalMs);
return () => clearTimeout(timerId);
}
人类可读的格式化
function formatInTZ(date, timeZone, locale = 'zh-CN') {
return new Intl.DateTimeFormat(locale, {
timeZone,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
}).format(date);
}
// 示例:统一后端存 UTC,前端根据用户所在时区显示
const storedISO = '2025-09-16T06:30:00Z';
const d = new Date(storedISO);
formatInTZ(d, 'Asia/Shanghai');
跨时区与国际化
- 存储层:推荐使用 UTC(ISO 字符串或毫秒时间戳)。
- 传输层:明确单位与时区;字段命名可包含后缀(如
createdAtMsUtc)。 - 展示层:根据用户偏好或业务要求指定
locale与timeZone。 - 业务日历:涉及工作日/节假日、地区规则时,需引入专门日历配置。
计时与性能测量
// 代码执行耗时:优先使用高精度时钟
const t0 = performance.now();
// ... 代码 ...
const t1 = performance.now();
console.log(`耗时 ${(t1 - t0).toFixed(2)} ms`);
// Node.js 可用 process.hrtime.bigint()
// const t0 = process.hrtime.bigint();
// const t1 = process.hrtime.bigint();
// const costMs = Number(t1 - t0) / 1e6;
注意:Date.now() 受系统时钟调整影响,performance.now() 为单调时钟,适合测量间隔。
测试中的时间与可控性
// Jest 示例:
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-09-16T00:00:00Z'));
const timer = setTimeout(() => console.log('done'), 1000);
jest.advanceTimersByTime(1000);
// 恢复真实计时器
jest.useRealTimers();
可控时间使得涉及超时、重试、缓存过期与轮询的逻辑更易于测试与稳定复现。
Temporal 提案与未来
Temporal 是一组新的原生时间 API(提案阶段,有 polyfill),旨在修复 Date 的历史问题:
- 不可变对象、明确的时区模型(
Temporal.ZonedDateTime) - 精准的日期时间算术(自然日 vs 时长)
- 清晰的类型:
Instant、PlainDate、PlainTime、Duration等
// 若运行环境支持或引入 polyfill:
// import { Temporal } from '@js-temporal/polyfill';
// 当前瞬时点(UTC)
// const now = Temporal.Now.instant();
// 格式化到指定时区
// const zdt = now.toZonedDateTimeISO('Asia/Shanghai');
// zdt.toString();
在现代架构中,优先考虑:数据层用 UTC;业务层用 Temporal(或优秀库)做精确运算;展示层用 Intl.* 渲染。
工具与库选择
- 轻量强类型:
date-fns(函数式、可 tree-shake) - API 友好:
dayjs(小巧、使用体验接近 moment) - 时区/格式强:
luxon(基于Intl,内置DateTime概念) - 历史项目:
moment(已进入维护模式,不建议新项目使用) - Temporal:
@js-temporal/polyfill(为未来 API 做过渡)
最佳实践清单
- 统一存储为 UTC:ISO 字符串或毫秒时间戳。
- 展示时总是指定时区:
Intl.DateTimeFormat(..., { timeZone })。 - 日期运算区分自然日与时长:避免 DST 误差。
- 避免模糊解析:只用 ISO 或自行解析。
- 长期定时使用自校正策略:不要盲信
setInterval。 - 性能测量用单调时钟:
performance.now()或hrtime.bigint()。 - 测试中使用假时钟:让时间可控、可复现。
- 面向未来:关注
Temporal,合理引入 polyfill 或替代库。
参考资料
- ECMAScript Language Specification(日期与时间章节)
- MDN:
Date、Intl.DateTimeFormat、Intl.RelativeTimeFormat - TC39 Temporal 提案与文档
- date-fns / dayjs / luxon 官方文档