WinddSnow

JavaScript-Date-and-Time-Intl-Temporal

字数统计: 3.6k阅读时长: 15 min
2026/07/30

第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
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 当前时间
const now = new Date();
console.log(now); // 当前本地时间

// 2. 时间戳(毫秒自 1970-01-01 00:00:00 UTC)
const fromTimestamp = new Date(1681545600000);

// 3. 日期字符串(强烈不推荐,解析行为因浏览器而异)
const fromString = new Date('2026-04-15T10:30:00');

// 4. 年月日时分秒毫秒(注意:月份从 0 开始!)
const fromParts = new Date(2026, 3, 15, 10, 30, 0, 0);
// 年 月 日 时 分 秒 毫秒
console.log(fromParts); // 2026-04-15T02:30:00.000Z(取决于本地时区)

月份从 0 开始:0 代表一月,11 代表十二月。这是 JavaScript 中最臭名昭著的设计缺陷之一,源自 Java 1.0 的遗留行为。

1.2 获取时间戳

Date.now() 返回当前时间的 Unix 时间戳(毫秒),是静态方法,比 new Date().getTime() 更高效。

1
2
3
4
5
const timestamp = Date.now();
console.log(timestamp); // 1713168000000(示例)

// 转换为秒(常见于 API 交互)
const timestampInSeconds = Math.floor(Date.now() / 1000);

1.3 Date.parse 的不可靠性

Date.parse(str) 尝试将字符串解析为时间戳。不同浏览器对非 ISO 格式的支持差异极大,可能导致不同用户看到不同结果,甚至返回 NaN。

1
2
3
4
5
6
// ✅ 可靠:ISO 8601 格式
console.log(Date.parse('2026-04-15T10:30:00Z')); // 明确

// ❌ 不可靠:非标准格式
console.log(Date.parse('04/15/2026')); // 浏览器差异:美式 vs 英式
console.log(Date.parse('2026-04-15')); // 注意:此格式被视为 UTC!

核心原则:始终使用 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
2
3
const date = new Date('2026-04-15T10:30:00+08:00');
console.log(date.getUTCHours()); // 2(UTC 时间的小时)
console.log(date.getHours()); // 10(本地时间:UTC+8)

2.2 Setter 方法

每个 getter 都有对应的 setter(如 setFullYear、setUTCMonth),它们会直接修改原 Date 对象。Date 对象是可变的。

1
2
3
const date = new Date(2026, 3, 15);
date.setFullYear(2027);
console.log(date.getFullYear()); // 2027

自动进位:如果设置的值超出范围,Date 会自动调整。例如 setDate(0) 会将日期设为上个月的最后一天。

1
2
3
const date = new Date(2026, 3, 15);
date.setDate(0);
console.log(date); // 2026-03-31 —— 上个月的最后一天

3. 时区与 UTC 的概念

3.1 UTC 与时区偏移

  • UTC(协调世界时):全球统一的时间标准,不随地理位置变化。
  • 本地时间:用户设备所在时区的当前时间。
  • 时区偏移:本地时间与 UTC 的差值,以分钟为单位(如 UTC+8 表示本地时间比 UTC 快 8 小时)。
1
2
const now = new Date();
console.log(now.getTimezoneOffset()); // 例如 -480(表示 UTC+8,偏移值为 -8 * 60)

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
2
3
4
5
6
// 使用本地时间
const localDate = new Date(2026, 3, 15); // 本地时区的 2026-04-15 00:00

// 使用 UTC 时间
const utcDate = new Date(Date.UTC(2026, 3, 15)); // UTC 的 2026-04-15 00:00
console.log(utcDate.toISOString()); // '2026-04-15T00:00:00.000Z'

最佳实践:对于日期操作,始终使用 Date.UTC() 创建基于 UTC 的日期,或者使用专门的时间库(如 date-fns、luxon)。仅在与用户交互的展示层转换为本地时间。


4. Intl.DateTimeFormat:国际化格式化

ECMAScript 国际化 API(Intl)提供了对语言敏感的日期格式化能力,完全替代了 toLocaleDateString 等不一致的旧方法。

4.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const date = new Date('2026-04-15T10:30:00Z');

// 按中文格式显示
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
});
console.log(formatterCN.format(date)); // '2026年4月15日星期三'

// 按美式英语显示
const formatterUS = new Intl.DateTimeFormat('en-US', {
dateStyle: 'full',
});
console.log(formatterUS.format(date)); // 'Wednesday, April 15, 2026'

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
2
3
4
5
6
7
// 显示其他时区的时间
const formatter = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'America/New_York',
dateStyle: 'long',
timeStyle: 'short',
});
console.log(formatter.format(new Date())); // 以纽约时区显示当前时间

4.3 formatToParts:获取格式化后的各个部分

对于需要自定义展示(如将年份用不同颜色渲染)的场景,可以使用 formatToParts 获取每个部分及其类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
const formatter = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'full' });
const parts = formatter.formatToParts(new Date());
parts.forEach(({ type, value }) => {
console.log(type, value);
});
// weekday '星期三'
// literal ','
// year '2026'
// literal '年'
// month '4'
// literal '月'
// day '15'
// literal '日'

4.4 Intl.RelativeTimeFormat:相对时间格式化

将时间差以人类可读的“3 天前”、“5 分钟后”形式展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const rtf = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' });

console.log(rtf.format(-3, 'day')); // '3天前'
console.log(rtf.format(5, 'minute')); // '5分钟后'
console.log(rtf.format(-1, 'week')); // '上周'

// 计算相对时间
function timeAgo(date) {
const now = new Date();
const diffInSeconds = (date - now) / 1000;
const units = [
{ unit: 'year', seconds: 31536000 },
{ unit: 'month', seconds: 2592000 },
{ unit: 'week', seconds: 604800 },
{ unit: 'day', seconds: 86400 },
{ unit: 'hour', seconds: 3600 },
{ unit: 'minute', seconds: 60 },
];
for (const { unit, seconds } of units) {
const value = Math.round(diffInSeconds / seconds);
if (Math.abs(value) >= 1) {
return rtf.format(value, unit);
}
}
return '刚刚';
}

const postDate = new Date(Date.now() - 2 * 86400000); // 2 天前
console.log(timeAgo(postDate)); // '2天前'

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建日期
const date = Temporal.PlainDate.from('2026-04-15');
console.log(date.toString()); // '2026-04-15'

// 日期运算(不可变,返回新对象)
const nextWeek = date.add({ days: 7 });
console.log(nextWeek.toString()); // '2026-04-22'

const daysBetween = date.until(nextWeek).days;
console.log(daysBetween); // 7

// 比较日期
const today = Temporal.Now.plainDateISO();
const isPast = Temporal.PlainDate.compare(date, today) < 0;
console.log(isPast); // true(如果今天 > 2026-04-15)

// 带时区的日期时间
const zoned = Temporal.ZonedDateTime.from('2026-04-15T10:30:00+08:00[Asia/Shanghai]');
console.log(zoned.withTimeZone('America/New_York').toString());
// 自动转换为纽约时间

当前状态:截至 2026 年,Temporal 处于 Stage 3,Chrome Canary 和 Firefox Nightly 已提供实验性实现。在生产环境中,可通过 @js-temporal/polyfill 使用。正式发布后,它将彻底改变 JavaScript 时间处理的方式。


课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) new Date(2026, 0, 1) 创建的是哪一天?
    A. 2026 年 1 月 1 日
    B. 2025 年 12 月 1 日
    C. 2026 年 12 月 1 日
    D. 2026 年 0 月 1 日(无效日期)

  2. (单选) 以下关于 Intl.DateTimeFormat 的说法,正确的是?
    A. 它仅在浏览器中可用,Node.js 不支持。
    B. 它可以按指定时区格式化日期(通过 timeZone 选项)。
    C. 它的输出格式与 toLocaleDateString 完全相同。
    D. 它不支持 12 小时制格式。

  3. (填空) Date 对象的 getMonth() 返回值范围是 ______(0 代表一月)。

  4. (多选) 以下哪些是 Temporal API 相比 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 提案,它将彻底解决这一问题。

课后练习答案

一、概念自测答案

  1. A

    • 解析:月份从 0 开始,0 代表 1 月,所以 new Date(2026, 0, 1) 是 2026 年 1 月 1 日。
  2. B

    • 解析:Intl.DateTimeFormat 支持 timeZone 选项指定时区。Node.js 13+ 内置完整 Intl 支持;它比 toLocaleDateString 更可靠和一致;hour12 选项控制 12 小时制。
  3. 0–11

    • 解析:getMonth() 返回 0(一月)到 11(十二月)。
  4. 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 个测试用例的调用示例(包括今天、昨天、远日期)。直接输出完整代码。”
CATALOG
  1. 1. 第57课:日期与时间——Date 对象、Intl.DateTimeFormat、时区处理、Temporal 展望
    1. 1.1. 1. Date 对象:创建与时间戳
      1. 1.1.1. 1.1 四种创建方式
      2. 1.1.2. 1.2 获取时间戳
      3. 1.1.3. 1.3 Date.parse 的不可靠性
    2. 1.2. 2. Date 实例方法:获取与设置
      1. 1.2.1. 2.1 Getter 方法
      2. 1.2.2. 2.2 Setter 方法
    3. 1.3. 3. 时区与 UTC 的概念
      1. 1.3.1. 3.1 UTC 与时区偏移
      2. 1.3.2. 3.2 创建时区无关的日期
    4. 1.4. 4. Intl.DateTimeFormat:国际化格式化
      1. 1.4.1. 4.1 基本用法
      2. 1.4.2. 4.2 常用选项
      3. 1.4.3. 4.3 formatToParts:获取格式化后的各个部分
      4. 1.4.4. 4.4 Intl.RelativeTimeFormat:相对时间格式化
    5. 1.5. 5. Temporal:现代化的时间处理 API
      1. 1.5.1. 5.1 Date 的核心痛点
      2. 1.5.2. 5.2 Temporal 的核心类型
      3. 1.5.3. 5.3 Temporal 代码示例(实验性)
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)