KoderKoder.ai
价格企业教育投资人
登录开始使用

产品

价格企业投资人

资源

联系我们支持教育博客

法律信息

隐私政策使用条款安全可接受使用政策举报滥用

社交

LinkedInTwitter
Koder.ai
语言

© 2026 Koder.ai 保留所有权利。

首页›博客›JavaScript 时间格式化与转换:常见陷阱
2025年9月06日·2 分钟

JavaScript 时间格式化与转换:常见陷阱

学习如何在 JavaScript 中可靠地格式化和转换时间,避免意外:时间戳、ISO 字符串、时区、夏令时、解析规则与稳健模式。

JavaScript 时间格式化与转换:常见陷阱

JavaScript 时间通常出问题的地方

JavaScript 的时间错误很少看起来像“时钟坏了”。它们通常以令人困惑的小偏移出现:某个日期在你的笔记本上是正确的,但在同事的机器上是错误的;API 的响应看起来没问题,直到在不同的时区渲染;或者报告在季节性时间变更附近“差一”的情况。

最常见的症状

你通常会注意到以下一项(或多项):

  • 相差一小时:尤其是在夏令时(DST)期间,或当值被无意间在本地时间与 UTC 之间转换时。
  • 相差一天:只有日期的值(例如 “2025-12-23”)会根据时区显示为前一天或后一天。
  • 时区错误:时间看起来正确,但偏移(例如 +02:00)与预期不同。
  • 格式不一致:在 Chrome 上“工作”,但在 Safari 上看起来不同,或者服务器和浏览器在解析字符串时意见不一。

为什么会发生:"时间" 可以有不同含义

一个很大的痛点是单词 时间 可以指不同的概念:

  • 一个瞬时点(instant):全球的具体时刻(例如 “2025-12-23T10:00:00Z”)。这通常是用于日志、事件和 API 存储的正确选择。
  • 日历日期(calendar date):没有时区的日历日(例如生日、发票日期)。将其当作瞬时点处理可能会跨天边界移动。
  • 本地时钟时间(wall-clock time):例如“柏林时间上午 9:00”,依赖于时区规则和夏令时变化。

JavaScript 内置的 Date 试图覆盖这些用例,但它主要表示一个瞬时点,同时又经常把你引向本地显示,这就使得无意转换变得很容易。

本文关注的内容

本指南着眼于实用性:如何在浏览器和服务器间得到可预测的转换,如何选择更安全的格式(比如 ISO 8601),以及如何发现经典陷阱(秒与毫秒、UTC 与本地、解析差异)。目标不是更多理论,而是更少的“为什么它会偏移?”的意外。

时间数据类型:时间戳、Date 与字符串

JavaScript 的时间错误常常始于混合看似可互换但实际上并非如此的表示法。

三种你最常见的表示法

1) 纪元毫秒(数字)

像 1735689600000 这样的纯数字通常表示“自 1970-01-01T00:00:00Z 起的毫秒数”。它表示一个瞬时点,没有格式或时区附带。

2) Date 对象(瞬时点的包装)

Date 存储与时间戳相同类型的瞬时点。令人困惑的地方在于:当你打印一个 Date 时,JavaScript 会使用运行环境的本地规则来格式化,除非你另外指定。

3) 格式化字符串(人类可读)

像 "2025-01-01"、"01/01/2025 10:00" 或 "2025-01-01T00:00:00Z" 这样的字符串并不是同一种东西。有些是明确的(带 Z 的 ISO 8601),有些依赖区域设置,还有些根本不包含时区信息。

“瞬时点” 与 “面向人类的显示时间”

  • 瞬时点:指“这个确切的全球时刻”(最好以纪元毫秒或以 UTC 的 ISO 字符串存储)。
  • 面向人类的显示时间:指“用户应该看到的时间”(依赖于语言环境与时区)。

同一个瞬时点在不同时区可以有不同的显示:

const instant = new Date("2025-01-01T00:00:00Z");

instant.toLocaleString("en-US", { timeZone: "UTC" });
// "1/1/2025, 12:00:00 AM"

instant.toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
// "12/31/2024, 4:00:00 PM" (前一天)

选择一个“单一事实来源(source of truth)”

在你的应用和 API 中选择一个统一的内部表示(通常是纪元毫秒或UTC ISO 8601),并坚持使用。仅在边界处(输入解析与 UI 显示)进行到/从 Date 与格式化字符串的转换。

时间戳:秒与毫秒(容易混淆)

“时间戳”通常指纪元时间(也称 Unix 时间):自 1970-01-01 00:00:00 UTC 起的时间计数。难点在于不同系统使用不同的单位。

JavaScript 的 Date 是大多数混淆的来源,因为它使用毫秒。许多 API、数据库与日志使用秒。

经验法则

  • Unix 时间戳(秒): 1704067200
  • JavaScript 时间戳(毫秒): 1704067200000

同一时刻,但毫秒版本多了三个零。

安全转换(秒 ↔ 毫秒)

使用显式的乘除法以确保单位明确:

// seconds -> Date
const seconds = 1704067200;
const d1 = new Date(seconds * 1000);

// milliseconds -> Date
const ms = 1704067200000;
const d2 = new Date(ms);

// Date -> seconds
const secondsOut = Math.floor(d2.getTime() / 1000);

// Date -> milliseconds
const msOut = d2.getTime();

经典错误:向 Date() 传入秒

这看起来合理,但当 ts 是以秒为单位时就是错的:

const ts = 1704067200;      // seconds
const d = new Date(ts);     // WRONG: treated as milliseconds

结果会是 1970 年 的某个日期,因为 1,704,067,200 毫秒大约仅距纪元约 19 天。

快速校验与调试检查

当你不确定拥有的是哪个单位时,加入快速的保护措施:

function asDateFromUnknownEpoch(x) {
  // crude heuristic: seconds are ~1e9-1e10, milliseconds are ~1e12-1e13
  if (x < 1e11) return new Date(x * 1000); // assume seconds
  return new Date(x);                      // assume milliseconds
}

const input = Number(valueFromApi);
console.log({ input, digits: String(Math.trunc(input)).length });
console.log('as ISO:', asDateFromUnknownEpoch(input).toISOString());

如果“位数”计数约为 10,通常是秒;如果约为 13,通常是毫秒。调试时还要打印 toISOString():它是明确的,可以帮助你立即发现单位错误。

本地时间与 UTC:为什么你的输出会偏移

JavaScript 的 Date 可能令人困惑,因为它存储一个时间瞬时点,但可以以不同时区呈现该瞬时点。

内部上,Date 本质上是“自 Unix 纪元(1970-01-01T00:00:00Z)以来的毫秒数”。这个数字表示 UTC 时刻。当你要求 JavaScript 将该时刻格式化为本地时间(基于计算机/服务器设置)与UTC时,就会发生“偏移”。

本地 getter 与 UTC getter

许多 Date API 都有本地与 UTC 的变体。它们对于同一瞬时点会返回不同的数值:

const d = new Date('2025-01-01T00:30:00Z');

d.getHours();      // hour in *local* time zone
d.getUTCHours();   // hour in UTC

d.toString();      // local time string
d.toISOString();   // UTC (always ends with Z)

如果你的机器在纽约(UTC-5),该 UTC 时间在本地可能显示为前一天的 “19:30”。而在设置为 UTC 的服务器上,它会显示为 “00:30”。相同的瞬时点,不同的显示。

为什么日志看起来“错了”

日志通常使用 Date#toString() 或隐式插入 Date,这会使用环境的本地时区。因此相同的代码可能在你的笔记本、CI 与生产环境打印不同的时间戳。

实用建议

以 UTC 存储和传输时间(例如纪元毫秒或带 Z 的 ISO 8601)。仅在显示时转换为用户本地:

  • 对于 API:优先使用 toISOString() 或传递纪元毫秒
  • 对于 UI:使用 Intl.DateTimeFormat 在用户时区中格式化

如果你在快速构建应用(例如在 Koder.ai 的快速生成工作流中),建议在生成的 API 合同中早早固定这些约定:字段命名清晰(createdAtMs, createdAtIso),并让服务器(Go + PostgreSQL)与客户端(React)对每个字段的含义保持一致。

ISO 8601 字符串:API 的最安全格式

如果需要在浏览器、服务器与数据库之间发送日期/时间,ISO 8601 字符串是最稳妥的默认选择。它们明确、广泛支持,并且(最重要的)携带时区信息。

使用显式的 UTC 或显式偏移

两种好的交换格式:

  • UTC 时间(当你不关心本地时区时推荐): 2025-03-04T12:30:00Z
  • 带偏移的本地时间(当“本地时钟时间”很重要时推荐): 2025-03-04T12:30:00+02:00

'Z' 代表什么?

Z 代表 Zulu 时间,也就是 UTC。所以 2025-03-04T12:30:00Z 是“UTC 的 12:30”。

何时偏移如 +02:00 很重要?

当一个事件与本地时区上下文相关(预约、预订、商店营业时间)时,偏移非常关键。2025-03-04T12:30:00+02:00 描述的是比 UTC 快两个小时的时刻,它与 2025-03-04T12:30:00Z 不是同一个瞬时点。

避免歧义的日期字符串

像 03/04/2025 这样的字符串是陷阱:是 3 月 4 日还是 4 月 3 日?不同的用户和环境会有不同解释。请偏好 2025-03-04(ISO 日期)或完整的 ISO 日期时间。

安全的往返(字符串 → Date → 字符串)

const iso = "2025-03-04T12:30:00Z";
const d = new Date(iso);
const back = d.toISOString();

console.log(iso);  // 2025-03-04T12:30:00Z
console.log(back); // 2025-03-04T12:30:00.000Z

这种“往返”行为正是 API 所需要的:一致、可预测且考虑时区。

解析陷阱:Date.parse 与浏览器差异

修复日期解析错误
描述输入字符串,即可获得更安全的解析代码,避免 Date.parse 带来的意外。
开始构建

Date.parse() 看起来很方便:给它一个字符串,返回时间戳。问题在于,对于任何非明确 ISO 8601 的内容,解析可能依赖浏览器的启发式规则。这些启发式在不同引擎与版本间有所差异,意味着相同的输入可能在不同环境下解析结果不同(或根本无法解析)。

为什么 Date.parse() 会有差异

JavaScript 仅对 ISO 8601 风格的字符串使解析成为标准化行为(即便如此,诸如时区这种细节也会影响结果)。对于“友好格式”——如 "03/04/2025"、"March 4, 2025" 或 "2025-3-4" ——浏览器可能会:

  • 根据区域设置假定月/日顺序或日/月顺序
  • 将缺失时区的字符串作为本地时间处理,某些引擎可能会拒绝
  • 对轻微格式错误容忍处理……直到有一天不再容忍

如果你无法预测字符串的精确形状,就无法预测解析结果。

一个常见的意外:YYYY-MM-DD

一个常见陷阱是纯日期形式 "YYYY-MM-DD"(例如 "2025-01-15")。许多开发者期望它被解释为本地午夜。实际上,一些环境会将此形式视为UTC 午夜。

这个差异很重要:UTC 午夜转换为本地时间时,可能会变成(负时区)前一天,或者小时数出现意外偏移。这是造成“为什么我的日期相差一天?”错误的常见来源。

解析清单:用户输入 vs 服务端输入

对于 服务端/API 输入:

  • 偏好带明确时区的完整 ISO 8601,例如 2025-01-15T13:45:00Z 或 2025-01-15T13:45:00+02:00。
  • 将仅日期值视为数据,而不是时间点。如果它是生日或到期日,就把它保留为纯字符串("YYYY-MM-DD"),不要转换为 Date,除非你也定义了预期的时区。

对于 用户输入:

  • 不要接受像 03/04/2025 这样的歧义格式,除非你的 UI 强制限定其含义。
  • 偏好受控输入(日期选择器),使其输出已知格式。
  • 如果必须解析自由文本,请预先定义并强制执行可接受格式。

使用显式规则(而非启发式)

不要依赖 Date.parse() 去“猜测”,选择以下模式之一:

  • 只接受来自服务器的 ISO 8601;拒绝其他格式。
  • 手动解析已知格式(拆分字符串并使用 new Date(year, monthIndex, day) 来创建本地日期)。
  • 存储并传输时间戳(纪元毫秒)用于精确的瞬时点,之后再格式化用于显示。

当时间数据很关键时,“在我的机器上解析没问题”不够——要使解析规则明确且一致。

使用 Intl.DateTimeFormat 为人类格式化

如果你的目标是“以人们期望的方式显示日期/时间”,JavaScript 中最好的工具是 Intl.DateTimeFormat。它使用用户的区域规则(顺序、分隔符、月份名称),避免手工拼接字符串(如 month + '/' + day)的脆弱做法。

它优于手工构建字符串的原因

手工格式化常常硬编码为美式输出,忘记前导零,或在 24/12 小时制上产生混淆。Intl.DateTimeFormat 还使你明确知道要以哪个时区显示——当数据以 UTC 存储但 UI 需反映用户本地时间时,这一点至关重要。

常用且实用的选项

对于“好看地格式化”,dateStyle 与 timeStyle 是最简单的:

const d = new Date('2025-01-05T16:30:00Z');

// User’s locale + user’s local time zone
console.log(new Intl.DateTimeFormat(undefined, {
  dateStyle: 'medium',
  timeStyle: 'short'
}).format(d));

// Force a specific time zone (great for event times)
console.log(new Intl.DateTimeFormat('en-GB', {
  dateStyle: 'full',
  timeStyle: 'short',
  timeZone: 'UTC'
}).format(d));

如果你需要一致的小时制(例如用户可以设置 12/24 小时制),使用 hour12:

console.log(new Intl.DateTimeFormat('en-US', {
  hour: 'numeric',
  minute: '2-digit',
  hour12: true
}).format(d));

实用 UI 模式

为 UI 中每种“时间类型”(消息时间、日志条目、事件开始)选择一个格式化函数,并让 timeZone 的决定变得明确:

  • 对于“对我来说何时发生”的情形,使用本地时间。
  • 对于审计日志、服务器事件或跨团队协调,使用固定时区(通常是 UTC)。

这样你就可以得到一致、符合区域习惯的输出,而无需维护脆弱的自定义格式字符串集合。

夏令时(DST):隐蔽的一小时偏差错误

在实时应用中验证转换
生成、部署并托管时区查看器,端到端验证格式化。
部署应用

夏令时是时区在特定日期改变其 UTC 偏移(通常为一小时)的一种情况。棘手之处在于:DST 不仅改变偏移——它还改变某些本地时间的存在性。

缺失与重复的本地时间

当时钟向前跳时,一段本地时间并不存在。例如在许多地区,时钟会从 01:59 跳到 03:00,因此02:30 本地时间是“缺失的”。

当时钟向后拨时,一段本地时间会发生两次。例如 01:30 可能在切换前与切换后各出现一次,意味着相同的本地时间可以对应两个不同的瞬时点。

加 24 小时 vs “明天同一本地时间”

在 DST 边界,这两者并不等价:

  • 加 24 小时:表示“精确的 24 * 60 * 60 秒后”
  • 明天同一本地时间:表示“在该时区下明天的 9:00”

如果今晚开始 DST,"明天的 9:00" 可能只比现在晚 23 小时。如果今晚结束 DST,可能晚 25 小时。

// Scenario: schedule “same local time tomorrow”
const d = new Date(2025, 2, 8, 9, 0); // Mar 8, 9:00 local

const plus24h = new Date(d.getTime() + 24 * 60 * 60 * 1000);
const nextDaySameLocal = new Date(d);
nextDaySameLocal.setDate(d.getDate() + 1);

// Around DST, plus24h and nextDaySameLocal can differ by 1 hour.

为什么 setHours 会让你吃惊

如果你在“向前跳”那天对某个日期执行 date.setHours(2, 30, 0, 0),JavaScript 可能会将其规范化为另一个有效时间(通常为 03:30),因为 02:30 在本地时间中并不存在。

更安全的方法

  • 对于经过时间(durations)使用 UTC 算术:使用纪元毫秒和 UTC 方法。
  • 对于本地日程安排,明确说明意图:“明天同一本地时间”应使用日历操作(setDate),而不是简单相加毫秒。
  • 通过 API 交换时间时,优先使用带偏移或 Z 的 ISO 8601,使瞬时点明确无疑。

持续时长与日期:不要用 Date 去表示计时器

一个常见错误是使用 Date 来表示并非日历时刻的东西。

时间戳(timestamp) 回答“什么时候发生的?”(一个特定的瞬时点,如 2025-12-23T10:00:00Z)。持续时长(duration) 回答“持续多久?”(比如“3 分 12 秒”)。这两个概念不同,混用会造成混乱的运算和意外的时区 / DST 效果。

为什么 Date 不是持续时长的正确工具

Date 总是表示相对于纪元的时间点。如果你把“90 秒”存为一个 Date,实际上你是存储了“1970-01-01 加 90 秒”在某个时区。格式化时它可能会显示为 01:01:30、被小时偏移或带上你未预期的日期。

对于持续时长,更好地使用纯数字:

  • 将持续时间存为秒或毫秒(选定一个单位并坚持使用)。
  • 用数字进行算术。
  • 仅在渲染时再转换为显示字符串。

秒转 HH:mm:ss

这是一个适用于倒计时或媒体时长的简单格式化器:

function formatHMS(totalSeconds) {
  const s = Math.max(0, Math.floor(totalSeconds));
  const hh = String(Math.floor(s / 3600)).padStart(2, "0");
  const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
  const ss = String(s % 60).padStart(2, "0");
  return `${hh}:${mm}:${ss}`;
}

formatHMS(75);    // "00:01:15" (倒计时)
formatHMS(5423);  // "01:30:23" (媒体时长)

如果你来自分钟数进行转换,先乘以(minutes * 60),并在渲染前始终保持数值状态。

比较、排序与范围:避免意外

在 JavaScript 中比较时间,最安全的方式是比较数字,而不是格式化后的文本。Date 对象本质上是对纪元毫秒数字的包装,因此你希望比较最终以“数字对数字”进行。

安全比较(时间戳优先)

使用 getTime()(或返回相同数字的 Date.valueOf())来可靠比较:

const a = new Date('2025-01-10T12:00:00Z');
const b = new Date('2025-01-10T12:00:01Z');

if (a.getTime() < b.getTime()) {
  // a is earlier
}

// Also works:
if (+a < +b) {
  // unary + calls valueOf()
}

避免比较像 "1/10/2025, 12:00 PM" 这样的格式化字符串——它们依赖区域设置并且无法正确排序。主要的例外是相同格式和时区下的 ISO 8601 字符串(例如全为 ...Z),它们可以按字典序排序。

按时间排序与范围过滤

如果按纪元毫秒排序很简单:

items.sort((x, y) => new Date(x.createdAt).getTime() - new Date(y.createdAt).getTime());

按范围过滤同样遵循该思路:

const start = new Date('2025-01-01T00:00:00Z').getTime();
const end   = new Date('2025-02-01T00:00:00Z').getTime();

const inRange = items.filter(i => {
  const t = new Date(i.createdAt).getTime();
  return t >= start && t < end;
});

“一天开始/结束”(本地 vs UTC)

“天的开始”取决于你是指本地时间还是 UTC:

// Local start/end of day
const d = new Date(2025, 0, 10); // Jan 10 in local time
const localStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
const localEnd   = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);

// UTC start/end of day
const utcStart = new Date(Date.UTC(2025, 0, 10, 0, 0, 0, 0));
const utcEnd   = new Date(Date.UTC(2025, 0, 10, 23, 59, 59, 999));

尽早选择一种定义,并在比较与范围逻辑中始终坚持它。

调试清单:如何诊断时间转换错误

统一跨应用时间戳
创建秒/毫秒的共享辅助函数,确保每个服务使用相同的单位。
创建项目

时间错误感觉随机,直到你弄清楚你拥有什么(时间戳?字符串?Date?)以及偏移在哪里引入(解析、时区转换、格式化)。

1) 捕获同一时刻的“三种视图”

首先以三种不同的方式记录同一值。这能快速揭示问题是秒与毫秒、本地与 UTC,还是字符串解析的问题。

console.log('raw input:', input);

const d = new Date(input);
console.log('toISOString (UTC):', d.toISOString());
console.log('toString (local):', d.toString());
console.log('timezone offset (min):', d.getTimezoneOffset());

要注意的点:

  • 如果 toISOString() 非常离谱(例如 1970 年或遥远的未来),怀疑是 秒 vs 毫秒。
  • 如果 toISOString() 看起来正确但 toString() 有“偏移”,你正在看到本地时区显示问题。
  • 如果 getTimezoneOffset() 随日期变化,说明你跨越了夏令时。

2) 验证环境:时区与语言环境

许多“在我机器上工作”的报告仅仅是环境默认值不同。

  • 浏览器: 检查操作系统时区设置与浏览器语言。然后打印:
console.log(Intl.DateTimeFormat().resolvedOptions());
  • Node.js / 服务器: 确认进程时区:
console.log('TZ:', process.env.TZ);
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);

如果你的服务器运行在 UTC 上而你的笔记本在本地时区,除非你显式指定 timeZone,否则格式化输出会不同。

3) 在最易出错处添加测试

围绕 DST 边界和“边缘”时间创建单元测试:

  • 在相关时区的 DST 切换前后各一小时
  • 月底/年末,以及 23:30 → 00:30 的交叉情况
  • 如果产品支持多时区,则覆盖多个时区

如果你快速迭代,考虑把这些测试作为你的脚手架的一部分。例如,在 Koder.ai 生成 React + Go 应用时,可以提前添加一个小型“时间契约”测试套件(API 负载示例 + 解析/格式化断言),以便在部署前捕获回归。

上线前快速检查清单

  • 输入有明确契约:纪元毫秒或带偏移的 ISO 8601。
  • 没有歧义字符串如 "2025-03-02 10:00"。
  • 格式化始终指定 locale,并在需要时指定 timeZone。
  • 测试覆盖你目标区域的 DST 边界。

可靠处理时间的推荐模式

在 JavaScript 中可靠地处理时间主要是选择一个“事实来源”并在存储到显示的整个链路中保持一致。

一组简单的最佳实践

在 UTC 中存储与计算。把面向用户的本地时间视为展示细节。

在系统间传输日期时使用带明确偏移的 ISO 8601 字符串(最好是 Z)。如果必须发送数值纪元时间,记录字段并注明单位(毫秒是 JS 的常见默认)。

使用 Intl.DateTimeFormat(或 toLocaleString)为人类展示,并在需要确定性输出时传入显式 timeZone(例如始终以 UTC 或特定业务区域显示时间)。

决策指南:数据库 vs API vs UI

  • 数据库: 将瞬时点存为 UTC(纪元毫秒或 UTC 的 datetime 类型)。除非你的领域确实存储“本地时钟时间”(如“商店在 09:00 开门”),否则避免使用本地 datetime。
  • API 边界: 偏好带 Z 的 ISO 8601(例如 2025-12-23T10:15:00Z)。如果使用纪元,字段名应体现单位(例如 createdAtMs)。
  • UI: 取出存储的 UTC 瞬时点并为用户的区域格式化。在调度 UI 中,明确标注时区并保持转换显式。

什么时候值得使用库

如果你需要重复事件、复杂时区规则、DST 安全的算术(“明天同一本地时间”)或大量来自不一致输入的解析,考虑使用专门的日期时间库。其价值体现在更清晰的 API 和更少的边缘情况错误上。

如果你想深入了解更多时间相关的指南,请访问 /blog。如果你在评估工具或支持选项,请参见 /pricing。

目录
JavaScript 时间通常出问题的地方时间数据类型:时间戳、Date 与字符串时间戳:秒与毫秒(容易混淆)本地时间与 UTC:为什么你的输出会偏移ISO 8601 字符串:API 的最安全格式解析陷阱:Date.parse 与浏览器差异使用 Intl.DateTimeFormat 为人类格式化夏令时(DST):隐蔽的一小时偏差错误持续时长与日期:不要用 Date 去表示计时器比较、排序与范围:避免意外调试清单:如何诊断时间转换错误可靠处理时间的推荐模式
分享
Koder.ai
使用 Koder 构建您自己的应用 立即!

了解 Koder 强大功能的最佳方式是亲自体验。

免费开始预约演示