【时间利器】4、JavaScript时间处理全解:Date/moment/dayjs/Temporal

JavaScript时间处理全解:Date/moment/dayjs/Temporal

【导语】 前端开发中,时间处理一直是个让人头疼的难题。new Date('2026-03-24') 在不同浏览器里结果不同;月份从0开始,11月居然写成 10;想显示"2小时前"还要自己写算法......本文带你系统梳理 JavaScript 时间处理的演进史,从原生 Date 的深坑,到 moment.js 的辉煌与落幕,再到轻量级 day.js 和函数式 date-fns 的崛起,最后展望未来的标准 Temporal API。读完本文,你将彻底告别时间处理的噩梦,写出健壮的前端时间代码。


📌 一、引言:JS时间处理的"噩梦"

1.1 Date对象的设计缺陷

JavaScript 的 Date 对象从诞生之初就充满了槽点:

  • 易变性Date 对象是可变的,任何修改都会影响原对象,容易引发副作用
  • 月份从0开始new Date(2026, 2, 24) 表示的是 3月24日,而不是2月,这是最反直觉的设计之一
  • 时区模糊new Date() 返回的是本地时间,但 new Date('2026-03-24') 在不同浏览器可能被解析为UTC或本地时间,导致跨浏览器结果不一致
  • 解析行为不统一Date.parse() 对非标准字符串的解析依赖于实现,例如 "2026-03-24 14:30:00" 在 Chrome 和 Safari 中可能返回不同的结果
  • 缺少人性化方法:没有内置"相对时间"(如"2小时前")的输出,需要自己计算

1.2 真实案例:前端时间显示错误

某 SaaS 系统的后台管理页面,后端返回订单时间:"2026-03-24T06:30:00Z"(UTC)。前端直接 new Date(orderTime) 后调用 toLocaleString(),结果美国用户看到的是 3/23/2026, 11:30:00 PM(因为 toLocaleString 默认使用用户本地时区,而 new Date 解析 ISO 字符串时会正确识别 UTC,但展示时转成了本地)。产品经理投诉:订单明明是今天创建的,为什么显示为昨天?原来是因为时区转换后,UTC 6:30 对应美国西部时间的前一天 22:30。

根源:前端没有明确控制时区展示逻辑,依赖了浏览器的默认行为。

1.3 前端时间处理的痛点

  • 跨浏览器兼容性 :不同浏览器对 Date 构造函数的字符串解析差异
  • 用户时区感知:需要根据用户所在时区展示时间(如中国用户看北京时间,美国用户看美东时间)
  • 国际化显示 :不同语言/地区的日期格式差异(如 03/24/2026 vs 24/03/2026
  • 性能与体积:移动端或首屏渲染要求库体积尽可能小

本文将从原生 Date 入手,逐步升级到现代化方案,帮助你根据场景选择最合适的时间处理方式。


🧨 二、原生Date对象:认清坑,用对场景

2.1 Date对象的底层逻辑

Date 对象在 JavaScript 中基于 Unix 时间戳(毫秒),存储的是自 1970年1月1日 00:00:00 UTC 以来的毫秒数。时区信息完全由运行环境(操作系统/浏览器)提供,Date 对象本身不保存时区。

2.2 基础操作

(1) 时间创建
javascript 复制代码
// 当前时间(本地)
const now = new Date();
console.log(now); // Thu Mar 24 2026 14:30:00 GMT+0800 (中国标准时间)

// 从时间戳创建(毫秒)
const fromTimestamp = new Date(1742807400000);
console.log(fromTimestamp); // 2026-03-24T06:30:00.000Z (UTC)

// 从ISO字符串创建(推荐)
const iso = new Date('2026-03-24T06:30:00Z');
console.log(iso.toISOString()); // 2026-03-24T06:30:00.000Z

// 从年月日创建(注意月份从0开始)
const dt = new Date(2026, 2, 24, 14, 30, 0); // 2026-03-24 14:30:00 本地时间
console.log(dt); // Thu Mar 24 2026 14:30:00 GMT+0800 (中国标准时间)
(2) UTC方法 vs 本地方法

Date 对象提供了两套获取时间组件的方法:本地方法(基于系统时区)和 UTC 方法(基于 UTC+0)。

javascript 复制代码
const date = new Date('2026-03-24T14:30:00+08:00'); // 北京时间 14:30

console.log(date.getHours());       // 14 (本地小时,取决于系统时区)
console.log(date.getUTCHours());    // 6  (UTC小时)
console.log(date.getMonth());       // 2  (3月,因为从0开始)
console.log(date.getUTCMonth());    // 2
(3) 格式化

Date 原生提供几种格式化方法:

javascript 复制代码
const d = new Date();

d.toString();           // "Thu Mar 24 2026 14:30:00 GMT+0800 (中国标准时间)"
d.toISOString();        // "2026-03-24T06:30:00.000Z"
d.toUTCString();        // "Tue, 24 Mar 2026 06:30:00 GMT"
d.toLocaleString();     // "2026/3/24 14:30:00" (取决于浏览器语言)
d.toLocaleDateString(); // "2026/3/24"

2.3 避坑指南(四个经典坑)

坑1:new Date('2026-03-24') 是UTC还是本地?

答案:不同浏览器行为不一致!根据 ES5 规范,仅包含日期的 ISO 8601 字符串应解析为 UTC,但部分旧浏览器或非标准实现会当作本地时间。

javascript 复制代码
// 在 Chrome 中:解析为 UTC
new Date('2026-03-24').toISOString(); // "2026-03-24T00:00:00.000Z"

// 在某些旧版 Safari 中:可能解析为本地时间
// 因此,强烈建议使用带时间的完整 ISO 字符串,如 '2026-03-24T00:00:00Z'

解决方案 :始终使用完整格式 '2026-03-24T00:00:00Z' 表示 UTC 日期,或使用 new Date(Date.UTC(2026, 2, 24)) 构造 UTC 时间。

坑2:月份从0开始
javascript 复制代码
const month = new Date().getMonth(); // 3月 => 2

解决方案:封装辅助函数,或者使用第三方库。

javascript 复制代码
function getRealMonth(date) {
    return date.getMonth() + 1;
}
坑3:Date对象是可变的
javascript 复制代码
const date1 = new Date();
const date2 = date1;
date2.setDate(10); // date1 也被修改了!

解决方案 :需要拷贝时,使用 new Date(date1) 创建新对象。

坑4:解析非标准时间字符串的兼容性问题
javascript 复制代码
const d = new Date('2026-03-24 14:30:00'); // 非 ISO 格式,可能返回 Invalid Date

解决方案 :永远不要用 Date 解析非标准格式。要么用正则拆解,要么用第三方库。

2.4 原生Date的适用场景

  • 简单的本地时间展示(不涉及时区转换)
  • 获取当前时间戳(Date.now()
  • 轻量级场景,且你已清楚上述坑并做了规避

对于复杂的时区、格式化和相对时间,建议使用第三方库。

📸 图1:Date对象常见坑点图示------用思维导图展示月份从0、可变性、解析不一致、UTC/本地混淆四个坑点及其解决方案。


📦 三、第三方库:从moment.js到day.js

3.1 moment.js:经典但笨重

moment.js 曾经是前端时间处理的王者,功能强大,但体积较大(约 200KB+),且不支持 Tree Shaking。目前官方已进入维护模式,不再添加新功能,推荐使用更现代的替代品。

核心功能示例

javascript 复制代码
// 安装:npm install moment
import moment from 'moment';

// 创建
const now = moment();                     // 当前时间
const utc = moment.utc();                 // UTC时间
const fromStr = moment('2026-03-24T14:30:00+08:00'); // 自动识别

// 时区转换(需要 moment-timezone 扩展)
import momentTz from 'moment-timezone';
const beijing = moment.tz('Asia/Shanghai');
const newYork = beijing.clone().tz('America/New_York');

// 格式化
now.format('YYYY-MM-DD HH:mm:ss'); // 2026-03-24 14:30:00

// 相对时间
now.fromNow(); // "2 hours ago"

// 运算
now.add(1, 'day').subtract(2, 'hours');

局限性

  • 体积大,影响首屏加载
  • 可变性(add/subtract 会修改原对象),容易出bug
  • 官方已停止功能开发,仅维护安全漏洞

3.2 day.js:轻量替代(2KB)

day.js 的 API 与 moment.js 几乎一致,但体积仅 2KB,支持插件扩展,是目前最受欢迎的轻量级时间库。

安装与使用

bash 复制代码
npm install dayjs
javascript 复制代码
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/zh-cn';

// 扩展插件
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
dayjs.locale('zh-cn'); // 设置中文

// 基础操作
const now = dayjs();                     // 当前时间(本地)
const utcNow = dayjs.utc();              // UTC时间
const parsed = dayjs('2026-03-24T14:30:00+08:00');

// 时区转换
const beijing = dayjs.tz('2026-03-24 14:30:00', 'Asia/Shanghai');
const newYork = beijing.tz('America/New_York');
console.log(newYork.format()); // 2026-03-24T02:30:00-04:00

// 格式化
console.log(now.format('YYYY-MM-DD HH:mm:ss')); // 2026-03-24 14:30:00

// 相对时间
console.log(now.fromNow()); // "2小时前"

// 运算(不可变,返回新对象)
const tomorrow = now.add(1, 'day');

day.js + 时区插件实现 UTC ↔ 北京时间互转

javascript 复制代码
// UTC 转北京时间
const utcTime = dayjs.utc('2026-03-24T06:30:00Z');
const beijingTime = utcTime.tz('Asia/Shanghai');
console.log(beijingTime.format()); // 2026-03-24T14:30:00+08:00

// 北京时间转 UTC
const beijing = dayjs.tz('2026-03-24 14:30:00', 'Asia/Shanghai');
const utc = beijing.utc();
console.log(utc.format()); // 2026-03-24T06:30:00Z

3.3 date-fns:函数式时间处理

date-fns 采用纯函数、按需导入的方式,每个功能都是独立函数,体积小,适合现代打包工具。

安装与使用

bash 复制代码
npm install date-fns
javascript 复制代码
import { format, addDays, differenceInHours, formatDistance } from 'date-fns';
import { zhCN } from 'date-fns/locale';

const now = new Date();

// 格式化
console.log(format(now, 'yyyy-MM-dd HH:mm:ss')); // 2026-03-24 14:30:00

// 运算(返回新对象)
const tomorrow = addDays(now, 1);

// 时间差
const diff = differenceInHours(tomorrow, now); // 24

// 相对时间
console.log(formatDistance(now, addDays(now, 2), { locale: zhCN })); // "2天"

date-fns 的优势

  • 完全按需,打包后体积小
  • 不可变性,纯函数,易于测试
  • 支持时区(通过 date-fns-tz 扩展包)

3.4 库选型建议

体积 特点 适用场景
moment.js ~200KB 功能全面,API 稳定 老旧项目维护,不推荐新项目
day.js ~2KB 轻量,API 兼容 moment,插件化 大多数新项目,需时区/相对时间
date-fns 按需 函数式,不可变,无副作用 追求打包体积、使用现代开发模式
原生 Date 0 无依赖 极简场景,且已充分了解坑点

📸 图2:库选型对比图------表格形式展示 moment、dayjs、date-fns、原生在体积、API风格、功能完整性、推荐指数上的对比。


🚀 四、未来标准:Temporal API

4.1 Temporal API 的设计目标

Temporal 是 TC39 正在推进的提案,旨在彻底解决 Date 对象的所有缺陷。它提供了不可变、语义清晰、支持时区、支持高精度时间的新 API。

核心特性

  • 不可变性:所有操作返回新对象
  • 明确的类型:区分带时区的时间、无时区日期、无时区时间、时间段等
  • 支持时区:内置 IANA 时区数据库
  • 支持高精度:纳秒级精度
  • 直观的 API:年份从1开始,月份从1开始,无需再记忆坑点

4.2 核心类解析

说明 示例
Temporal.Instant 绝对的纳秒级时间点(类似 Unix 时间戳) 用于机器存储
Temporal.ZonedDateTime 带时区的日期时间(推荐业务使用 2026-03-24T14:30:00+08:00[Asia/Shanghai]
Temporal.PlainDate 不带时区的日期(生日、纪念日) 2026-03-24
Temporal.PlainTime 不带日期的时间 14:30:00
Temporal.PlainDateTime 不带时区的日期+时间 2026-03-24T14:30:00
Temporal.Duration 时间段(支持年/月/日/时/分/秒/毫秒/微秒/纳秒) { hours: 2, minutes: 30 }

4.3 实战案例

(1) 创建 UTC 时间和本地时间
javascript 复制代码
// 当前 UTC 时间(Instant)
const now = Temporal.Now.instant();
console.log(now.toString()); // 2026-03-24T06:30:00.123456789Z

// 当前系统时区的 ZonedDateTime
const nowInLocal = Temporal.Now.zonedDateTimeISO();
console.log(nowInLocal.toString()); // 2026-03-24T14:30:00.123456789+08:00[Asia/Shanghai]

// 从 ISO 字符串创建 ZonedDateTime
const beijing = Temporal.ZonedDateTime.from('2026-03-24T14:30:00+08:00[Asia/Shanghai]');
console.log(beijing.toString()); // 2026-03-24T14:30:00+08:00[Asia/Shanghai]
(2) 时区转换
javascript 复制代码
const beijing = Temporal.ZonedDateTime.from('2026-03-24T14:30:00+08:00[Asia/Shanghai]');
const newYork = beijing.withTimeZone('America/New_York');
console.log(newYork.toString()); // 2026-03-24T02:30:00-04:00[America/New_York]
(3) 时间运算
javascript 复制代码
const now = Temporal.Now.zonedDateTimeISO();

// 加 2 天 3 小时
const later = now.add({ days: 2, hours: 3 });

// 时间差
const diff = now.until(later);
console.log(diff.total({ unit: 'hours' })); // 51
(4) 无时区日期处理
javascript 复制代码
const date = Temporal.PlainDate.from('2026-03-24');
const nextMonth = date.add({ months: 1 });
console.log(nextMonth.toString()); // 2026-04-24

4.4 如何提前使用 Temporal

目前 Temporal 提案处于 Stage 3,尚未被所有浏览器原生支持。但可以通过 polyfill 提前体验:

bash 复制代码
npm install @js-temporal/polyfill
javascript 复制代码
import { Temporal } from '@js-temporal/polyfill';

const now = Temporal.Now.zonedDateTimeISO();
console.log(now.toString());

生产环境建议:待 Temporal 正式成为标准后,再逐步迁移。目前仍推荐 day.js 或 date-fns。

📸 图3:Temporal 核心类关系图------展示 Instant、ZonedDateTime、PlainDate 等类的关系及各自用途。


🌍 五、前端实战:用户时区与国际化

5.1 获取用户时区

浏览器提供了 Intl.DateTimeFormat().resolvedOptions().timeZone 来获取用户的时区 ID:

javascript 复制代码
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log(userTimeZone); // "Asia/Shanghai" 或 "America/New_York"

这个时区 ID 是 IANA 标准格式(如 Asia/Shanghai),可直接用于 day.js 或 Temporal。

5.2 后端返回 UTC,前端转换本地

假设后端 API 返回 ISO 8601 UTC 字符串:

json 复制代码
{
  "create_time_utc": "2026-03-24T06:30:00Z"
}

前端使用 day.js 转换:

javascript 复制代码
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';

dayjs.extend(utc);
dayjs.extend(timezone);

const utcTime = dayjs.utc('2026-03-24T06:30:00Z');
const localTime = utcTime.tz(userTimeZone);
console.log(localTime.format('YYYY-MM-DD HH:mm:ss')); // 根据用户时区显示

5.3 国际化显示(多语言日期格式)

使用 Intl.DateTimeFormat 或 day.js 的 locale 插件:

javascript 复制代码
// 原生 Intl
const date = new Date('2026-03-24T14:30:00Z');
const formatter = new Intl.DateTimeFormat('zh-CN', {
    dateStyle: 'full',
    timeStyle: 'medium',
    timeZone: 'Asia/Shanghai'
});
console.log(formatter.format(date)); // 2026年3月24日星期二 22:30:00

// day.js 国际化
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/en';
dayjs.locale('zh-cn');
console.log(dayjs(date).format('LLLL')); // 2026年3月24日星期二 22:30

5.4 跨端时间处理(浏览器/Node.js/小程序)

  • 浏览器:使用上述库,注意兼容性
  • Node.js :同样可以使用 day.js 或 date-fns,但需要注意 Intl API 可能需要 Node.js 编译时开启 ICU 支持
  • 微信小程序 :不支持 Intl 完整功能,建议使用 day.js 并打包时区数据

🏭 六、工程最佳实践

6.1 前端存储:优先存 UTC 时间戳

在本地存储(localStorage、IndexedDB)中,不要存储格式化的字符串,而应存储 UTC 时间戳(毫秒)。这样在读取时可以直接传给 day.js 或 Temporal 进行格式化,避免时区混乱。

javascript 复制代码
// 存储
const timestamp = dayjs.utc().valueOf();
localStorage.setItem('lastVisit', timestamp);

// 读取
const stored = parseInt(localStorage.getItem('lastVisit'));
const localTime = dayjs(stored).tz(userTimeZone);

6.2 接口传输:统一用 ISO 8601 格式

与后端交互时,统一使用 2026-03-24T06:30:00Z 这样的 UTC 格式。不要使用本地格式字符串,也不要使用时间戳(除非特殊场景,如大量数据传输)。

6.3 避免依赖客户端时区(敏感场景)

对于订单创建时间、支付时间等敏感业务,建议由后端直接返回展示格式(如北京时间字符串),避免前端时区转换带来的争议。前端只负责展示后端已确定的时间。

6.4 性能优化:减少频繁创建 Date/Temporal 对象

在列表渲染或高频操作中,避免在循环中创建大量时间对象。可以预先计算好需要的时间字符串,或使用 Intl.DateTimeFormat 批量格式化。

javascript 复制代码
// 优化前
items.forEach(item => {
    item.displayTime = dayjs(item.time).format('YYYY-MM-DD');
});

// 优化后:使用 formatter 批量格式化
const formatter = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short' });
items.forEach(item => {
    item.displayTime = formatter.format(new Date(item.time));
});

📝 七、总结与预告

7.1 JS时间处理核心建议

场景 推荐方案
简单本地时间展示 原生 Date(注意坑)
需要时区转换、相对时间 day.js + 插件
追求极致体积、函数式 date-fns
新项目,期望未来升级 day.js,待 Temporal 稳定后迁移
探索未来标准 使用 @js-temporal/polyfill 体验

核心原则

  • 后端统一返回 UTC ISO 8601 字符串
  • 前端根据用户时区转换展示
  • 存储用时间戳或 UTC 字符串
  • 敏感时间由后端决定展示格式

7.2 下一篇预告

下一篇将进入后端领域:《Go/C#/Rust时间处理:多语言实战与统一规范》,带你掌握主流后端语言的时间处理最佳实践,并提炼跨语言通用规范,敬请关注!


如果本文对你有帮助,欢迎点赞、收藏、关注三连,让更多人看到!

相关推荐
星轨初途1 小时前
类和对象(中):六大默认成员函数与运算符重载全解析
开发语言·c++·经验分享·笔记·ajax·servlet
骇客野人1 小时前
用python实现一个查询当天天气的MCP服务器
服务器·开发语言·python
踩着两条虫2 小时前
AI 驱动的 Vue3 应用开发平台 深入探究(二十五):API与参考之Renderer API 参考
前端·vue.js·人工智能
踩着两条虫2 小时前
AI 驱动的 Vue3 应用开发平台 深入探究(二十四):API与参考之Provider API 参考
前端·vue.js·ai编程
天空属于哈夫克32 小时前
拒绝被动响应:企业微信主动调用接口高阶方案
开发语言·python
2501_941982052 小时前
Go 语言实现企业微信外部群消息主动推送方案
开发语言·golang·企业微信
恋猫de小郭2 小时前
Android Studio Panda 2 ,支持 AI 用 Vibe Coding 创建项目
android·前端·flutter
南山love2 小时前
spring-boot多线程并发执行任务
java·开发语言
爱学习的程序媛2 小时前
【Web前端】WebSocket 详解
前端·websocket·网络协议·web