时间戳与时区:秒 / 毫秒、2038 问题与常见坑
「差 1000 倍」和「差 8 小时」是时间处理里最高频的两类 bug。
一、Unix 时间戳的定义
Unix 时间戳指从 1970-01-01 00:00:00 UTC 起到现在经过的秒数(或毫秒数)。它是一个绝对时刻,本身不带任何时区信息。
关键认知:时间戳不需要「加 8 小时」。所谓「加 8 小时」只在把时间戳格式化成人类可读字符串时才发生,是格式化阶段的事。
二、秒还是毫秒?先看位数
| 单位 | 位数 | 典型值 | 出现场景 |
|---|---|---|---|
| 秒 | 10 位 | 1762000000 | Unix / PHP / Go / MySQL |
| 毫秒 | 13 位 | 1762000000000 | Java / JavaScript / 大部分 API |
| 微秒 | 16 位 | 1762000000000000 | Python time.time_ns()/1000、部分数据库 |
一个稳妥的自动判断:
function toMs(ts) {
const s = String(ts);
if (s.length <= 10) return ts * 1000; // 秒
if (s.length <= 13) return ts; // 毫秒
return Math.floor(ts / 1000); // 微秒
}
把 10 位秒数直接丢给 new Date(),会得到 1970 年 1 月 20 日左右的时间——因为它被当成毫秒了。这是最经典的「时间显示成 1970」现象。
三、时区:时间戳是 UTC,展示是本地
同一个时间戳,在不同时区的人眼里是不同钟表时间,但指向同一时刻:
| 时区 | 本地时间 | 偏移 |
|---|---|---|
| UTC | 2026-11-01 05:45 | +00:00 |
| 中国 (CST) | 2026-11-01 13:45 | +08:00 |
| 纽约 (EST) | 2026-11-01 00:45 | -05:00 |
所以设计接口时:存储与传输一律用时间戳或带偏移的 ISO 8601,只有展示层才做时区转换。不要在数据库里存「本地时间字符串」,否则服务器迁移到别的时区就全乱了。
四、new Date 解析字符串的坑
这是前端最容易踩的隐式时区转换:
new Date('2026-11-01') // 按 UTC 零点解析(ES 规范)
new Date('2026-11-01T00:00') // 按本地时区解析
new Date('2026/11/01') // 按本地时区解析(非标准格式)
// 结果可能相差一整天
带 T 和不带 T 的行为不一致,是规范遗留问题。稳妥做法:自己解析并用 Date.UTC() 构造,或者明确带上 Z / 偏移量。
五、2038 年问题
32 位有符号整数能表示的最大值是 2147483647,对应 2038-01-19 03:14:07 UTC。用 32 位 time_t 存储秒数的系统,过了这一刻会溢出成 1901 年。
影响范围:老版本 32 位 Linux / 嵌入式设备 / 部分数据库的 INT 字段。规避方式很简单——时间字段用 64 位整数(BIGINT)或 DATETIME 类型,不要用 32 位 INT 存秒数。
六、ISO 8601 还是时间戳?
| 时间戳 | ISO 8601 | |
|---|---|---|
| 可读性 | 差 | 好 |
| 体积 | 10~13 字节 | 20~25 字节 |
| 时区信息 | 隐含 UTC | 可显式携带 |
| 适合 | 存储、排序、计算 | 日志、API 返回、人工排查 |
常见折中:数据库存时间戳(排序快、无歧义),接口返回时同时给 timestamp 和 iso 两个字段。