new Date('2024-01-01') 和 new Date('2024/01/01') 差在哪

两行只差一个分隔符的代码,打印出来的时间却差了整整 8 小时。一个显示 1 月 1 日早上 8 点,另一个显示 1 月 1 日零点,看上去像是浏览器的 bug。

其实这是规范里写明的两条不同解析路径:一条被当成 UTC,另一条走了实现相关的宽松解析。区别一旦看清,后面那些少一天、排序错乱的怪现象就都能解释了。

同一个日期,为什么两个写法差 8 小时?

先把两种写法放在一起跑一遍。

javascript 复制代码
const a = new Date('2024-01-01');
const b = new Date('2024/01/01');

console.log(a.toString());     // Mon Jan 01 2024 08:00:00 GMT+0800
console.log(b.toString());     // Mon Jan 01 2024 00:00:00 GMT+0800

console.log(a.toISOString());  // 2024-01-01T00:00:00.000Z
console.log(b.toISOString());  // 2023-12-31T16:00:00.000Z

console.log(a.getDate());      // 1
console.log(b.getDate());      // 1

在 UTC+8 环境下,横线写法被当作伦敦时间零点,换算到本地就是早上 8 点;斜杠写法被当作本地时间零点,所以本地读数是 0 点。

两者的时间戳相差 8 小时,getDate 却都是 1,这也是它最容易被忽略的原因:只看日期部分,肉眼根本发现不了。

规范里到底是怎么规定的?

ECMAScript 对日期字符串的解析分得很细。它把不含时区偏移的格式拆成两类:只有日期部分的形式,按 UTC 解析;带时间的日期时间形式,按本地时间解析。

MDN 的 Date 文档把这句写得很明确:date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time。翻成中文就是,只有日期的形式按 UTC 解释,带时间的日期时间形式按本地时间解释。

所以 '2024-01-01' 属于前者,被当成 UTC 零点。而 '2024-01-01T00:00:00' 属于后者,会被当成你所在时区的零点,哪怕它看起来只多了一串零。

需要强调一点:这条规则只管规范格式。斜杠写法不在规范认可的格式里,它走的是各引擎自己的宽松解析,行为不做保证。

| 输入写法 | 解析依据 | 时区来源 | UTC+8 下的本地时间 | | 2024-01-01 | 规范,仅日期形式 | UTC | 1 月 1 日 08:00 | | 2024-01-01T00:00:00 | 规范,日期时间形式 | 本地 | 1 月 1 日 00:00 | | 2024-01-01T00:00:00Z | 规范,带 Z 偏移 | UTC | 1 月 1 日 08:00 | | 2024-01-01T00:00:00+08:00 | 规范,带偏移 | 指定偏移 | 1 月 1 日 00:00 | | 2024/01/01 | 非规范,实现相关 | V8 按本地 | 1 月 1 日 00:00 |

表格最后一行值得多看两眼。V8 确实把斜杠写法按本地时间处理,但这只是当前实现的选择,不是标准承诺。换一个引擎,理论上就可能拿到不同的结果。

为什么 toISOString 会少一天?

toISOString 的定义就是输出 UTC 时间,并且固定带 Z 后缀。它不做任何时区换算之外的补偿,也不关心你本地是几点。

于是在东八区,本地时间 1 月 1 日 07:59 转成 UTC 是 12 月 31 日 23:59。这时候取前十个字符,得到的日期是 12 月 31 日,比用户看到的日期早了一天。

javascript 复制代码
const d = new Date(2024, 0, 1, 7, 59); // 本地 1 月 1 日 07:59
console.log(d.toISOString().slice(0, 10)); // 2023-12-31

同样的代码在下午跑就是对的,在早上跑就是错的,这让问题看起来时有时无。写日报、做统计、按天分组时,这类偏差会直接把数据摊到相邻的两天里。

正确的做法是不要用 toISOString 去取本地日期。要么用 getFullYear、getMonth、getDate 这些本地 getter 自己拼,要么明确接受 UTC 语义并统一按 UTC 展示。

可靠的写法有哪几种?

第一种是直接用数字参数构造:new Date(2024, 0, 1)。这里的月份从 0 开始,0 代表一月,所以写 0 而不是 1,这是最容易写错的地方。用数字参数时,时间按本地时区解释,语义明确。

第二种是把偏移写全:'2024-01-01T00:00:00+08:00'。带偏移的字符串属于规范格式,解析结果与运行环境的时区无关,跨时区传输时最稳。

第三种是先按 UTC 构造再取 UTC 分量,适合服务端已经统一用 UTC 存储的场景。此时展示层再按用户时区转换,不要在中间环节反复取本地日期。

格式化输出时也建议自己拼字符串,或者用 Intl 提供的日期格式化能力,不要在 toISOString 的结果上做切片。

如果团队里同时有人写横线、有人写斜杠,最省事的办法是约定一个入口函数,内部固定用数字参数构造,把月份偏移和时区差异都关在里面,外面只传年月日。

JSON.parse 会顺手把日期转成 Date 吗?

不会。JSON 里没有日期类型,JSON.parse 遇到的日期只会是一个普通字符串,不会自动变成 Date 对象。

javascript 复制代码
const obj = JSON.parse('{"day":"2024-01-01"}');
console.log(typeof obj.day); // string
console.log(obj.day instanceof Date); // false

所以从接口拿到日期后,类型转换这一步必须自己做,而且要显式选一种解析方式。直接把它塞进 new Date 是可以的,但要清楚它会被按哪条规则解释。

比较和排序时会出什么问题?

比较两个 Date 用减法或者 getTime,得到的是毫秒差,这条路径是安全的,因为两者都被换算到了同一个时间轴上。

危险的是拿字符串直接比较。'2024-01-01' 和 '2024/01/01' 在字符串层面就不同,字典序和真实时间先后也没有必然关系。

按日期排序时如果混用了两种写法,顺序就会错乱。更隐蔽的情况是同一个列表里既有日期形式又有日期时间形式,一个按 UTC 解析、一个按本地解析,排序结果看上去毫无规律。

所以在把日期放进数组排序之前,先统一转换一次,全部转成时间戳再排。这一步的代价很小,能避免绝大多数莫名其妙的问题。

实际项目里,AI墨衍 建议把日期处理收敛到一个工具函数里,输入输出都带明确时区,不让调用方各自决定解析方式。

反方向也一样:JSON.stringify 遇到 Date 会调用它的 toJSON,输出的是带 Z 的 UTC 字符串。于是本地零点在序列化之后就变成了前一天的 16 点,再解析回来时如果又按本地解释,时间就会漂移。

这类问题的共同点是:出错时程序不报错,只是差 8 小时或者差一天。判断标准只有一条,凡是跨时区或者跨端传递的日期,都把偏移写全;凡是只在本机展示的日期,就用数字参数按本地构造。

相关推荐
Oiiouui2 小时前
linux服务器部署Logto后端与对应前端的实现
linux·服务器·前端·后端
shenlongproxy2 小时前
代理IP故障演练怎么做:注入怎么分档、恢复怎么判定
后端
知识搬运临时工2 小时前
Kafka 发 1 GiB 数据,用户态 CPU 只花 0.016 秒,总耗时反而慢了 11%
后端·kafka
wt95272 小时前
VMware 虚拟机磁盘扩容实战文档(Ubuntu 22.04 + LVM)
linux·后端
王中阳Go2 小时前
预算给够了,时间花错了地方:三跳链路给了 27 秒,实际只用掉 9 秒
人工智能·后端·性能优化
用户394995084632 小时前
文件变了,inotify 为什么没反应
后端
陈随易2 小时前
神级 Skill,让 AI 用 HTML 代替 Markdown 回复
前端·后端·程序员
Gopher_HBo2 小时前
go tool trace 使用指南(Go 1.24)
后端
一条Go2 小时前
能力套餐:菜单、权限、开通范围怎么拆成三层而不打架
后端·github·saas