学完这篇你能得到:一套把 JavaScript 日期时间处理“踩坑清单”和“正确写法”一次讲清的方法,包括 Date 内部到底存的是什么、字符串解析为什么时好时坏、跨时区怎么显示、以及业务里算天数/存数据库该怎么写。
第一步:先认清 Date 内部只有一个数字
new Date() 创建的对象,本质上只保存一个值:距离 1970-01-01T00:00:00Z 的毫秒数(时间戳)。它没有“存着时区”这回事,时区只在你把时间戳转成人能看懂的字符串时才参与计算。
const d = new Date();
console.log(d.getTime()); // 1699999999999,一个绝对时刻
console.log(d.toISOString()); // 2023-11-14T22:13:19.999Z,UTC 表示
console.log(d.toString()); // 2023-11-15 06:13:19 GMT+0800(本地时区)
所以记住一句话:存储和传输永远用时间戳或 ISO 字符串,显示才考虑时区。
注意:Date.now() 返回的是毫秒数,不是 Date 对象;要对象就写 new Date(Date.now()),但通常没必要。
第二步:构造 Date 的四种方式,只有两种可靠
new Date(1699999999999) // ✅ 时间戳,绝对安全
new Date(2023, 10, 15, 6, 13) // ✅ 数字参数,按“本地时区”解释
new Date("2023-11-15T06:13:19+08:00") // ✅ 带时区的 ISO 字符串
new Date("2023-11-15") // ⚠️ 按 UTC 解析
new Date("2023/11/15") // ⚠️ 按本地时区解析
new Date("2023-11-15 06:13:19") // ⚠️ 非标准格式,行为因浏览器而异
两个大坑:
new Date(2023, 10, 15) 的月份是 0 开始的,10 表示 11 月。这是最常见的差一个月 bug。
"2023-11-15"(短横线日期)会被当成 UTC 零点,在东八区打印出来是 11-15 08:00;换成斜杠 "2023/11/15" 就变成本地零点。同一个日期字符串,两种结果。
注意:只要字符串里没有明确写时区(Z 或 +08:00),解析结果就不可靠。生产代码里不要依赖浏览器去猜。
第三步:本地方法与 UTC 方法必须分清
Date 提供了成对的方法,区别只在于“按哪个时区算”:
| 用途 | 本地 | UTC |
|---|
| 取小时 | getHours() | getUTCHours() |
| 取日期 | getDate() | getUTCDate() |
| 取星期 | getDay() | getUTCDay() |
| 设置 | setHours() | setUTCHours() |
要拿“用户所在时区相对 UTC 差多少分钟”,用 getTimezoneOffset()(东八区返回 -480,注意是负数且以分钟计,和常见的 +08:00 符号相反)。
const offsetHour = -new Date().getTimezoneOffset() / 60; // 8
注意:别看一个值直接做日期加减。date.getTime() + 86400000 在遇到夏令时切换的那天会偏差一小时。需要“加一天”时用 setDate(d.getDate() + 1),让引擎处理。
第四步:按指定时区格式化,交给 Intl
不要自己拼接 yyyy-MM-dd,Intl.DateTimeFormat 天生支持 timeZone,还能本地化:
const d = new Date(Date.now());
new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: false,
}).format(d); // "2025-03-08 14:20"
new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
dateStyle: 'full', timeStyle: 'short',
}).format(d); // "Saturday, March 8, 2025 at 1:20 AM"
需要注意,format() 输出的是给人看的字符串,不要再拿去 new Date() 解析。要机器可读就用 toISOString()。
第五步:业务里真正高频的三个场景
1. 存数据库 / 传接口
统一 date.toISOString() 或 date.getTime(),服务端按 UTC 存。前端展示时再按用户时区格式化。绝不要把 '2025-03-08 14:20:00' 这种不带时区的字符串直接入库。
2. 算相差天数
先抹平时分秒,避免 23 小时被算成 0 天:
function diffDays(a, b) {
const MS = 86400000;
const da = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
const db = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
return Math.round((db - da) / MS);
}
3. 判断“今天”
isToday 这类判断要基于同一个时区做比较,不能把本地日期和 UTC 日期混着比:
const isToday = (d) => {
const n = new Date();
return d.getFullYear() === n.getFullYear()
&& d.getMonth() === n.getMonth()
&& d.getDate() === n.getDate();
};
注意:如果服务器时区是 UTC 而用户在东八区,凌晨 0-8 点这段时间“今天”会算错。要么后端传时间戳前端算,要么显式指定业务时区。
第六步:什么时候该引库,什么时候等 Temporal
- 只是格式化、加减几天:原生 +
Intl 够用,不必引依赖。
- 涉及多时区、夏令时、重复日程、复杂的“每月最后一个周五”:用
date-fns(函数式、可摇树)或 dayjs(体积小、API 像 moment)。旧的 moment.js 已停止新功能开发,新项目不建议再用。
- 未来的标准是 Temporal(
Temporal.ZonedDateTime、Temporal.PlainDate),把“绝对时刻”和“墙上时间”彻底分开,正是为了解决上面这些坑。新环境可以先做能力检测再渐进使用。
小结
- Date 内部只有一个毫秒时间戳,时区只影响“读出来”的结果,不影响存的值。
- 构造时优先用时间戳或带时区偏移的 ISO 字符串;
new Date(2023, 10, 15) 的月份从 0 开始,短横线日期字符串按 UTC 解析。
- 存储传输用
toISOString() / getTime(),展示用 Intl.DateTimeFormat + timeZone。
- 日期加减用
setDate() 之类的语义方法,别硬加 86400000,避开夏令时。
- 复杂时区逻辑上
date-fns / dayjs,长期看齐 Temporal。