第57课:日期与时间——Date 对象、Intl.DateTimeFormat、时区处理、Temporal 展望
在 Web 应用中,日期和时间处理无处不在:从显示文章发布时间、计算倒计时,到处理跨时区的国际航班预订。JavaScript 提供了内置的 Date 对象来处理时间,但其 API 设计存在诸多缺陷——月份从 0 开始、时区转换困难、解析行为不一致。ES6 引入了 Intl.DateTimeFormat 进行本地化格式化,而 TC39 的 Temporal 提案则致力于提供现代化的不可变时间 API。本节课将系统讲解 Date 对象的创建、操作、格式化方法,深入探讨时区的概念与处理陷阱,并通过 Intl.DateTimeFormat 实现优雅的本地化展示,最后展望 Temporal 如何解决 Date 的长期痛点。
1. Date 对象:创建与时间戳
1.1 四种创建方式
1 | // 1. 当前时间 |
月份从 0 开始:0 代表一月,11 代表十二月。这是 JavaScript 中最臭名昭著的设计缺陷之一,源自 Java 1.0 的遗留行为。
1.2 获取时间戳
Date.now() 返回当前时间的 Unix 时间戳(毫秒),是静态方法,比 new Date().getTime() 更高效。
1 | const timestamp = Date.now(); |
1.3 Date.parse 的不可靠性
Date.parse(str) 尝试将字符串解析为时间戳。不同浏览器对非 ISO 格式的支持差异极大,可能导致不同用户看到不同结果,甚至返回 NaN。
1 | // ✅ 可靠:ISO 8601 格式 |
核心原则:始终使用 ISO 8601 格式(YYYY-MM-DDTHH:mm:ss.sssZ),或直接通过时间戳/年月日构造函数创建日期。永远不要依赖 Date.parse 对非 ISO 字符串的解析。
2. Date 实例方法:获取与设置
2.1 Getter 方法
每个日期组件都有对应的 get 方法,分为本地时间和 UTC 时间两个版本。
| 组件 | 本地时间方法 | UTC 时间方法 | 取值范围 |
|---|---|---|---|
| 年 | getFullYear() |
getUTCFullYear() |
四位数字 |
| 月 | getMonth() |
getUTCMonth() |
0–11 |
| 日 | getDate() |
getUTCDate() |
1–31 |
| 星期 | getDay() |
getUTCDay() |
0(周日)–6(周六) |
| 时 | getHours() |
getUTCHours() |
0–23 |
| 分 | getMinutes() |
getUTCMinutes() |
0–59 |
| 秒 | getSeconds() |
getUTCSeconds() |
0–59 |
| 毫秒 | getMilliseconds() |
getUTCMilliseconds() |
0–999 |
| 时间戳 | getTime() |
— | 毫秒(自 Unix Epoch) |
1 | const date = new Date('2026-04-15T10:30:00+08:00'); |
2.2 Setter 方法
每个 getter 都有对应的 setter(如 setFullYear、setUTCMonth),它们会直接修改原 Date 对象。Date 对象是可变的。
1 | const date = new Date(2026, 3, 15); |
自动进位:如果设置的值超出范围,Date 会自动调整。例如 setDate(0) 会将日期设为上个月的最后一天。
1 | const date = new Date(2026, 3, 15); |
3. 时区与 UTC 的概念
3.1 UTC 与时区偏移
- UTC(协调世界时):全球统一的时间标准,不随地理位置变化。
- 本地时间:用户设备所在时区的当前时间。
- 时区偏移:本地时间与 UTC 的差值,以分钟为单位(如 UTC+8 表示本地时间比 UTC 快 8 小时)。
1 | const now = new Date(); |
getTimezoneOffset() 返回 UTC 与本地时间的分钟差。如果本地时间比 UTC 快 8 小时,则返回 -480。注意符号:UTC = 本地时间 + offset,所以东八区(UTC+8)的 offset 为 -8 * 60 = -480。
3.2 创建时区无关的日期
当通过日期部件(new Date(year, month, ...))创建时,日期是基于本地时区的。当解析 ISO 字符串 '2026-04-15'(仅日期无时间)时,ES6 规范将此时视为 UTC 时间(而非本地时间),这与 ES5.1 不一致。
1 | // 使用本地时间 |
最佳实践:对于日期操作,始终使用 Date.UTC() 创建基于 UTC 的日期,或者使用专门的时间库(如 date-fns、luxon)。仅在与用户交互的展示层转换为本地时间。
4. Intl.DateTimeFormat:国际化格式化
ECMAScript 国际化 API(Intl)提供了对语言敏感的日期格式化能力,完全替代了 toLocaleDateString 等不一致的旧方法。
4.1 基本用法
1 | const date = new Date('2026-04-15T10:30:00Z'); |
4.2 常用选项
| 选项 | 说明 | 可能的值 |
|---|---|---|
dateStyle |
日期预定义风格 | 'full'、'long'、'medium'、'short' |
timeStyle |
时间预定义风格 | 'full'、'long'、'medium'、'short' |
year |
年份显示 | 'numeric'(2026)、'2-digit'(26) |
month |
月份显示 | 'numeric'、'2-digit'、'long'(四月)、'short'(4月)、'narrow'(4) |
day |
日期显示 | 'numeric'、'2-digit' |
hour |
小时显示 | 'numeric'、'2-digit' |
hour12 |
是否使用 12 小时制 | true(上午/下午)、false(24 小时制) |
timeZone |
指定时区 | 'Asia/Shanghai'、'America/New_York'、'UTC' 等 IANA 时区名 |
1 | // 显示其他时区的时间 |
4.3 formatToParts:获取格式化后的各个部分
对于需要自定义展示(如将年份用不同颜色渲染)的场景,可以使用 formatToParts 获取每个部分及其类型。
1 | const formatter = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'full' }); |
4.4 Intl.RelativeTimeFormat:相对时间格式化
将时间差以人类可读的“3 天前”、“5 分钟后”形式展示。
1 | const rtf = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' }); |
5. Temporal:现代化的时间处理 API
Temporal 是一个正处于 TC39 Stage 3 阶段的提案,旨在全面取代 Date 对象。它提供了不可变、明确时区、按职责分离的时间 API。Chrome 和 Firefox 已开始实验性支持。
5.1 Date 的核心痛点
- 可变性:
setHours等会修改原对象,导致不必要的副作用。 - 月份从 0 开始:直觉违反,每年都有新手掉进坑里。
- 解析不一致:字符串解析取决于浏览器实现。
- 时区混乱:
Date无法方便地在不同时区之间转换。 - 缺乏纯日期/纯时间类型:一个
Date对象总是同时包含日期和时间。
5.2 Temporal 的核心类型
| 类型 | 说明 | 示例值 |
|---|---|---|
Temporal.PlainDate |
纯日期(无时间、无时区) | 2026-04-15 |
Temporal.PlainTime |
纯时间(无日期、无时区) | 10:30:00.000 |
Temporal.PlainDateTime |
日期 + 时间(无时区) | 2026-04-15T10:30:00 |
Temporal.PlainMonthDay |
月份和日(无年份) | 04-15 |
Temporal.PlainYearMonth |
年份和月(无日) | 2026-04 |
Temporal.ZonedDateTime |
带时区的完整日期时间 | 2026-04-15T10:30:00+08:00[Asia/Shanghai] |
Temporal.Instant |
绝对时间点(类似 Unix 时间戳,纳秒精度) | 以 UTC 为基础的瞬时时间 |
Temporal.Duration |
时间间隔(可表示年、月、周、天、时、分、秒等) | P1Y2M3DT4H5M6S |
5.3 Temporal 代码示例(实验性)
1 | // 创建日期 |
当前状态:截至 2026 年,Temporal 处于 Stage 3,Chrome Canary 和 Firefox Nightly 已提供实验性实现。在生产环境中,可通过 @js-temporal/polyfill 使用。正式发布后,它将彻底改变 JavaScript 时间处理的方式。
课后练习
一、概念自测(选择题 / 填空题)
(单选)
new Date(2026, 0, 1)创建的是哪一天?
A. 2026 年 1 月 1 日
B. 2025 年 12 月 1 日
C. 2026 年 12 月 1 日
D. 2026 年 0 月 1 日(无效日期)(单选) 以下关于
Intl.DateTimeFormat的说法,正确的是?
A. 它仅在浏览器中可用,Node.js 不支持。
B. 它可以按指定时区格式化日期(通过timeZone选项)。
C. 它的输出格式与toLocaleDateString完全相同。
D. 它不支持 12 小时制格式。(填空)
Date对象的getMonth()返回值范围是______(0 代表一月)。(多选) 以下哪些是
TemporalAPI 相比Date的改进点?
A. 不可变对象(方法返回新实例)
B. 月份从 1 开始
C. 明确的纯日期和纯时间类型
D. 内置的时区支持(ZonedDateTime)
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 JavaScript 函数 formatEventDate(eventDate, locale),接收一个 ISO 日期字符串(如 '2026-12-25')和一个地区标签(如 'zh-CN'),返回一个格式化的对象,包含:
short:短格式,如'2026年12月25日'。long:长格式,如'2026年12月25日星期五'。relative:相对时间(今天/昨天/明天/3 天后/…),如果时间差在 ±7 天以内显示友好文本;否则显示long格式。dayOfYear:该日期是当年的第几天。
使用Intl.DateTimeFormat实现short和long,使用Intl.RelativeTimeFormat实现relative。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定 Intl.DateTimeFormat 和 Intl.RelativeTimeFormat 的用法、日期差值计算方式、以及函数签名与返回结构。
三、面试真题与参考答案
题目(腾讯前端面试题):
请解释 JavaScript 中
Date对象的月份为什么从 0 开始计数?这带来了哪些问题?如何安全地进行日期计算(如获取上个月的最后一天)?
参考答案:
1. 月份从 0 开始的原因:这源于 JavaScript 对 Java 1.0 的模仿。Java 的 java.util.Date 将月份设计为 0-11,以便将月份作为数组索引使用(数组索引从 0 开始)。JavaScript 的设计者 Brendan Eich 在 Netscape 2 中快速实现了这一特性,之后因兼容性无法修改。
2. 带来的问题:
- 直觉矛盾:
new Date(2026, 3, 15)代表 4 月 15 日而非 3 月 15 日。 - 转换错误:开发者经常在月份上 +1 或 -1 时出错。
- 数据库和 API 通常使用 1-12 表示月份,前后端传递月份时需要转换。
3. 安全进行日期计算:
- 使用
Date.UTC()创建 UTC 日期避免本地时区干扰。 - 利用自动进位特性:
date.setDate(0)获取上个月最后一天。 - 推荐使用第三方时间库(如
date-fns、dayjs、luxon)处理复杂日期逻辑,它们提供不可变 API 和清晰的月份定义(1-12)。 - 对于新项目,关注
Temporal提案,它将彻底解决这一问题。
课后练习答案
一、概念自测答案
A
- 解析:月份从 0 开始,
0代表 1 月,所以new Date(2026, 0, 1)是 2026 年 1 月 1 日。
- 解析:月份从 0 开始,
B
- 解析:
Intl.DateTimeFormat支持timeZone选项指定时区。Node.js 13+ 内置完整Intl支持;它比toLocaleDateString更可靠和一致;hour12选项控制 12 小时制。
- 解析:
0–11- 解析:
getMonth()返回 0(一月)到 11(十二月)。
- 解析:
A、B、C、D
- 解析:
Temporal的所有这些特性都是与Date相比的改进。不可变对象、月份从 1 开始、纯日期/时间类型分离、ZonedDateTime内置时区支持,均是其设计目标。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个 JavaScript 函数formatEventDate(eventDate, locale)。要求:
- 参数
eventDate是 ISO 日期字符串(如'2026-12-25'),locale如'zh-CN'。- 返回对象包含:
short:使用Intl.DateTimeFormat(locale, { dateStyle: 'short' })格式化。long:使用Intl.DateTimeFormat(locale, { dateStyle: 'full' })格式化。relative:计算传入日期与当前日期的差值。若差值在 ±7 天内,使用Intl.RelativeTimeFormat(locale, { numeric: 'auto' })返回类似 ‘今天’、’昨天’、’3天后’;否则返回long的值。dayOfYear:计算该日期是当年的第几天。- 使用
const声明函数,添加 JSDoc 注释。- 提供至少 3 个测试用例的调用示例(包括今天、昨天、远日期)。直接输出完整代码。”