两行只差一个分隔符的代码,打印出来的时间却差了整整 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 小时或者差一天。判断标准只有一条,凡是跨时区或者跨端传递的日期,都把偏移写全;凡是只在本机展示的日期,就用数字参数按本地构造。