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

使用 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,Coordinated Universal Time)的标准偏移量。纽约根据夏令时使用 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());
// Output: 1742331600000

ISO 字符串末尾的 Z 表示该时间为 UTC。Date 对象存储的时间戳 1742331600000,无论用户身处哪个时区,都代表同一时刻。

当你在 Date 对象上调用 toString() 等方法时,JavaScript 会将 UTC 时间戳转换为本地时区时间进行显示。当你需要显示其他时区的时间时,这种自动转换可能会带来困扰。

通过 Intl.DateTimeFormat API 结合 timeZone 选项,可以明确指定用于格式化的时区。

使用 timeZone 选项

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

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));
// Output: "3/15/25, 3:00 PM"

该日期表示 UTC 时间晚上 8:00。纽约在标准时期间为 UTC-5,夏令时期间为 UTC-4。三月份为夏令时,因此纽约为 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)}`);
});

// Output:
// 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 会将日期格式化为协调世界时(UTC),该时间与本初子午线没有时差。

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));
// Output: "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);
// Output: "America/New_York" (or user's actual time zone)

这样可以获取用户当前时区的 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));
// Output varies based on user's time zone

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

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

您可以为多个时区格式化同一个 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)}`);
});

// Output:
// New York: March 15, 2025 at 4:00 PM
// London: March 15, 2025 at 8:00 PM
// Tokyo: March 16, 2025 at 5:00 AM

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

跨时区格式化不含时间的日期

在格式化不含时间的日期时,时区会影响显示的日历日期。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(`Los Angeles: ${formatter1.format(date)}`);
console.log(`Tokyo: ${formatter2.format(date)}`);

// Output:
// Los Angeles: March 15, 2025
// Tokyo: March 16, 2025

协调世界时(UTC)3月16日凌晨 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));
// Output: "3/16/25, 5:00:00 AM 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(`Winter: ${formatter.format(winterDate)}`);
console.log(`Summer: ${formatter.format(summerDate)}`);

// Output:
// Winter: January 15, 2025 at 3:00:00 PM EST
// Summer: July 15, 2025 at 4:00:00 PM EDT

一月份,纽约使用东部标准时间(UTC-5),显示为下午 3:00。七月份,纽约使用东部夏令时间(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(`Event time: ${eventFormatter.format(eventTime)} (Paris)`);
console.log(`Your time: ${userFormatter.format(eventTime)}`);

// Output (for a user in New York):
// Event time: Saturday, March 15, 2025 at 7:00 PM (Paris)
// Your time: Saturday, March 15, 2025 at 2:00 PM

这种模式可以让用户同时看到事件在其本地时区的发生时间,以及他们应根据自己所在位置加入的时间。

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

服务器日志和数据库记录通常使用 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(`Activity: ${formatter.format(serverTimestamp)}`);
// Output varies based on user's time zone and locale
// For en-US in New York: "Activity: 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));
// Output: "Monday, March 16, 2025 at 5:00:00 AM Japan Standard Time"

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

需要避免的做法

不要将 ESTPSTGMT 这类时区缩写作为 timeZone 选项的值。这些缩写存在歧义且并非所有系统都支持。

// Incorrect - abbreviations may not work
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'EST',  // This may throw an error
  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));
});

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

创建格式化器时需要处理选项并加载本地化数据。对于多个日期复用同一个格式化器,可以避免这些额外开销。