
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/2026vs24/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,但需要注意
IntlAPI 可能需要 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时间处理:多语言实战与统一规范》,带你掌握主流后端语言的时间处理最佳实践,并提炼跨语言通用规范,敬请关注!
如果本文对你有帮助,欢迎点赞、收藏、关注三连,让更多人看到!