JavaScript中的时间

81 阅读6分钟

摘要

时间处理是前端与全栈开发中最容易踩坑、却又无处不在的基础能力:显示时间、记录日志、性能计时、跨时区日程、报表统计、倒计时与定时任务……本文系统梳理 JavaScript 中与“时间”相关的概念、API、常见陷阱与工程实践,帮助你写出正确、稳定、可维护的时间处理代码。

目录

  • 概念速览:UTC、本地时间、时区与 DST
  • 核心 API:Date、时间戳与 Intl.*
  • 常见陷阱与避坑清单
  • 实战技巧:显示、格式化、运算与计时
  • 跨时区与国际化
  • 计时与性能测量
  • 测试中的时间与可控性
  • Temporal 提案与未来
  • 工具与库选择
  • 最佳实践清单

概念速览:UTC、本地时间、时区与 DST

  • UTC(协调世界时):全球统一的基准时间,不受时区影响。
  • 本地时间:根据用户机器所在区域(操作系统设置)计算出的时间。
  • 时区(Time Zone):地理区域的时间偏移规则,如 Asia/ShanghaiAmerica/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 混用getHours vs getUTCHours,跨时区逻辑要用 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)。
  • 展示层:根据用户偏好或业务要求指定 localetimeZone
  • 业务日历:涉及工作日/节假日、地区规则时,需引入专门日历配置。

计时与性能测量

// 代码执行耗时:优先使用高精度时钟
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 时长)
  • 清晰的类型:InstantPlainDatePlainTimeDuration
// 若运行环境支持或引入 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:DateIntl.DateTimeFormatIntl.RelativeTimeFormat
  • TC39 Temporal 提案与文档
  • date-fns / dayjs / luxon 官方文档