[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f2w26sny81i6eh":3},{"_id":4,"slug":5,"title":6,"subtitle":7,"kind":8,"cards":9,"tags":29,"categories":30,"source":31,"lang":34,"author":35,"audioState":38,"stats":39,"publishedAt":42,"renderer":43},"6abc0caaca21c797c7e9e457","5-8-6074b90a","我实测了前端日期的 5 个坑：差 8 小时只是开始","日期处理是前端少有的「每个项目都要写、每个项目都写出 bug」的领域。我把最容易踩的 5 个坑写成一个实验脚本，在 UTC+8 的 Node 里逐个跑了一遍，下面每个症状都是真实输出，可以自己复跑。先看总账： 三种写法，三种结果（Node，UTC+8 实测）： 第一行和第二行差 8 小时。不是 bug，是规范本身：ISO 格式里只有日期的字符串按 UTC…","news",[10,14,19,24],{"headline":6,"body":11,"imageUrl":12,"images":13},"日期处理是前端少有的「每个项目都要写、每个项目都写出 bug」的领域。我把最容易踩的 5 个坑写成一个实验脚本，在 UTC+8 的 Node 里逐个跑了一遍，下面每个症状都是真实输出，可以自己复跑。先看总账： 三种写法，三种结果（Node，UTC+8 实测）： 第一行和第二行差 8 小时。不是 bug，是规范本身：ISO 格式里只有日期的字符串按 UTC 解析，带时间但没带时区的按本地时间解析。写代码的人觉得「这不就是个日期」，解析器认为「这是两种不同的东西」。","\u002Fapi\u002Fmedia\u002Fposts\u002F5-8-6074b90a\u002F0.webp",{"local":12},{"headline":15,"body":16,"imageUrl":17,"images":18},"被坑的典型场景：表单里选了活动开始日 2026-03-15，前端 new Date(dateStr) 一转再传给后端，凭空多出 8 小时；或者接口返回…","被坑的典型场景：表单里选了活动开始日 2026-03-15，前端 new Date(dateStr) 一转再传给后端，凭空多出 8 小时；或者接口返回 2026-03-15T00:00:00Z，前端 .getDate() 直接取，东八区用户看是 15 号，换成负时区的海外用户就成 14 号了。 防御：别让「纯日期」过 Date。字符串直接存、直接传；非要算，就显式补时区（'2026-03-15T00:00:00+08:00'），或者等文末的 Temporal。 坑 2：toISOString() 让日期变「昨天」 toISOString() 永远输出 UTC。本地 0:30（东八区）在 UTC 是前一天 16:30，slice 一刀下去，日期倒退一天。 本地开发在 UTC+8，CI 和容器默认 UTC，海外用户在负时区，同一段代码三个结果。「我本地好好的」这句话在日期 bug 里根本不成立。 防御：给人看的日期别用 toISOString。用 Intl.DateTimeFormat，时区可以显式传： toISOString 留给接口和日志这种给机器看的场景。 坑 3：空格写法，Safari 直接 Invalid Date 坑 1 的第三种写法（空格分隔）在规范里根本不存在。ES 只定义了 T 分隔，空格属于实现自定义行为，于是 V8 帮你兜了，Safari 不兜：","\u002Fapi\u002Fmedia\u002Fposts\u002F5-8-6074b90a\u002F1.webp",{"local":17},{"headline":20,"body":21,"imageUrl":22,"images":23},"这个坑阴在测试不出来：开发机 Chrome 全绿，安卓机也绿，直到有人在 iPhone 上打开页面看到 NaN。Stack Overflow 上","这个坑阴在测试不出来：开发机 Chrome 全绿，安卓机也绿，直到有人在 iPhone 上打开页面看到 NaN。Stack Overflow 上 2010 年就有人问，社区修法一水儿的 replace(' ', 'T') 或 replace(\u002F-\u002Fg, '\u002F')： 更稳的做法是别喂字符串，字段拆开传：new Date(2026, 2, 15, 0, 0)。 坑 4：1 月 31 日加一个月，跳到 3 月 3 日 「下个月同一号」这种需求（续费提醒、账单日），setMonth 会直接溢出： 月份长度不一致，setMonth 只改月份数字，2 月装不下 31 日就往前进位。构造函数也一样沉默：不报错、不警告，日期悄悄变成下个月。 Java 默认给毫秒，Python 和 Unix 惯例是秒，接口文档不写清楚就各猜各的。现在的秒级时间戳是 10 位、毫秒级是 13 位，量级差 1000 倍。防御一行： 这条分界线（1e12，也就是 2001 年 9 月）离现在的两种时间戳都很远，够用到下个世纪。 最后三条是实验里顺手验证的：new Date(2026, 2, 1) 跑出来是 3 月 1 日；美国东部 2026-03-08（夏令时切换日）那天的两个本地零点间隔实测只有 23 小时，凌晨 1:30 加一小时直接落在 3:30。","\u002Fapi\u002Fmedia\u002Fposts\u002F5-8-6074b90a\u002F2.webp",{"local":22},{"headline":25,"body":26,"imageUrl":27,"images":28},"回头看这 5 个坑的公共根源：Date 把「日期」和「时刻」混在一个类型里，字符串解析规则又叠了一层历史包袱。Temporal 把两件事拆开了，Node 26…","回头看这 5 个坑的公共根源：Date 把「日期」和「时刻」混在一个类型里，字符串解析规则又叠了一层历史包袱。Temporal 把两件事拆开了，Node 26 起默认启用（我这台 Node 24 要加 --harmony-temporal 才能用，刚试过）： 纯日期用 PlainDate，时刻用 ZonedDateTime，坑 1 和坑 2 在类型层面就没了。Node 工具链现在就能试，浏览器端全面铺开之前，上面的防御写法还得再扛一阵。 这 5 个坑按「出现频率 × 隐蔽程度」挑的，实验脚本 40 行，换个 TZ 环境变量就能复跑。你踩过的是第几个、在什么场景发现的，评论区报个数。第 6 条留给你们补充。","\u002Fapi\u002Fmedia\u002Fposts\u002F5-8-6074b90a\u002F3.webp",{"local":27},[],[],{"name":32,"url":33},"Juejin","https:\u002F\u002Fjuejin.cn\u002Fpost\u002F7690463587244458035","zh",{"handle":36,"displayName":37},"spots","Spots",null,{"views":40,"likes":41,"saves":41,"shares":41,"completions":41,"opens":41,"skips":41,"depthSum":41},4,0,"2026-09-29T19:08:26.674Z","local"]