在线工具集

Unix 时间戳与时区处理避坑指南

深入讲解时间戳本质、JavaScript Date 陷阱、时区转换最佳实践、UTC vs ISO 8601、夏令时边界、数据库存储建议,帮助开发者彻底搞懂时间处理。

✍️ XTechTools 编辑团队 · 📅 发布 2026-04-29 · 🔄 更新 2026-05-13 · ⏱ 约 13 分钟阅读 ·→ 立即使用 时间戳转换

Unix 时间戳看似简单——一个整数,代表自 1970-01-01 00:00:00 UTC 起经过的秒数——却是后端 bug、前端显示错误、跨服务数据不一致的重灾区。本文从原理出发,梳理秒级 vs 毫秒级的区别、JavaScript `Date` 的常见陷阱、时区转换的正确姿势,以及数据库存储的最佳实践,帮助你一次性把时间处理搞清楚。

时间戳本质:秒级 vs 毫秒级

Unix 时间戳的标准单位是,但 JavaScript 的 Date.now()new Date().getTime() 返回的是毫秒。两者混用是最常见的 bug 来源。

常见场景对比:

Math.floor(Date.now() / 1000)  // 秒级时间戳 ✅
Date.now()                      // 毫秒级时间戳
new Date(1714368000)            // ❌ 被当成毫秒,结果是 1970-01-20
new Date(1714368000000)         // ✅ 正确的毫秒级

判断一个时间戳是秒级还是毫秒级的经验法则: - 10 位数字 → 秒级(如 1714368000) - 13 位数字 → 毫秒级(如 1714368000000

接收外部 API 数据时,务必确认单位,或使用如下防御性代码:

function toMs(ts) {
  return String(ts).length <= 10 ? ts * 1000 : ts;
}

JavaScript Date 的 7 个常见陷阱

1. 字符串解析行为不一致

new Date("2024-01-01") 在大多数浏览器中被解析为 UTC 时间,而 new Date("2024/01/01") 则被解析为本地时间。这意味着同一天在不同写法下可能差 8 小时(UTC+8 环境)。

new Date("2024-01-01").toISOString()  // "2024-01-01T00:00:00.000Z"  — UTC
new Date("2024/01/01").toISOString()  // "2023-12-31T16:00:00.000Z"  — 中国用户看到前一天!

2. getMonth() 从 0 开始

new Date().getMonth() 返回 0–11,而非 1–12,忘记 +1 是新手必踩的坑。

3. 夏令时导致的小时丢失

在实行夏令时(DST)的地区,某天凌晨 2 点会直接跳到 3 点,导致 new Date("2024-03-10 02:30:00") 在美东时区实际上不存在。

4. toLocaleDateString 格式因系统语言而异

new Date().toLocaleDateString() 在中文系统返回 "2024/1/1",英文系统返回 "1/1/2024",不适合用于日志或存储。

5. Invalid Date 无报错

new Date("abc") 不抛出异常,只返回 Invalid Date 对象,isNaN(new Date("abc")) 才能检测。

6. JSON.stringify 自动转 UTC

JSON.stringify({ t: new Date() }) 会把 Date 序列化为 ISO 8601 字符串(带 Z 后缀,表示 UTC),反序列化时需手动 new Date(str)

7. 时间比较应用数值而非对象

new Date("2024-01-01") === new Date("2024-01-01")  // false ❌ 对象引用比较
new Date("2024-01-01").getTime() === new Date("2024-01-01").getTime()  // true ✅

时区转换:toLocaleString 的正确用法

toLocaleString / toLocaleTimeString / toLocaleDateString 支持 timeZone 选项,是原生 API 中最简洁的时区转换方式:

const ts = 1714368000000; // 某个 UTC 时间戳
const date = new Date(ts);

// 显示为北京时间 date.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }); // "2024/4/29 12:00:00"

// 显示为纽约时间 date.toLocaleString("en-US", { timeZone: "America/New_York" }); // "4/29/2024, 12:00:00 AM" ```

时区标识符遵循 IANA 时区数据库([iana.org/time-zones](https://www.iana.org/time-zones)),常用值: - Asia/Shanghai(中国标准时间 CST,UTC+8) - Asia/Tokyo(JST,UTC+9) - Europe/London(GMT/BST) - America/New_York(EST/EDT) - UTC(始终不偏移)

如果需要获取另一时区的年月日数字(用于逻辑计算而非展示),推荐用 Intl.DateTimeFormat + formatToParts

function getPartsInTZ(ts, tz) {
  const fmt = new Intl.DateTimeFormat("en-CA", {
    timeZone: tz,
    year: "numeric", month: "2-digit", day: "2-digit",
    hour: "2-digit", minute: "2-digit", second: "2-digit",
    hour12: false,
  });
  return Object.fromEntries(
    fmt.formatToParts(new Date(ts)).map(p => [p.type, p.value])
  );
}
// { year: "2024", month: "04", day: "29", hour: "12", minute: "00", second: "00" }

UTC vs ISO 8601:格式规范与互转

UTC(协调世界时)是一个时间标准,没有夏令时,是全球所有时区的基准。

ISO 8601 是一种日期时间格式规范,常见形式:

2024-04-29T04:00:00Z          ← Z 表示 UTC
2024-04-29T12:00:00+08:00     ← 带时区偏移
2024-04-29T12:00:00.000+08:00 ← 带毫秒

JavaScript Date.toISOString() 始终输出 UTC 的 ISO 8601 字符串(以 Z 结尾)。如果要输出带本地时区偏移的字符串,需要手动计算:

function toLocalISO(date) {
  const off = -date.getTimezoneOffset();
  const sign = off >= 0 ? "+" : "-";
  const pad = n => String(Math.abs(n)).padStart(2, "0");
  const hh = pad(Math.floor(Math.abs(off) / 60));
  const mm = pad(Math.abs(off) % 60);
  const local = new Date(date.getTime() + off * 60000);
  return local.toISOString().slice(0, 19) + sign + hh + ":" + mm;
}
// "2024-04-29T12:00:00+08:00"

在 API 设计中,推荐统一使用 ISO 8601 + 明确时区偏移(或 Z)作为接口传输格式,避免歧义。

夏令时(DST)的边界问题

夏令时(Daylight Saving Time)每年会让时钟向前或向后拨动一小时,影响美国、欧洲、澳大利亚等地区,但中国(UTC+8)自 1992 年起已废除 DST。

DST 带来的两类问题:

1. 时间不存在(Spring Forward)

美东时间 2024-03-10 凌晨 2:00 → 直接跳到 3:00,这意味着 2:30 不存在。如果你的代码在这个时间触发 cron job,结果会令人意外。

2. 时间重复(Fall Back)

美东时间 2024-11-03 凌晨 2:00 → 退回到 1:00,这一小时会经历两次,日志时间戳可能出现"倒退"。

最佳实践:所有内部计算、存储、比较一律使用 UTC 时间戳(整数),只在最终展示给用户时才转为本地时区。这样 DST 的复杂性完全由 Intl / toLocaleString 的 IANA 数据库处理,你的业务代码无需感知。

数据库存储建议

方案一:存 UNIX 时间戳(整数) - 优点:无时区歧义,索引性能好,任何语言都能解析 - 适用:大多数业务场景(事件日志、消息、订单) - 精度:秒级用 INT,毫秒级用 BIGINT

方案二:存带时区的类型(TIMESTAMPTZ) - PostgreSQL 的 TIMESTAMP WITH TIME ZONE 内部存储 UTC,查询时按 session 时区自动转换 - MySQL 的 TIMESTAMP 类似,但受服务器时区配置影响,迁移时需注意 - 适用:需要在 SQL 层做时区相关查询的场景

方案三:存 ISO 8601 字符串 - 缺点:字符串比较性能差,排序可能错误,不推荐用于高频查询字段 - 可接受:日志、配置、导出文件

  1. 所有服务器 NTP 同步到同一时间源
  2. 服务间传递时间使用 UTC 整数时间戳或 ISO 8601 Z 字符串
  3. 数据库服务器时区设置为 UTC,避免 ORM 自动转换带来的隐患
  4. 前端展示时在用户浏览器本地做时区转换,不在服务端硬编码用户时区

可以使用 [xtechtools.com 的时间戳工具](/timestamp/) 快速完成时间戳与可读时间的互转,支持任意时区转换。

闰秒:几乎不用管,但要知道它存在

闰秒(Leap Second)由国际地球自转服务(IERS)不定期插入 UTC,用于补偿地球自转速度的细微变化。插入时,UTC 的某一分钟会有 61 秒(即 23:59:60)。

对绝大多数业务系统来说,闰秒影响极小: - Linux 内核通过"闰秒抹平"(leap smear)将额外的 1 秒分散到数小时内 - Google、AWS、Azure 都采用类似策略 - NTP 校时会在闰秒后自动修正

唯一需要注意的场景:高频交易、精密授时等对毫秒级时间准确性有极高要求的系统,需要使用支持闰秒的时钟库(如 PTP 硬件时间戳)。

对普通 Web 应用而言:确保服务器开启 NTP 同步即可,无需额外处理。

常见问题

为什么我的时间戳转出来差了 8 小时?

最常见原因:把 UTC 时间戳直接显示,没有转换到用户所在的 UTC+8 时区。使用 `new Date(ts).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })` 即可得到北京时间。

`Date.now()` 和 `+new Date()` 有区别吗?

结果相同,都返回毫秒级时间戳。`Date.now()` 不需要创建 Date 对象,性能略好,是推荐写法。

如何判断两个时间是否是同一天(按北京时间)?

不要直接比较时间戳除以 86400000,时区会导致结果错误。应将两个时间戳都转为北京时间的年月日字符串再比较,或使用 `Intl.DateTimeFormat` + `formatToParts` 提取年月日数字后对比。

数据库存时间戳用 INT 还是 DATETIME?

推荐用 INT(秒级)或 BIGINT(毫秒级)存 Unix 时间戳,索引性能好,无时区歧义。如需在 SQL 层做时区感知查询,PostgreSQL 的 TIMESTAMPTZ 是更好的选择。避免裸 DATETIME 类型(不带时区信息)。

moment.js 还值得用吗?

moment.js 已停止维护(进入维护模式),包体积大(~300KB gzip 后约 70KB)。新项目推荐用原生 `Intl` API 或轻量替代品 dayjs(~2KB)、date-fns(按需引入)。

中国有没有夏令时?

中国大陆自 1992 年起废除夏令时,全年统一使用 UTC+8(中国标准时间 CST)。香港、台湾、澳门也不实行夏令时。使用 IANA 时区标识符 `Asia/Shanghai` 时,系统会自动处理历史上的 DST 记录(1986–1991 年间曾实行)。