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(如 setFullYearsetUTCMonth),它们会直接修改原 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-fnsluxon)。仅在与用户交互的展示层转换为本地时间。


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 实现 shortlong,使用 Intl.RelativeTimeFormat 实现 relative

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定 Intl.DateTimeFormatIntl.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-fnsdayjsluxon)处理复杂日期逻辑,它们提供不可变 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 编程任务参考答案(提示词示例)