先记住这个答案
夏令时切换会让本地时间出现断层和重叠。以纽约为例,2024 年 3 月 10 日凌晨时钟从 01:59 直接跳到 03:00,02:30 不存在,这时 new Date(2024, 2, 10, 2, 30) 不会报错,而是得到 03:30。11 月 3 日时钟从 01:59 拨回 01:00,01:30 出现两次,Date 取前一次。所以两类计算要分开:「过 24 小时」这种持续时长,直接在时间戳上加毫秒;「明天早上 9 点」这种日历时间,按年月日推进,再让时区规则去换算。
- 春季拨快:一段本地时间不存在,Date 会往后挪
- 秋季拨慢:同一钟点出现两次,Date 取较早那次
- 加 24 小时和加一天,在切换那天会差 1 小时
- 构造成功不代表时间存在,要回读字段核对
那一小时去哪了
夏令时就是夏天把时钟拨快一小时,冬天再拨回来。拨快的那一刻,墙上的钟跳过了一小时;拨回的那一刻,同一小时要走两遍。时间本身是连续的,断的是本地钟点。
const clock = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York', hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
})
// 春季:UTC 相差 30 分钟,纽约的钟却跳了 90 分钟
console.log(clock.format(new Date('2024-03-10T06:30:00Z')))
console.log(clock.format(new Date('2024-03-10T07:00:00Z')))
// 秋季:UTC 相差 1 小时,纽约的钟显示同一个时刻
console.log(clock.format(new Date('2024-11-03T05:30:00Z')))
console.log(clock.format(new Date('2024-11-03T06:30:00Z')))查看输出与解释
01:30
03:00
01:30
01:30前两行说明 02:00 到 02:59 在纽约那天不存在。后两行说明 01:30 那天出现了两次,第一次是夏令时的 01:30(UTC-4),第二次是标准时间的 01:30(UTC-5)。 所以把本地钟点换成时间戳时会碰到两种麻烦:遇到不存在的钟点,没有对应的时间戳;遇到重复的钟点,有两个候选。Date 的本地构造器对这两种情况各有固定处理:不存在的往后挪,重复的取较早那个。它不会报错,也不给你选择的机会。
加 24 小时和加一天不是一回事
这是业务里最容易出 bug 的地方。比如提醒功能要在「明天同一时间」再提醒一次,很多人直接写 time + 24 * 60 * 60 * 1000。平时没问题,碰上切换那天就会差一小时。
// 以下注释是在 America/New_York 时区运行的结果
const start = new Date(2024, 2, 9, 9, 0) // 3 月 9 日 09:00,切换前一天
const plus24h = new Date(start.getTime() + 24 * 60 * 60 * 1000)
console.log(plus24h.getHours()) // 10,钟点变了
const nextDay = new Date(start)
nextDay.setDate(nextDay.getDate() + 1) // 按日历加一天
console.log(nextDay.getHours()) // 9,还是早上 9 点
console.log((nextDay - start) / 3600000) // 23,这一天只有 23 小时两种写法都没错,回答的是不同的问题。「接口 24 小时后过期」「倒计时还剩多久」是持续时长,按毫秒加才准确。「每天早上 9 点提醒」「预约明天下午 3 点」是日历时间,要按年月日推进,让时区规则决定对应哪个时间戳。
- 日程类业务应该怎么存
- 预约、航班、排班这类数据,只存一个时间戳不够,还要存用户所在的 IANA 时区名,比如
America/New_York,最好连原始的日期和钟点一起存。原因是时区规则会变,各国政府会调整夏令时政策,未来某天的偏移今天未必能确定。有了时区名和本地钟点,到时候再按最新规则算时间戳就行。只存一次getTimezoneOffset()的结果,换季之后就错了。
容易答错的地方
- 用固定 24 小时表示「明天同一时间」
- 跨过切换点时,本地的「明天同一钟点」可能只隔 23 小时,也可能隔 25 小时。先问清楚需求是持续时长还是日历时间,再决定是加毫秒还是加天数。
- 构造完只检查
isNaN - 不存在的本地时间会被自动挪成一个有效的 Date,不会变成 Invalid Date,所以
isNaN(date.getTime())检查不出来。要确认用户输入的钟点真实存在,得把getHours()和getMinutes()读回来和输入比一下。 - 以为全部改成 UTC 就解决了
- UTC 没有夏令时,做时间戳运算确实更安全。但用户要的是「当地每天 9 点」,这个 9 点属于某个时区,全换成 UTC 固定时刻后,换季时提醒就会变成当地 8 点或 10 点。
面试官还会怎么问?
秋季重复的两个 01:30,怎么让用户选?
旧的 Date API 表达不了这个选择,只给本地字段时总是取较早那个。能做的是让输入带上明确的偏移,比如 01:30-05:00,或者用支持歧义选项的库。新的 Temporal API 里 ZonedDateTime 有 disambiguation 参数,可以选 earlier 或 later,也可以直接让它报错。
测试夏令时的代码怎么写才不依赖开发机时区?
在 Node 里用 TZ=America/New_York 环境变量跑测试,或者在 Jest 配置里固定时区。测试样本挑切换点前后的时刻,春季和秋季各一组,同时断言时间戳差值和本地钟点,两个都对才算对。
用 dayjs 或 date-fns 能避开这个问题吗?
库本身不会让不存在的钟点变存在,它们帮的是把规则显式化。比如 dayjs 的 timezone 插件、date-fns-tz 都能按指定 IANA 时区做换算,并且区分加天数和加小时。前提还是得先想清楚业务要的是时长还是日历时间。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。