[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fyZkbl_CLDv5qmMBPUvdzy6pd9YMy5g_0dPwn38ekAKw":3},{"item":4,"related":48},{"id":5,"type":6,"title":7,"slug":8,"summary":9,"body":10,"coverUrl":11,"productScreenshots":12,"productLinks":13,"authorName":14,"authorUrl":15,"authorSubject":16,"category":17,"tags":22,"sourceLabel":39,"sourceName":40,"sourceUrl":40,"status":41,"seoTitle":40,"seoDescription":40,"canonicalUrl":40,"isFeatured":42,"sno":43,"sortOrder":44,"publishedAt":45,"updatedAt":46,"createdAt":47},"8e1c755d-5417-4978-a9a4-d4c3e44af06e","article","Temporal：为什么程序员最怕 2 月 29 日和夏令时","javascript-temporal-date-time-explained","日期、时间点和时区不是同一种东西。本文以会议、生日和日志三个场景拆解 JavaScript Date 的边界问题，讲清 Temporal 的 Instant、PlainDate、ZonedDateTime 等类型，以及如何避免夏令时和跨时区计算陷阱。","有些 Bug 看起来像玄学：同一个会议在不同人的日历里差了一个小时，月底订阅在某些时区提前一天扣款，出生日期从数据库取出来后变成了前一天。它们通常不是“时间不听话”，而是程序把三种不同的东西混在了一起：绝对发生的时刻、某个地区的墙上时间，以及日历上的日期。\n\n![世界时区分布图](https:\u002F\u002Fcommons.wikimedia.org\u002Fwiki\u002FSpecial:FilePath\u002FWorld%20Time%20Zones%20Map.svg)\n\nJavaScript 过去主要靠一个 `Date` 对象处理这些问题。它能工作，但 API 既承载时间点，又承载本地日期和时区转换，很多操作还会改变原对象。Temporal 的目标，就是把这些概念拆成一组有明确含义、不可变的类型。TC39 的规范草案列出了时区、夏令时安全运算、日期\u002F时间分离和持续时间等核心能力。[Temporal 规范](https:\u002F\u002Ftc39.es\u002Fproposal-temporal\u002F)\n\n## 先别急着记 API：时间其实有三种语义\n\n### 1. 绝对时刻：全世界只有一个答案\n\n“服务器在 2026 年 8 月 6 日 02:00:00 收到请求”是一个绝对时刻。它通常用 UTC 或 Unix 时间戳表示。无论用户在上海、纽约还是伦敦，这个事件只发生过一次。\n\n适合用绝对时刻的场景包括：日志、支付完成时间、消息发送时间、文件创建时间。它们的重点是“什么时候发生”，而不是“当地钟表显示什么”。Temporal 用 `Temporal.Instant` 表达这种值：它不带时区，只表示时间线上一个准确位置。\n\n### 2. 墙上时间：同一个事件在不同地方看起来不同\n\n“上海办公室每天 9:00 开会”不是一个绝对时刻。它首先是一个当地规则：在 `Asia\u002FShanghai` 这个时区，每天的墙上时间是 09:00。换算成纽约时间时，必须把时区规则、夏令时和历史变更都考虑进去。\n\n这类值适合 `Temporal.ZonedDateTime`。它把日期、时间、时区和时间线联系在一起。时区不是简单的“加八小时”，而是一套会随地区政策变化的规则数据库。IANA 的时区数据库正是很多运行时进行转换时依赖的基础。[IANA Time Zone Database](https:\u002F\u002Fwww.iana.org\u002Ftime-zones)\n\n### 3. 日历日期：生日不是一个瞬间\n\n“用户生日是 8 月 6 日”通常不应该被转换成 UTC。把它存成某个时间点后，用户在另一个时区打开页面，生日可能显示成 8 月 5 日。这是因为生日是日历概念，不是全球同步发生的事件。\n\n这类值应该使用 `Temporal.PlainDate`。类似地，“店铺每天 09:00 开门”可以用 `Temporal.PlainTime`，而“2026 年 8 月 6 日 09:00”但暂时不知道在哪个时区，可以用 `Temporal.PlainDateTime`。它们故意不替你猜时区。\n\n## Temporal 解决的不是“日期格式丑”，而是边界不清\n\n看一个常见的会议例子：\n\n```js\nconst meeting = Temporal.ZonedDateTime.from(\n  \"2026-08-06T09:00:00+08:00[Asia\u002FShanghai]\"\n)\n\nconst inNewYork = meeting.withTimeZone(\"America\u002FNew_York\")\nconsole.log(inNewYork.toString())\n```\n\n这里的含义很清楚：会议发生在上海时区的 9 点，然后把同一个瞬间显示成纽约时间。`withTimeZone` 改变的是“怎么看”，不是会议本身。\n\n如果业务说的是“从会议开始后经过两小时”，应使用时间线上的加法；如果业务说的是“下个月同一天的 9 点”，应使用日历加法。两者在夏令时切换附近可能得到不同结果，这正是很多排班 Bug 的来源。\n\n```js\nconst start = Temporal.ZonedDateTime.from(\n  \"2026-03-08T01:30:00-08:00[America\u002FLos_Angeles]\"\n)\n\nconst twoHoursLater = start.add({ hours: 2 })\nconst nextCalendarDay = start.add({ days: 1 })\n```\n\n“加两小时”强调经过了 7200 秒；“加一天”强调日历向后翻一页。遇到夏令时缺失或重复的本地时间，Temporal 还允许通过选项明确指定如何处理，而不是静默地替你做一个很难发现的决定。\n\n## 不可变性：少一个隐形副作用\n\n传统 `Date` 的很多方法会修改原对象。一个函数如果拿到 `Date` 后调用 `setHours`，调用者手里的值也可能被改变。Temporal 对象是不可变的：`add`、`with`、`withTimeZone` 都会返回新对象。这个设计让时间计算更接近普通值，更容易测试，也更适合在前端状态管理中传递。\n\n但不可变不等于“自动正确”。你仍然要在数据模型里做选择：\n\n- 支付、日志和消息事件存 `Instant`。\n- 生日、节假日和账单日存 `PlainDate`。\n- 固定地点的营业时间存 `PlainTime` 加时区规则。\n- 远程会议存带时区的 `ZonedDateTime`，展示时再转换。\n- “三天后”要先问清楚是 72 小时，还是跨过三个日历日期。\n\n```mermaid\nflowchart TD\n    A[业务出现一个时间值] --> B{它代表什么}\n    B -->|发生过的事件| C[Temporal.Instant]\n    B -->|某地钟表时间| D[ZonedDateTime]\n    B -->|日历上的日期| E[PlainDate]\n    B -->|每天的时刻| F[PlainTime]\n    C --> G[展示时再转换时区]\n    D --> G\n    E --> H[不要偷偷转换成 UTC]\n    F --> I[绑定地点后再生成瞬间]\n```\n\n## Temporal 现在适合怎么用\n\n第一步不是把项目里所有 `Date` 全部替换掉，而是盘点字段的语义。可以从新功能开始：订单事件使用 `Instant`，生日使用 `PlainDate`，会议使用 `ZonedDateTime`。老接口仍然需要和 ISO 字符串、Unix 时间戳互操作，因此要把转换集中在边界层，而不是让每个组件各自解析日期。\n\n还要特别测试三类日期：时区切换前后、月份末尾、闰年 2 月 29 日。时间处理的难点从来不是把字符串格式化得漂亮，而是让每个值从一开始就有准确的含义。\n\n## 一句话带走\n\n时间 Bug 的根源经常不是算法错，而是“生日、会议和日志”被当成了同一种数据。Temporal 的价值，是让类型先替你问一句：你说的到底是一个瞬间，还是某个地方的钟表，还是日历上的一天？\n\n## 延伸阅读\n\n- [TC39 Temporal 规范](https:\u002F\u002Ftc39.es\u002Fproposal-temporal\u002F)\n- [Temporal 文档与示例](https:\u002F\u002Ftc39.es\u002Fproposal-temporal\u002Fdocs\u002F)\n- [IANA Time Zone Database](https:\u002F\u002Fwww.iana.org\u002Ftime-zones)","\u002Fuploads\u002F2026-08-06\u002F467bd509-312c-48dc-aeb3-84e9317e647c.jpg",[],[],"Foundit","https:\u002F\u002Ffoundit.cn","f39339b1-aaa6-4e86-b0c2-a6e6a21113b5",{"id":18,"name":19,"slug":20,"description":21},"6179d3b6-dc34-4483-9ded-3cd9f1b37a47","科普","abbreviation","介绍各领域新兴概念",[23,27,31,35],{"id":24,"name":25,"slug":26},"4c2bbea6-eab7-40a8-8447-1de478ff7749","分析","analyse",{"id":28,"name":29,"slug":30},"144abe77-0dc6-4f66-a176-20bddb1c0bfa","编程","coding",{"id":32,"name":33,"slug":34},"7c76bfc2-f80f-4ee0-a95d-27bd8708b434","技术","slug",{"id":36,"name":37,"slug":38},"a202d639-99a6-488a-a712-4d4c6ffd7e15","开发","dev","资料来源",null,"published",false,64,0,"2026-08-06T00:00:00.000Z","2026-08-06T05:47:06.819Z","2026-08-06T05:38:21.657Z",[49,59,68],{"id":50,"type":6,"title":51,"slug":52,"summary":53,"coverUrl":54,"authorName":55,"sno":56,"publishedAt":57,"createdAt":58},"8b883dd6-d112-4adc-ab3c-e5fa1c71bdc1","长上下文 vs RAG：什么时候还需要检索，什么时候直接塞","long-context-vs-rag-decision","模型支持百万 token 上下文后，RAG 还有必要吗？本文用一张决策图讲清长上下文与 RAG 的成本、信噪比、实时性、可溯源差异，并给出「RAG 粗筛 + 长上下文精读」的混用思路。","https:\u002F\u002Foxqtewbrpuiouqqjrvdv.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fpublic-media\u002F2026-07-20\u002F10f52f80-db7f-4a1d-861d-37514ea09646.jpg","Foundit AI",73,"2026-07-10T00:00:00.000Z","2026-07-20T01:12:58.458Z",{"id":60,"type":6,"title":61,"slug":62,"summary":63,"coverUrl":64,"authorName":14,"sno":65,"publishedAt":66,"createdAt":67},"9ccde95c-d754-4808-91f7-488f392e3eeb","你的品牌在AI眼里到底存不存在？这套系统说了算","automated-geo-monitoring-system","靠手动抽查来验证GEO效果，本质上是在跟概率玩游戏。赢一次，不代表能一直赢。","\u002Fuploads\u002F2026-08-07\u002Fdf111c0d-f14a-4b2a-8347-141e96b71654.jpg",1,"2026-08-07T00:00:00.000Z","2026-08-07T04:31:30.843Z",{"id":69,"type":6,"title":70,"slug":71,"summary":72,"coverUrl":73,"authorName":55,"sno":74,"publishedAt":75,"createdAt":76},"544fc658-c911-4de6-93b0-d2520087119a","MCP：AI 的「USB-C」时刻","mcp-ai-usb-c-moment","以前每个 AI 应用都要为 GitHub、数据库、日历各写一套私有连接器，这是 M×N 的集成噩梦，直到 MCP 的出现","https:\u002F\u002Foxqtewbrpuiouqqjrvdv.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fpublic-media\u002F2026-07-19\u002Fc3c06d99-a0ac-40ac-a283-7e77aabb4c4c.jpg",46,"2026-07-20T00:00:00.000Z","2026-07-19T16:13:40.316Z"]