Menu

JavaScript 日期和时间:new Date、格式化与比较

JavaScript 的 Date 对象到底是怎么工作的:创建日期、格式化、日期运算、处理时区,以及那些容易让人踩坑的怪异之处。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

Date 对象表示一个时间点

JavaScript 中的 Date 表示一个单独的时刻:在内部,它只是自 1970 年 1 月 1 日 UTC(即“Unix 纪元”)以来的毫秒数。其余的一切,包括年、月、日、时区和格式,都是叠加在这个数字之上的视图。

now.getTime() 是原始的毫秒数。Date 做的所有事情,比较、加天数、格式化,归根结底都是操作这个数字,然后再重新解读它。

把这个模型记在脑子里。Date 并不是“巴黎的 3 月 14 日”,而是一个全球统一的时刻:从不同的时区去看,它可以显示为巴黎的 3 月 14 日,也可以显示为洛杉矶的 3 月 13 日。

创建日期

构造 Date 主要有四种方式:

有两点需要注意:

  • 按各部分构造时,月份从 0 开始计数。2 表示三月,一月是 0。这是差一错误的常见来源。好在整个 API 里的月份都是从 0 开始的,至少它自身是一致的。
  • new Date("2026-03-14")(不带时间)会被解析为 UTC 午夜。new Date("2026-03-14T09:30")(不带 Z)会被解析为本地时间。这种不对称是一个经典陷阱。

如果你只想要表示“此刻”的数字,优先使用 Date.now(),它省去了创建对象的开销:

测量耗时、处理超时,以及任何不需要日历运算的场景,Date.now() 都是合适的工具。

读取日期的各个部分

拿到一个 Date 后,可以用 getter 取出各个组成部分。每个 getter 都有两种版本:本地时间和 UTC。

本地版本取决于运行代码的那台机器。如果你要在不同用户和服务器之间存储或比较日期,就要明确选择 UTC,否则你会追查一些莫名其妙的 bug。经验法则:写入数据库或日志的内容用 UTC getter;要展示给人看的内容用本地 getter。

不要使用 getYear()。它是一个遗留方法,返回 year - 1900,只是为了兼容而存在。始终使用 getFullYear()。

为人类格式化日期

只要你在意输出结果,就不要用 date.toString(),它的输出取决于语言区域和引擎。有两个格式化工具值得了解。

如果需要标准的机器可读字符串,就用 toISOString():

打印日志、存入 JSON 或通过网络发送时,都应该使用这种格式。它始终是 UTC,始终没有歧义。

如果是给人看的字符串,就用 Intl.DateTimeFormat,或者封装了它的 toLocale* 系列方法:

Intl.DateTimeFormat 能处理语言区域、时区以及你想要的各种字段组合。在手写 ${year}-${month}-${day} 这样的格式之前,先考虑用它:这种手动拼接字符串的写法,正是月份差一错误的温床。

比较日期

两个表示同一时刻的 Date 对象并不 === 相等:=== 检查的是对象是否为同一个,而不是值。应该比较它们的时间戳:

如果要比较先后,比较运算符可以直接使用,因为它们会把日期强制转换为数字:

两个日期相减,得到的是相差的毫秒数。除以 1000 * 60 * 60 * 24 就得到天数。第一次先把这个常量完整写出来,过一段时间你就能一眼认出 86_400_000。

日期运算

JavaScript 没有 addDays 方法。惯用的做法是使用 setDate、setMonth 等方法:它们接受超出范围的值,并能正确地进位:

有两点值得强调:

  • new Date(date) 会复制日期。setDate 会修改原对象,所以一定要先复制,否则你会改掉调用方的值。
  • 对一个有 31 天的月份调用 setDate(35),会自动进位到下个月。setMonth(14) 也一样,它会把年份往后推。这让日期运算远比看上去轻松。

对于复杂的需求,比如工作日、周期性事件、需要考虑月份的时长,就用一个库(date-fns、Luxon,或者即将推出的 Temporal API)。除了“加几天”这种简单运算之外,自己实现日历计算就是一个泥潭。

认清时区的现实

时区是日期 bug 最大的单一来源。值得牢记的规则:

  • Date 存储的是一个 UTC 时刻。只有在读取各部分或格式化时,才会应用时区。
  • getHours()、getDate() 等方法使用的时区,是运行代码的那台机器的本地时区。服务器和浏览器的时区常常不一致。
  • new Date("2026-03-14")(只有日期)解析为 UTC。new Date("2026-03-14T00:00")(有时间、没有时区)解析为本地时间。new Date(2026, 2, 14)(各部分)是本地时间。

如果显示时需要指定时区,就把 timeZone 传给 Intl.DateTimeFormat:

同一个时刻,两种视图。Date 对象本身并没有改变。

一个可运行的小例子

把这些组合起来:一个格式化“多久之前”的函数:

输入时间戳,输出人类可读的字符串。现实中九成的日期代码都是这个模式:两个时刻相减,除以一个单位,取整,再格式化。

本篇要点

  • Date 是一个 UTC 时刻。时区只在读取或格式化时出现。
  • 时间戳用 Date.now(),日历相关的工作用 new Date()。
  • 存储和日志用 toISOString(),给用户看用 Intl.DateTimeFormat。
  • 用 getTime() 或 </> 比较,绝不要用 ===。
  • 月份从 0 开始计数。小心只有日期的字符串的解析陷阱。
  • 严肃的日期运算,交给库来做。

下一步:URL 和查询字符串

日期经常出现在 URL 中:按日期范围筛选,或者把时间戳作为查询参数传递。手动解析和构建 URL 和手动格式化日期一样容易出错,而标准库提供了一个 URL 对象,可以干净利落地处理它。下一篇就讲这个。

常见问题

JavaScript 中如何获取当前日期?

不带参数调用 new Date()。它返回一个 Date 对象,表示构造函数运行的那一刻。如果你只需要一个数字时间戳(自 1970 年以来的毫秒数),就用 Date.now():它更快,也不会创建完整的对象。

JavaScript 中如何比较两个日期?

比较它们的时间戳,而不是 Date 对象本身。a.getTime() < b.getTime() 可行,a < b 也可行,因为 < 会把日期强制转换为数字。但 a === b 不行:=== 检查的是对象是否为同一个,所以两个表示同一时刻的 Date 对象永远不会严格相等。

JavaScript 中如何格式化日期?

凡是给用户看的内容,都用 Intl.DateTimeFormat 或 date.toLocaleDateString(),它们能正确处理语言区域和时区。对于机器可读的输出,date.toISOString() 会给出类似 2026-03-14T09:30:00.000Z 的标准字符串。不要用 date.toString() 来存储,它的格式依赖于语言区域。

为什么我的 JavaScript 日期差了一天?

通常是时区问题。new Date('2026-03-14') 会被解析为 UTC 午夜,但 date.getDate() 返回的是本地时区的日期,可能是前一天。用 getUTCDate() 获取 UTC 日期,或者用 new Date(year, month, day) 构造日期,它从一开始就使用本地时间。

Coddy 编程语言插图

和 Coddy 一起学编程

开始