Unix 时间戳的常见错误:单位、时区与 1970

更新于 6 分钟阅读

Unix 时间是从 1970-01-01T00:00:00Z 起算的秒数。这个数字本身没有歧义,歧义都在处理环节上。

数位数就知道是什么单位

当前时代的数值落在很窄的区间里,所以数一下位数就能判断单位:10 位秒值从 1e9 到 9999999999, 对应 2001-09-09 到 2286-11-20;13 位毫秒值覆盖同样的年份区间。2001 年 9 月之前的秒数只有 9 位, 老数据会让这个快捷判断失效。

位数单位当前典型值直接传给 new Date() 的结果
10秒17590000001970-01-21,距纪元 20 天
13毫秒1759000000000正确——Date 收的就是毫秒
16微秒1759000000000000公元 57710 年
19纳秒1759000000000000000Invalid Date,而且它本来也不是整数

JavaScript 的 Date 收毫秒,而多数 HTTP 接口给的是秒,所以最常见的症状就是 1970。两个方向都要记住: 日期落在纪元附近,说明秒被当成了毫秒;年份是五位数,说明毫秒又被乘了一次 1000。精度另有一条断崖—— 微秒值大约到 2255 年才会超出 JavaScript 整数的安全范围,而纳秒值如今已经超过 Number.MAX_SAFE_INTEGER, 只能用字符串或 BigInt 传递。

超出范围不报错,只变成 Invalid Date

Date 只在纪元前后 ±8,640,000,000,000 毫秒内有效,上限是 +275760-09-13T00:00:00.000Z。越界时构造函数 并不抛错,你得到的是一个 getTime() 为 NaN 的 Invalid Date,然后它以别的方式失败:JSON.stringify 把它写成 null,.toISOString() 抛 RangeError,与它的任何比较结果都是 false。 new Date("不是一串日期") 得到同样的对象,因此 NaN 说明值缺失或被读错,而不是那一天不存在。构造之前 先检查数值:有限、单位正确、绝对值不超过 8.64e15。

日期字符串不是时间戳

两个在人眼里完全一样的字符串,在 UTC+8 的读者那里相差八小时:

new Date("2026-03-15").toISOString();  // 2026-03-15T00:00:00.000Z
new Date("2026/03/15").toISOString();  // 2026-03-14T16:00:00.000Z

标准只定义了 YYYY-MM-DD 这种纯日期形式,按 UTC 解析;标准没有定义的形式——斜杠写法,或者把 T 换成空格且不带偏移的 2026-03-15 10:00——都属于实现自定义,引擎要么按本地时间解析,要么直接拒绝; new Date("2026年3月15日") 在 V8 里就是 Invalid Date。于是同一个字符串在不同地区、不同浏览器指向 不同的瞬间。日期要紧时请带上偏移或 Z:2026-03-15T08:00:00+08:00 与 2026-03-15T00:00:00Z 是同一 时刻。输出同样分裂:toISOString() 永远是 UTC,toString() 与 toLocaleString() 是本地时间,所以 2026-03-14T23:00:00Z 在北京已经是 3 月 15 日——这正是刚过本地午夜生成的”今日报表”与另一个办公室 对不上的原因。

本地钟点不是全序

2026-03-08 的 America/New_York,时钟从 01:00(GMT-5)直接跳到 03:00(GMT-4),那天早上的 02:30 根本不存在。2026-11-01 反过来:05:30Z 和 06:30Z 都显示为 01:30,而显示成 01:45 的 05:45Z 反而 早于显示成 01:30 的 06:30Z。由此有两件事:

  • 墙上时钟字符串不能当主键:在重复的那一小时里它指代两个瞬间,按它排序还会把 01:30 排在 01:45 前面, 尽管这个 01:30 其实更晚发生。
  • 定在 02:30 的任务在跳过的那半天会被漏掉或顺延;定在 01:30 的任务在重复的那天可能触发两次。按本地 时间做小时分桶,会出现一天 23 小时、另一天 25 小时。

算术请在 UTC 里做——用纪元毫秒,或者整数秒字段——只在渲染时选时区,用 Intl.DateTimeFormat(locale, { timeZone: "Asia/Shanghai" })。手工加减偏移正是这些 Bug 的来源。

偏移会变,时区名才是可靠标识

对任何一个实行夏令时的时区,写死的偏移都有半年是错的:America/Los_Angeles 一月是 GMT-8,七月是 GMT-7。今天没有夏令时的时区同样有历史:Asia/Shanghai 现在是恒定的 GMT+8,但 tz 数据库在 1990-05-13 给出 GMT+9,到 1990-09-16 又回到 GMT+8,所以拿今天的规则去换算旧记录,会安静地差一个小时。缩写不是 标识符:CST 至少同时是中国标准时间(UTC+8)、美国中部标准时间(UTC−6)和古巴标准时间(UTC−5), IST 一样含混。请把 IANA 名称(America/…、Asia/Shanghai)保留在数据结构和参数里:日期库只认这种 写法,而它背后的规则本身还会随数据库更新而变。

给人看用格式,给机器存用 UTC

YYYY-MM-DD HH:MM:SS 只有在全员补零且同处一个时区时,字典序才等于时间序。任何一条不成立顺序就会乱: 把不补零的 2026-2-1、2026-11-1、2026-10-9 排序,结果正好是十月、十一月、二月。同一个瞬间也分别 显示为 en-US 的 1/5/2026, 5:07:00 PM、de-DE 的 5.1.2026, 17:07:00 和 zh-CN 的 2026/1/5 17:07:00, 而且这类输出还随引擎的 ICU 数据变化。toLocaleString 是渲染结果,不要拿它当存储格式或排序键。存 UTC—— 带 Z 的 ISO 字符串,或整数毫秒字段——展示时再转换。页面下方链接的时间戳换算工具会把同一个值双向显示, 单位和时区都写清楚。

2038 是存储问题,不是浏览器问题

32 位有符号秒计数器的上限是 2147483647,即 2038-01-19T03:14:07Z;再过一秒它变成 -2147483648, 也就是 1901-12-13T20:45:52Z。你的运行环境没有这个问题——Date 是毫秒精度的浮点数,能撑到公元 275760 年。仍然有问题的地方是那些把这个数塞进 4 个字节的场合:32 位嵌入式与物联网固件、较旧 32 位 ABI 上的 time_t、声明为 32 位整数或 MySQL TIMESTAMP 类型的列(该类型止步于这条边界,DATETIME 与 BIGINT 不受影响),以及 ZIP 与 MS-DOS 时间戳这类老文件格式:它们从 1980 年起算、大约在 2107 年封顶,而且完全 不携带时区。识别办法是:来自设备或数据转储的内容里出现了 1901 年的翻转。

负数时间戳是合法的

1970 年之前的日期都是负数:new Date(-982000000000) 是 1938-11-19T06:13:20Z,Date 处理起来毫无 困难。随后两个惯用检查把数据弄坏:value > 0 会拒绝一切 1970 年前的日期,出生日期和档案记录都在这条 规则下失效;if (!value) 连 0 一起拒掉,而 0 恰恰是纪元那一刻的合法时间戳,同时又常被弱类型接口 当成”未设置”。先把单位定下来,再校验是否有限、是否落在范围内;至于 1938 年是不是一个合理的生日, 交给业务规则去判断。

打开工具: Unix 时间戳转换

返回指南列表

更多指南