先记住这个答案
Date 内部只存一个数:距离 1970-01-01T00:00:00Z 的毫秒数。几种构造方式的区别,是怎么算出这个数。传数字,直接就是这个毫秒数。传年、月、日这样的多个参数,按运行环境的本地时区理解,而且月份从 0 开始。传字符串要看格式:带 Z 或 +08:00 就按它说的时区;只有日期的 YYYY-MM-DD 按 UTC;有日期有时间但没写偏移,按本地时区。
- 数字参数是 UTC 纪元毫秒,和时区无关
- 多参数按本地时区,月份 0 是一月
- 只有日期的 ISO 字符串按 UTC,加了时间没偏移按本地
- 接口传时间点一律带
Z或偏移
把几种写法放在一起对比
最直观的办法是把它们都转成 toISOString()。toISOString() 永远输出 UTC,看它就知道 Date 里存的到底是哪个时间点。
// 数字:从 1970-01-01T00:00:00Z 起的毫秒数
console.log(new Date(0).toISOString())
// 只有日期的 ISO 字符串:按 UTC 零点
console.log(new Date('2026-09-10').toISOString())
// 带 Z:明确是 UTC
console.log(new Date('2026-09-10T08:00:00Z').toISOString())
// 带偏移:东八区 8 点就是 UTC 0 点
console.log(new Date('2026-09-10T08:00:00+08:00').toISOString())查看输出与解释
1970-01-01T00:00:00.000Z
2026-09-10T00:00:00.000Z
2026-09-10T08:00:00.000Z
2026-09-10T00:00:00.000Z这四种写法在任何时区的机器上跑,结果都一样,因为输入本身就说清了是哪个时区。第二行容易被忽略:只写日期不写时间,规范规定按 UTC 零点理解,而不是本地零点。
会随机器时区变化的两种写法
另外两种写法没有说明时区,引擎只能用运行环境的本地时区去补。同一行代码放在北京和纽约的机器上,存下的时间戳就不一样。
// 以下注释是在东八区(Asia/Shanghai)运行的结果
const a = new Date('2026-09-10') // 只有日期:UTC 零点
const b = new Date('2026-09-10T00:00') // 有时间没偏移:本地零点
const c = new Date(2026, 8, 10) // 多参数:本地零点,8 表示九月
console.log(a.toISOString()) // 2026-09-10T00:00:00.000Z
console.log(b.toISOString()) // 2026-09-09T16:00:00.000Z
console.log(c.toISOString()) // 2026-09-09T16:00:00.000Z
console.log(b.getTime() === c.getTime()) // truea 和 b 只差一个 T00:00,在东八区却差了 8 小时。这是真实项目里最常见的日期 bug:后端返回 2026-09-10,前端 new Date() 之后用 getDate() 显示,在美洲时区的用户会看到 9 号,因为 UTC 零点在那边还是前一天晚上。
toISOString() 只是把存好的时间戳按 UTC 打印出来,它不会改变构造时的时区选择。差异发生在构造那一步。
月份从 0 开始和日期溢出
多参数构造还有两个老坑。一是月份参数叫 monthIndex,0 是一月,11 是十二月,所以 new Date(2026, 9, 1) 是十月一号。二是字段越界不会报错,会自动进位。
const d = new Date(2026, 1, 31) // 想要 2 月 31 日
console.log(d.getMonth(), d.getDate()) // 月份 2 是三月
const next = new Date(2026, 12, 1) // 月份 12 超出范围
console.log(next.getFullYear(), next.getMonth())查看输出与解释
2 3
2027 02026 年 2 月只有 28 天,31 日多出来的 3 天滚进了三月,得到 3 月 3 日。月份 12 同理,进位成下一年的一月。这个行为有时候很好用,比如 new Date(year, month + 1, 0) 能拿到当月最后一天。但做表单校验时就要当心,构造成功不代表用户输入的日期存在,得把年月日读回来和输入比一下。
容易答错的地方
- 以为没写
Z的字符串都按本地时间 - 只有日期的标准格式
YYYY-MM-DD是例外,按 UTC 解析。日期加时间但不带偏移,才按本地时间。两种格式长得很像,结果可能差一个时区。 - 以为
toISOString()会把本地构造转成 UTC 构造 toISOString()只是换一种方式显示同一个时间戳。new Date(2026, 8, 10)在构造时就已经按本地零点算好了,打印出来的16:00Z就是本地零点对应的 UTC 时间。- 依赖非标准字符串格式
2026/09/10或者Sep 10 2026这类格式大多数浏览器能解析,但规范没有规定,结果可能因实现不同而不同。存储和接口里只用标准的 ISO 8601 格式。
面试官还会怎么问?
想按 UTC 的年月日构造 Date 怎么写?
用 new Date(Date.UTC(2026, 8, 10))。Date.UTC 接收和多参数构造一样的字段,但按 UTC 理解,返回时间戳。注意它的月份同样从 0 开始。
不传参数的 new Date() 是什么时区?
它就是当前时刻,和时区无关,内部存的是此刻的 UTC 毫秒数。只有在你读 getHours() 或者 toString() 时,才会按本地时区显示出来。
后端传时间,用什么格式最稳?
时间点用带 Z 或明确偏移的 ISO 8601 字符串,或者直接传毫秒时间戳。生日、合同日期这类纯日历日期,传 YYYY-MM-DD 字符串,前端也当字符串处理,别急着 new Date(),否则就会碰上前面说的时区偏一天问题。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。