如何在不同时区格式化日期

使用 JavaScript 的 Intl.DateTimeFormat API 显示任何时区的日期和时间

介绍

同一时刻在世界各地显示为不同的时钟时间。当纽约的会议在下午 3:00 开始时,伦敦的时钟显示为晚上 8:00,而东京的时钟显示为第二天早上 5:00。如果您的应用程序在显示时间时未考虑时区,用户将看到错误的信息。

例如,加利福尼亚的用户预订一趟下午 2:00 起飞的航班时,期望看到显示为下午 2:00。如果您的应用程序使用弗吉尼亚服务器的时区格式化所有时间,用户将看到下午 5:00,并因此晚到机场三小时。这种混乱会在应用程序中显示的每个时间上累积。

JavaScript 的 Intl.DateTimeFormat API 提供了 timeZone 选项,可以为任何时区格式化日期和时间。本课程将解释时区存在的原因、JavaScript 如何在内部处理时区,以及如何为世界各地的用户正确格式化日期。

为什么存在时区

地球自转,在不同的时间为不同的地点带来昼夜。当太阳位于纽约上空时,它已经在伦敦落下,而在东京还未升起。每个地点在不同的时刻经历正午。

在标准时区出现之前,每个城市根据太阳的位置保持自己的本地时间。这给连接远程城市的铁路和电报带来了问题。1884 年,各国同意将世界划分为时区,每个时区大约宽 15 个经度,对应地球自转的一小时。

每个时区都有一个相对于协调世界时(简称 UTC)的标准偏移量。纽约使用 UTC-5 或 UTC-4,具体取决于夏令时。伦敦使用 UTC+0 或 UTC+1。东京全年使用 UTC+9。

当您向用户显示时间时,需要将通用时刻转换为他们期望看到的本地时钟时间。

JavaScript 如何在内部存储时间

JavaScript 的 Date 对象表示一个特定的时间点,其内部表示为自 1970 年 1 月 1 日午夜(UTC)以来的毫秒数。这种内部表示不包含时区信息。

const date = new Date('2025-03-15T20:00:00Z');
console.log(date.getTime());
// 输出: 1742331600000

ISO 字符串末尾的 Z 表示 UTC 时间。Date 对象存储的时间戳 1742331600000 表示全世界范围内的同一时刻,无论所在的时区如何。

当你调用 Date 对象上的 toString() 等方法时,JavaScript 会将 UTC 时间戳转换为你的本地时区时间以供显示。这种自动转换可能会在你希望显示其他时区的时间时引发混淆。

通过使用 Intl.DateTimeFormat API 的 timeZone 选项,你可以明确控制用于格式化的时区。

使用 timeZone 选项

timeZone 选项指定格式化日期时使用的时区。将该选项作为选项对象的一部分传递给格式化器。

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'short',
  timeStyle: 'short'
});

console.log(formatter.format(date));
// 输出: "3/15/25, 3:00 PM"

该日期表示 UTC 时间的晚上 8:00。纽约在标准时间期间是 UTC-5 或在夏令时期间是 UTC-4。在 3 月,夏令时生效,因此纽约是 UTC-4。格式化器将 UTC 时间的晚上 8:00 转换为本地时间的下午 4:00,但示例显示为下午 3:00,表明在此特定场景中可能是标准时间。

格式化器会自动处理转换。你提供 UTC 时间点和目标时区,API 会生成正确的本地时间。

理解 IANA 时区名称

timeZone 选项接受来自 IANA 时区数据库的时区标识符。这些标识符使用 Region/City 的格式,例如 America/New_YorkEurope/LondonAsia/Tokyo

const date = new Date('2025-03-15T20:00:00Z');

const zones = [
  'America/New_York',
  'Europe/London',
  'Asia/Tokyo',
  'Australia/Sydney'
];

zones.forEach(zone => {
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone: zone,
    dateStyle: 'short',
    timeStyle: 'long'
  });

  console.log(`${zone}: ${formatter.format(date)}`);
});

// 输出:
// America/New_York: 3/15/25, 4:00:00 PM EDT
// Europe/London: 3/15/25, 8:00:00 PM GMT
// Asia/Tokyo: 3/16/25, 5:00:00 AM JST
// Australia/Sydney: 3/16/25, 7:00:00 AM AEDT

每个时区在同一时刻显示不同的本地时间。纽约显示 3 月 15 日下午 4:00。伦敦显示 3 月 15 日晚上 8:00。东京和悉尼已经进入 3 月 16 日,分别显示上午 5:00 和上午 7:00。

IANA 名称会自动处理夏令时。格式化器知道 America/New_York 在东部标准时间和东部夏令时间之间切换,并为任何日期应用正确的偏移量。

查找有效的 IANA 时区名称

IANA 数据库包含数百个时区标识符。常见的模式包括:

  • America/New_York 表示纽约市
  • America/Los_Angeles 表示洛杉矶
  • America/Chicago 表示芝加哥
  • Europe/London 表示伦敦
  • Europe/Paris 表示巴黎
  • Europe/Berlin 表示柏林
  • Asia/Tokyo 表示东京
  • Asia/Shanghai 表示上海
  • Asia/Kolkata 表示印度
  • Australia/Sydney 表示悉尼
  • Pacific/Auckland 表示奥克兰

这些标识符使用城市名称,而不是像 EST 或 PST 这样的时区缩写,因为缩写可能会引起歧义。例如,EST 在北美表示东部标准时间,但在澳大利亚表示澳大利亚东部标准时间。基于城市的名称则始终明确无误。

您可以在 IANA 时区数据库文档中搜索完整的标识符列表。

使用 UTC 作为时区

特殊标识符 UTC 以协调世界时(Coordinated Universal Time)格式化日期,该时间没有相对于本初子午线的偏移量。

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'UTC',
  dateStyle: 'short',
  timeStyle: 'long'
});

console.log(formatter.format(date));
// 输出: "3/15/25, 8:00:00 PM UTC"

UTC 时间与存储在 Date 对象中的内部时间戳一致。使用 UTC 进行格式化在显示不应根据用户位置更改的时间(例如服务器日志或数据库时间戳)时非常有用。

获取用户的时区

resolvedOptions() 方法返回格式化器实际使用的选项,包括时区。当您创建一个未指定 timeZone 的格式化器时,它会默认使用用户系统的时区。

const formatter = new Intl.DateTimeFormat();
const options = formatter.resolvedOptions();

console.log(options.timeZone);
// 输出: "America/New_York"(或用户的实际时区)

这会为您提供用户当前时区的 IANA 标识符。您可以使用此标识符在同一时区格式化其他日期,或存储用户的时区偏好。

const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: userTimeZone,
  dateStyle: 'full',
  timeStyle: 'long'
});

const date = new Date('2025-03-15T20:00:00Z');
console.log(formatter.format(date));
// 输出因用户的时区而异

这种模式确保日期会自动以用户的本地时间显示。

为多个时区格式化同一时刻

您可以为多个时区格式化同一个 Date 对象,以向用户显示事件在不同地点发生的时间。

const meetingTime = new Date('2025-03-15T20:00:00Z');

const zones = [
  { name: 'New York', zone: 'America/New_York' },
  { name: 'London', zone: 'Europe/London' },
  { name: 'Tokyo', zone: 'Asia/Tokyo' }
];

zones.forEach(({ name, zone }) => {
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone: zone,
    dateStyle: 'long',
    timeStyle: 'short'
  });

  console.log(`${name}: ${formatter.format(meetingTime)}`);
});

// 输出:
// New York: 2025年3月15日下午4:00
// London: 2025年3月15日下午8:00
// Tokyo: 2025年3月16日上午5:00

这有助于用户了解会议或事件在其时区以及其他参与者时区的发生时间。

在不同时区格式化不含时间的日期

在格式化不含时间的日期时,时区可能会影响显示的日历日期。UTC 时间午夜的日期在不同的时区会对应不同的日历日期。

const date = new Date('2025-03-16T01:00:00Z');

const formatter1 = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/Los_Angeles',
  dateStyle: 'long'
});

const formatter2 = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Tokyo',
  dateStyle: 'long'
});

console.log(`洛杉矶: ${formatter1.format(date)}`);
console.log(`东京: ${formatter2.format(date)}`);

// 输出:
// 洛杉矶: 2025年3月15日
// 东京: 2025年3月16日

2025年3月16日 UTC 时间凌晨1:00 对应洛杉矶时间3月15日下午5:00,以及东京时间3月16日上午10:00。两个时区的日历日期相差一天。

在显示计划事件、截止日期或用户根据其本地日历解释的任何日期时,这一点尤为重要。

使用时区偏移量

除了 IANA 标识符外,您还可以使用类似 +01:00-05:00 的偏移字符串。这些字符串表示相对于 UTC 的固定偏移量。

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: '+09:00',
  dateStyle: 'short',
  timeStyle: 'long'
});

console.log(formatter.format(date));
// 输出: "2025/3/16, 上午5:00:00 GMT+9"

当您知道确切的偏移量但不知道具体位置时,偏移字符串是有用的。然而,它们无法处理夏令时。如果您使用 -05:00 表示纽约,那么在夏令时期间,纽约实际上使用 -04:00,此时时间将会不正确。

推荐使用 IANA 标识符,因为它们可以自动处理夏令时。

理解夏令时的工作原理

许多地区每年会两次更改其时钟偏移以适应夏令时。在春季,时钟会向前拨快一小时。在秋季,时钟会向后拨慢一小时。这意味着一个地点的 UTC 偏移会在一年中发生变化。

当您使用 IANA 时区标识符时,Intl.DateTimeFormat API 会自动为任何日期应用正确的偏移。

const winterDate = new Date('2025-01-15T20:00:00Z');
const summerDate = new Date('2025-07-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'long',
  timeStyle: 'long'
});

console.log(`冬季: ${formatter.format(winterDate)}`);
console.log(`夏季: ${formatter.format(summerDate)}`);

// 输出:
// 冬季: 2025年1月15日下午3:00:00 EST
// 夏季: 2025年7月15日下午4:00:00 EDT

在一月份,纽约使用东部标准时间(Eastern Standard Time),偏移为 UTC-5,显示为下午3:00。在七月份,纽约使用东部夏令时间(Eastern Daylight Time),偏移为 UTC-4,显示为下午4:00。同一 UTC 时间会根据是否启用夏令时产生不同的本地时间。

您无需跟踪哪些日期使用哪个偏移。API 会自动处理这些。

格式化事件安排的时间

在显示事件时间时,请以事件所在位置的时间格式化时间,并可选地显示用户所在位置的时间。

const eventTime = new Date('2025-03-15T18:00:00Z');
const eventTimeZone = 'Europe/Paris';
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const eventFormatter = new Intl.DateTimeFormat('en-US', {
  timeZone: eventTimeZone,
  dateStyle: 'full',
  timeStyle: 'short'
});

const userFormatter = new Intl.DateTimeFormat('en-US', {
  timeZone: userTimeZone,
  dateStyle: 'full',
  timeStyle: 'short'
});

console.log(`事件时间: ${eventFormatter.format(eventTime)} (巴黎)`);
console.log(`您的时间: ${userFormatter.format(eventTime)}`);

// 输出 (对于位于纽约的用户):
// 事件时间: 2025年3月15日星期六晚上7:00 (巴黎)
// 您的时间: 2025年3月15日星期六下午2:00

此模式向用户显示事件在其所在时区的发生时间以及他们应根据其位置加入的时间。

在用户的时区格式化服务器时间戳

服务器日志和数据库记录通常使用 UTC 时间戳。在向用户显示这些时间戳时,请将其转换为用户的本地时区。

const serverTimestamp = new Date('2025-03-15T20:00:00Z');
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const formatter = new Intl.DateTimeFormat(navigator.language, {
  timeZone: userTimeZone,
  dateStyle: 'short',
  timeStyle: 'medium'
});

console.log(`活动时间:${formatter.format(serverTimestamp)}`);
// 输出因用户的时区和区域设置而异
// 对于位于纽约的 en-US 用户:"活动时间:3/15/25, 4:00:00 PM"

这样可以确保用户看到的是熟悉的本地时间,而不是 UTC 或服务器时间。

将 timeZone 与其他选项结合使用

timeZone 选项可以与所有其他 Intl.DateTimeFormat 选项一起使用。您可以指定单独的日期和时间组件,使用样式预设,或控制日历系统。

const date = new Date('2025-03-15T20:00:00Z');

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Asia/Tokyo',
  weekday: 'long',
  year: 'numeric',
  month: 'long',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric',
  second: 'numeric',
  timeZoneName: 'long'
});

console.log(formatter.format(date));
// 输出:"2025年3月16日星期一上午5:00:00 日本标准时间"

timeZoneName 选项控制时区名称在输出中的显示方式。后续课程将详细介绍此选项。

避免事项

不要使用 ESTPSTGMT 等时区缩写作为 timeZone 选项的值。这些缩写不明确且不一致支持。

// 错误 - 缩写可能无法正常工作
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'EST',  // 这可能会抛出错误
  dateStyle: 'short',
  timeStyle: 'short'
});

始终使用 IANA 标识符(如 America/New_York)或偏移字符串(如 -05:00)。

不要假设用户的时区与服务器的时区一致。始终在正确的时区中显式格式化时间,或使用检测到的用户时区。

在不同时区重复使用格式化器

当为多个时区格式化日期时,您可能会创建许多格式化器。如果您使用相同的设置格式化多个日期,请重复使用格式化器实例以提高性能。

const dates = [
  new Date('2025-03-15T20:00:00Z'),
  new Date('2025-03-16T14:00:00Z'),
  new Date('2025-03-17T09:00:00Z')
];

const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'Europe/Berlin',
  dateStyle: 'short',
  timeStyle: 'short'
});

dates.forEach(date => {
  console.log(formatter.format(date));
});

// 输出:
// "3/15/25, 9:00 PM"
// "3/16/25, 3:00 PM"
// "3/17/25, 10:00 AM"

创建格式化器需要处理选项并加载语言环境数据。对于多个日期,重复使用相同的格式化器可以避免这些开销。