在 JavaScript 里怎么拿到当前的纪元毫秒?答案就一句:Date.now()。
Date.now() 返回自 Unix 纪元(1970 年 1 月 1 日 UTC 零点)以来的毫秒数。要打时间戳、做时间比较,或者把“此刻”变成一个计算机能排序的值,它就是默认选择。
const nowMs = Date.now(); // 13 位,纪元毫秒
const nowSeconds = Math.floor(nowMs / 1000); // 10 位,Unix 秒
const date = new Date(nowMs); // Date 要的是毫秒
有个小但要命的细节:JavaScript 内置的 Date API 全部以毫秒运作,而大量接口、数据库和命令行工具期待的是以秒计的 Unix 时间。两者一混,一个 2026 年的日期就莫名其妙变成了 1970 年——技术上相当于发邮件挂错了附件。
判断是毫秒还是秒,最快的自检就是数位数:
- 当代以秒计的 Unix 时间戳一般是 10 位。
- JavaScript 那种以毫秒计的纪元时间戳一般是 13 位。
| 任务 | 直接抄这句 | 单位 |
|---|---|---|
| 拿到给 JavaScript Date 用的当前时间戳 | Date.now() |
毫秒 |
| 拿到大多数接口要的当前 Unix 时间戳 | Math.floor(Date.now() / 1000) |
秒 |
| 把 Unix 秒转成 Date | new Date(seconds * 1000) |
输入:秒 |
| 把纪元毫秒转成 Date | new Date(milliseconds) |
输入:毫秒 |
| 把时间戳格式化成 ISO UTC | new Date(ms).toISOString() |
输出:字符串 |
| 测一段代码耗时多久 | performance.now() |
单调递增的毫秒 |
需要在纪元值和日期之间来回转,可以用这两个工具:
Date.now() 究竟返回了什么
Date.now() 返回一个 number,表示自 1970-01-01 00:00:00 UTC 起的毫秒数。它不返回 Date 对象,不携带自己的时区,也不测量某个函数跑了多久。它就是一个时间戳:共享 UTC 时间轴上的一个点。
JavaScript 的 Date 对象底层做的是同一件事——每个对象内部都持有一个相对 Unix 纪元的毫秒值。时区只在把这个值格式化出来展示时才有用,比如通过 UTC 系列方法、本地时间系列方法,或者带 "America/Los_Angeles" 这类 IANA 时区的 Intl.DateTimeFormat。
const ms = Date.now();
console.log(ms); // 1781947500000
console.log(new Date(ms)); // 表示那一刻的 Date 对象
console.log(new Date(ms).toISOString()); // UTC 字符串
同一个时刻,写对和写错分别是这样:
| 值 | 被如何解释 | 结果 |
|---|---|---|
1781947500000 |
JavaScript / Unix 毫秒 | 2026-06-20T09:25:00.000Z |
1781947500 |
Unix 秒 | 2026-06-20T09:25:00.000Z |
1781947500 直接传给 new Date() |
被误当成毫秒 | 1970-01-21T14:59:07.500Z |
用秒还是用毫秒
选哪个单位,取决于这个时间戳下一站要去哪儿。
// 浏览器界面、JS Date、localStorage、客户端事件
const createdAtMs = Date.now();
// 服务端接口、JWT 的 exp/iat、Unix 风格的数据库过滤条件
const createdAtSeconds = Math.floor(Date.now() / 1000);
如果下一行代码还是 JavaScript,用毫秒:
new Date(createdAtMs);
典型场景包括:
- 浏览器里产生的埋点事件
- 前端缓存的过期时间
- React / Vue / Svelte 的界面状态
- 文档里明确写了是纪元毫秒的接口字段
而当你面对的是 Unix 风格的系统时,用秒:
- JWT 的
exp、iat和nbfclaims - 文档里写明是 Unix 秒的接口字段(很多支付和基础设施类 API 都是)
- 期待秒值的 SQL 纪元过滤条件
- Linux shell 命令,比如
date -d @SECONDS - 给 PHP 的
time()或 Python 的int(time.time())用的数据
诀窍是别让读你代码的人去猜。createdAt 这个字段名什么单位都没交代。createdAtMs、createdAtUnixSeconds、expiresAtEpochMs 是啰嗦了一点,但它们能避开一类代价高得离谱的 bug。
很多时候,最体贴的命名恰恰是最无聊的命名。
经典的 1970 bug
这是每个 JavaScript 开发者迟早都会撞上的时间戳错误:
new Date(1700000000).toISOString();
// "1970-01-20T16:13:20.000Z" ← 如果这是 Unix 秒,那就错了
new Date(1700000000000).toISOString();
// "2023-11-14T22:13:20.000Z" ← 正确
原因很简单:new Date(number) 永远把数值参数按毫秒解释。你给它一个 10 位的 Unix 秒,JavaScript 就把它当成一个很小的毫秒值,于是日期落在 1970 年 1 月。
构造 Date 之前先把秒换算好:
const unixSeconds = 1700000000;
const date = new Date(unixSeconds * 1000);
如果你的代码确实要处理来自不同来源的时间戳,写一个小的边界辅助函数是合理的。但请把它当成兼容层,而不是一份定义良好的 API 约定的永久替代品。
function unixToDate(value) {
const n = Number(value);
if (!Number.isFinite(n)) {
throw new TypeError("Timestamp must be numeric");
}
return new Date(Math.abs(n) < 1e11 ? n * 1000 : n);
}
1e11 这个阈值只适用于当代的普通时间戳:未来几千年里 Unix 秒都在它之下,而当下的纪元毫秒远在它之上。如果调用方要处理归档数据、科研数据,或者故意设得很古老/很遥远的日期,那就老老实实把单位问清楚。
new Date().getTime() 和 Date.now()
这两个表达式产出的是同一类值:当前纪元时间的毫秒数。
Date.now();
new Date().getTime();
因为时间一直在走,连着调用它们并不保证返回同一个数字。但实际上两者回答的是同一个问题。
差别在于过程。Date.now() 是静态方法,直接返回一个数字;new Date().getTime() 则先创建一个 Date 对象,再从中取出毫秒值。
对一次性的代码来说这点差别微不足道。但如果你要的只是一个时间戳,Date.now() 是更清楚的默认选择——尤其是在紧凑的循环、请求日志、埋点采集或渲染帧回调里。
如果你本来就要一个 Date 对象,那就直接造:
const date = new Date();
const iso = date.toISOString();
如果你只要纪元毫秒,就用 Date.now():
const sentAtMs = Date.now();
如果手上已经有一个 Date 对象了,调 date.getTime() 取它的纪元毫秒。+new Date() 也能得到同样的数值,但它简写到会拖慢代码评审的程度;生产代码里请用有名字的方法。
Date.now() 与 performance.now()
这两个 API 是干不同活的。
Date.now() 读的是墙上时钟,所以适合记录某件事发生在什么时候。但墙上时钟是会变的:用户可能改系统时间,虚拟机可能从挂起状态恢复,时间同步服务也可能把时钟校正一下。
测量流逝时间请用 performance.now()。它是一个相对 performance.timeOrigin 的高分辨率单调递增计时器,设计上就保证在当前运行期间只会往前走。
// 适合回答"这件事什么时候发生的?"
const receivedAtMs = Date.now();
// 适合回答"这个操作花了多久?"
const startTime = performance.now();
await doWork();
const elapsedMs = performance.now() - startTime;
一条好用的经验法则:
| 问题 | 用哪个 API |
|---|---|
| 什么时候发生的? | Date.now() |
| 花了多久? | performance.now() |
| 接口负载里该放什么? | 按文档写明的秒或毫秒 |
| 给用户看什么? | Intl.DateTimeFormat |
把时间戳格式化成 UTC 或用户所在时区
一个 Date 是时间上的一个点。格式化决定的是这一刻在人眼里长什么样:是 UTC,是客户所在时区的本地时间,还是按他们语言习惯呈现的样子。
const date = new Date(1781947500000);
date.toISOString();
// "2026-06-20T09:25:00.000Z"
new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "America/New_York",
}).format(date);
// "Jun 20, 2026, 5:25 AM"
toISOString() 特别适合日志、接口响应和其他机器可读的输出。结尾那个 Z 永远表示它是 UTC。它是刻意保持一致,而不是刻意讨人喜欢。
给用户看的文字,请用 Intl.DateTimeFormat:locale 控制语言和呈现习惯,timeZone 控制用哪块钟来显示这个瞬间——这是两个互相独立的选项。比如一个住在东京的美国人,可能想要 "Asia/Tokyo" 时区搭配 "en-US" 的格式。
时区请用 "UTC"、"America/New_York"、"Asia/Tokyo" 这样的 IANA 名字,它们会在需要时正确处理夏令时。别为一个实行夏令时的地方硬编码 "-05:00" 这样的偏移——纽约并不总是比 UTC 慢五小时。
重复输出时请复用同一个 formatter。没必要为表格的每个单元格或操作日志的每一行都新建一个:
const fmt = new Intl.DateTimeFormat("zh-CN", {
timeZone: "UTC",
dateStyle: "medium",
timeStyle: "medium",
});
const formattedRows = rows.map((row) =>
fmt.format(new Date(row.createdAtMs)),
);
务实的分工很简单:存储和传输时,把瞬间表示为纪元毫秒或 ISO 字符串;只在有人要读它的那个边界上做格式化。这样底层的时间保持不变,而每个读者都能在对自己有意义的那块钟上看到它。
Intl.DateTimeFormat 接受 IANA 时区名;不传 timeZone 时会退回到运行时的本地时区。
对于要存储或传输的瞬间,toISOString() 是最安全的内置输出,因为它永远是 UTC。toUTCString() 给出的是人可读的 UTC 字符串;而 toString() 和 toLocaleString() 在你没给 Intl.DateTimeFormat 明确选项时,会依赖宿主的 locale 或时区。需要保住一个瞬间时,别把这些用于显示的字符串持久化下来。
安全地解析日期字符串
JavaScript 可靠支持的是 ECMAScript 的日期时间字符串格式,它和 ISO 8601 非常接近。除此之外的写法,可能一个运行时接受、另一个拒绝,或者给出完全不同的解释。那不叫解析策略,那叫一场伪装起来的、未来某天的 debug 之夜。
在系统边界上,请使用明确的 UTC 时间戳,或者带明确偏移的时间戳:
new Date("2026-06-20T09:25:00Z"); // UTC
new Date("2026-06-20T09:25:00-04:00"); // 明确偏移
Date.parse("1970-01-01T00:00:00Z"); // 0
Z 表示 UTC。像 -04:00 这样的偏移同样能确定一个瞬间,所以 JavaScript 可以安全地把它转成内部的纪元毫秒值。
而在接口、数据库导入和其他系统边界上,别依赖这些格式:
new Date("06/20/2026"); // 遗留写法,解析行为依赖具体实现
new Date("01/02/2026"); // 人看着有歧义,代码里也脆弱
new Date("Jun 20, 2026"); // 又一次押注在遗留解析器上
看着眼熟的字符串未必可移植。01/02/2026 尤其阴险:它是 1 月 2 日还是 2 月 1 日?就算你们团队用的每个浏览器今天都给出一致结果,这个格式也已经输掉了最重要的一场测试——评审的人一眼看不出它是什么意思。
还有一条微妙的规则:
new Date("2019-01-01"); // 按 UTC 处理
new Date("2019-01-01T00:00:00"); // 按本地时间处理
第一个是只有日期的字符串,被解释为 UTC 的零点。第二个带了时间却没有偏移,于是 JavaScript 假定它是运行时本地时区的零点。在开发机和另一个地区的服务器上,这两者可能是完全不同的两个时间点。
另外,对于用户填进来的日期,别用那种一厢情愿的 new Date(input)——请用一个知道格式的库或者 Temporal 去解析。
转换之前先校验时间戳输入
来自接口请求、CSV 导入、webhook、浏览器事件和查询字符串的时间戳,一律不要轻信。构造 Date 之前先检查这个值,更重要的是——让发送方把单位声明清楚。
一个叫 parseEpochMs() 的解析函数,不该暗中假定“小的值就是秒”。这么做对遗留系统迁移或许方便,却让别处的约定更难理解。
function parseEpochMs(input) {
if (typeof input !== "number" && typeof input !== "string") {
throw new TypeError("timestamp must be a number or numeric string");
}
const n = Number(input);
if (!Number.isFinite(n)) throw new TypeError("timestamp is not finite");
const ms = Math.abs(n) < 1e11 ? n * 1000 : n;
const date = new Date(ms);
if (Number.isNaN(date.getTime())) throw new RangeError("timestamp is outside Date range");
return { ms, date };
}
还有几道护栏值得留着:
- 在
Number("")把空字符串变成0之前,先把它拒掉。 - 在
Number(true)把布尔值变成1之前,先把它拒掉。 - 拒绝一个 webhook 或 CSV 记录时,把原始值连同请求 ID 或行号一起留下来。上游发来的脏数据会因此好查得多。
- 如果某个接口确实允许小数秒,就把取舍策略定义好、写进文档,而不是让转换在不知不觉中发生。
- “秒还是毫秒”的经验式判断,只允许出现在一个名字起得很明白的兼容适配器里。它应该是临时管道,而不是你对外的公开约定。
JavaScript 里的亚毫秒精度
Date.now() 返回的是整毫秒。对绝大多数事件时间戳来说,这正好够用。
但测量流逝时间时,performance.now() 更合适:它返回一个相对 performance.timeOrigin 的、高分辨率的单调递增毫秒值,所以即便时钟被调整,时长也不会倒退。
const start = performance.now();
await doWork();
const elapsedMs = performance.now() - start;
它适合在同一个页面或进程内部关联各项测量,但它不是 API 负载里 Date.now() 的升级替代品。每个浏览上下文或 worker 都有自己的时间原点,浏览器出于隐私考虑可能降低计时器分辨率,而且一个带小数的数值并不保证对应同样精确的真实时间。多几位数字,有时只是让人误以为可靠的伪装。
同样地,别把这些亚毫秒的值发给一个只声明支持 Unix 秒或 Unix 毫秒的接口。
Temporal:更好的日期 API,但先确认可用性
Temporal 是 JavaScript 新的日期时间 API。它把 Date 混作一团的那些概念区分开了:瞬间、纯日期、钟面时间、带时区的日期时间、时长和日历。
这些场景适合用 Temporal:
- 跨越夏令时去加减天数或月份
- 表示一个不带时区的日历日期
- 把“钟面预约时间 + IANA 时区”变成一个瞬间
- 不想再调那些会改变自身状态的 Date 方法
- 代码库可以要求 Node.js 26+,或者能交付 polyfill
例如:
const instant = Temporal.Instant.fromEpochMilliseconds(Date.now());
const zdt = instant.toZonedDateTimeISO("America/New_York");
const tomorrowSameWallClock = zdt.add({ days: 1 });
截至 2026 年 7 月 15 日,MDN 仍把 Temporal 列为有限可用,因为一些用户量很大的浏览器还不支持它。Node.js 26 则内置了 Temporal。面向公众的 Web 应用,在全面改用原生 Temporal 之前,请保留 polyfill 或做特性检测。
几个该避开的时间戳坑
| 症状 | 原因 | 解决办法 |
|---|---|---|
| 日期是 1970 年 1 月 | Unix 秒被传给了 new Date() |
乘以 1000 |
| 日期在遥远的未来 | 毫秒被发给了按秒收的接口 | 除以 1000 并向下取整 |
| 本地测试通过、CI 上失败 | 运行时时区不同 | 传 timeZone,或者断言 ISO UTC |
| 相等判断不成立 | 在比较 Date 对象本身 | 比较 .getTime() 的值 |
| 用户看到的日期差了一天 | 纯日期与时区语义错配 | 把纯日期值单独建模 |
| 夏令时导致多/少了一小时 | 直接拿毫秒做日历算术 | 用 Temporal 或感知时区的库 |
一月变成了第 0 月 |
getMonth() 和分量构造函数都是 0 起始 |
显示时加 1;new Date(year, month, day) 里一月写作 0 |
| “明天”把钟面时间弄变了 | 加 86400000 毫秒跨过了夏令时切换 |
用感知日历的 API;纯 Date 代码里,有意识地用 setDate(getDate() + 1) |
代码评审时最有用的一句话就是:这个值在这个边界上,单位是什么、时区是什么?
一些可以直接抄的写法
// 当前纪元毫秒
const nowMs = Date.now();
// 当前 Unix 秒
const nowSeconds = Math.floor(Date.now() / 1000);
// Unix 秒 → ISO 字符串
const iso = new Date(1700000000 * 1000).toISOString();
// 纪元毫秒 → ISO 字符串
const isoFromMs = new Date(1700000000000).toISOString();
// Date → 纪元毫秒
const ms = new Date("2026-06-20T09:25:00Z").getTime();
// Date → Unix 秒
const seconds = Math.floor(new Date("2026-06-20T09:25:00Z").getTime() / 1000);
// 按 UTC 格式化
const utcText = new Intl.DateTimeFormat("zh-CN", {
timeZone: "UTC",
dateStyle: "medium",
timeStyle: "medium",
}).format(new Date());
// 按具名时区格式化
const nyText = new Intl.DateTimeFormat("zh-CN", {
timeZone: "America/New_York",
dateStyle: "medium",
timeStyle: "medium",
}).format(new Date());
感谢阅读。
Frequent questions:
- Q: JavaScript 的 Date 里存时区吗?
- A: 不存。Date 里只有一个时间戳值。本地时区、UTC 和具名时区都是显示时的选择,由 toString()、toISOString()、toLocaleString() 和 Intl.DateTimeFormat 这些方法后续施加。
- Q: 怎么把 Unix 秒转成 JavaScript 的 Date?
- A: 构造 Date 之前先把秒乘以 1000:new Date(seconds * 1000)。如果输入本来就是纪元毫秒,直接传:new Date(milliseconds)。
- Q: 怎么把 JavaScript 的 Date 格式化到指定时区?
- A: 用 Intl.DateTimeFormat 或 toLocaleString,并带上 timeZone 选项,比如 America/New_York 或 UTC。不传这个选项时,JavaScript 默认用运行时的本地时区。
- Q: Date.parse() 对所有日期字符串都安全吗?
- A: 不安全。ISO 日期时间字符串格式是可移植的,其他格式则由各实现自行定义。请优先用 2026-06-20T09:25:00Z 这样的写法,或者带上明确的偏移量。
- Q: 该用 Temporal 代替 Date 吗?
- A: 在支持它、或者你能交付 polyfill 的地方可以用,尤其是做时区和日历运算时。MDN 仍把 Temporal 标为有限可用,而 Node.js 26 已默认启用。对于简单的时间戳读取和广泛的浏览器兼容,Date 依然够用。
- Q: Date.now() 比 new Date().getTime() 快吗?
- A: 通常是的,因为 Date.now() 不用分配 Date 对象。偶尔读一次的话差别可以忽略,但在热点路径上,Date.now() 是更干净的默认选择。