☰
前端时间戳格式化完全指南:手写、Intl.DateTimeFormat与dayjs方案对比
2026/10/1 4:23:07 网站建设 项目流程

做前端这几年,我写过最多的工具函数大概就是时间戳格式化。尤其拿到后端接口返回的一串1699999999或者1699999999000,要展示成YYYY-MM-DD HH:mm:ss这种人类可读的格式。这活儿看着简单,真做起来坑不少:10位和13位时间戳搞混、Safari 解析字符串返回 NaN、服务器时区跟本地差8小时……每一个都能让你多加班一小时。这篇文章就专门聊这个高频操作,我会把三种主流的实现办法拆开揉碎讲清楚,从手写函数、内置 API 到第三方库,各自适合什么场景、有什么坑,一次性说明白。

1. 时间戳那点事:先搞清楚你手里拿的到底是啥

1.1 10位和13位:单位不一样,结果是天壤之别

时间戳的本质是“从1970年1月1日 00:00:00 UTC 到指定时间经过的毫秒数或秒数”。10位时间戳精确到秒,13位时间戳精确到毫秒。为什么有区别?因为不同语言、不同接口返回到前端时习惯不同。比如 Java 的System.currentTimeMillis()返回 13 位,PHP 的time()返回 10 位,很多 Python 后端直接返回int(time.time())也是 10 位。前端拿到之后如果不做判断,直接丢给new Date(),问题就来了。

new Date(1699999999)得到的是 1970 年,因为 JS 的 Date 构造器会把数值当成毫秒处理。你需要new Date(1699999999 * 1000)才会得到正常时间。这个坑几乎每个前端都踩过,而且一旦踩了,页面上显示的不会是报错,而是一串非常离谱的日期,排查起来特别费劲。所以在做格式化之前,第一步永远是先确认单位,并统一转成毫秒。

1.2 不同来源下拿到的数据形态

除了 10 位和 13 位的数值,实际项目里还经常遇到这几种情况:

  • 后端返回字符串:"2024-01-05 12:30:00"或"2024/01/05 12:30:00"。
  • 后端直接返回 ISO 字符串:"2024-01-05T12:30:00.000Z"。
  • 数据库里存的是bigint,经 JSON 序列化后变成字符串型的"1700000000000"。
  • 你可能已经有一个Date对象,只是想重新格式化。

这几种形态本质上都不是“时间戳”,但最终都要转换成YYYY-MM-DD HH:mm:ss。很多人只针对“时间戳数字”写格式化函数,遇到字符串就懵了。我的建议是:先写一个解析函数,能把各种输入统一变成合法的 Date 对象,再在这个基础上做格式化。这样无论后续用哪种方案,入口都是干净的。

1.3 统一入口:先写一个安全的解析函数

下面这个函数是我自己在项目里一直在用的基础工具,兼容数字、数字字符串、Date 对象,并且自动识别 10 位和 13 位:

function parseDate(input) { // 已经是 Date 对象直接返回 if (input instanceof Date) return input; let ts = input; // 数字字符串先转成 number if (typeof ts === 'string' && /^\d+$/.test(ts.trim())) { ts = Number(ts); } // 字符串日期直接交给 Date 处理 if (typeof ts === 'string') { return new Date(ts); } // 数字:小于 1e12 的按秒处理,大于等于 1e12 的按毫秒处理 if (typeof ts === 'number') { if (ts < 1e12) { ts = ts * 1000; } return new Date(ts); } // 其他情况返回无效日期 return new Date(NaN); }

为什么要用1e12这个阈值?因为 1e12 毫秒对应的是 2001 年 9 月 9 日 01:46:40 UTC。任何 10 位时间戳(秒)都远小于这个值,任何 13 位时间戳(毫秒)都在这个值之上。只有那些极端冷门的时间(比如 2001 年 9 月 9 日之前的毫秒时间戳)才会误判,正常业务场景完全够用。

2. 第一种办法:手写格式化函数,最可控也最踏实

2.1 完整代码与逐行拆解

手写函数是我最早用的方式,到现在也是我脑子里的“默认方案”,因为它不依赖任何外部代码,运行性能高,逻辑完全可控。核心思路就三步:从 Date 对象中获取年、月、日、时、分、秒,给每个部分补零到两位,最后拼成目标字符串。

function formatTimestampSimple(input) { const date = parseDate(input); if (isNaN(date.getTime())) { throw new Error('Invalid date input'); } const Y = date.getFullYear(); const M = String(date.getMonth() + 1).padStart(2, '0'); const D = String(date.getDate()).padStart(2, '0'); const H = String(date.getHours()).padStart(2, '0'); const Min = String(date.getMinutes()).padStart(2, '0'); const S = String(date.getSeconds()).padStart(2, '0'); return `${Y}-${M}-${D} ${H}:${Min}:${S}`; }

这几个 get 方法的含义要记牢:getMonth()返回 0 到 11,所以必须+1才是真实月份;getHours()返回的是本地时区的小时;getFullYear()和getYear()不一样,后者已经废弃,千万不要用。

2.2 补零的两种姿势:padStart 与 slice

补零是手写方案里最容易出细节问题的地方。早期老代码里常见的是这种写法:

const M = ('0' + (date.getMonth() + 1)).slice(-2);

原理很简单:如果月份是 9,'0' + 9得到'09',slice(-2)取后两位还是'09';如果月份是 11,'0' + 11得到'011',slice(-2)取后两位得到'11'。效果没问题,只是可读性稍微差一点。现在String.prototype.padStart已经是 ES2017 标准,浏览器兼容性早就没有问题了,直接用它更清晰。

顺带提一句,padStart(2, '0')只对“内容少于两位”做填充,不会截断超过两位的内容,所以 10 月、11 月、12 月传进去也不会有问题。我见过有人担心 padStart 会把 11 截成 01,这是误解。

2.3 这个方案的适用场景与优缺点

优点很明显:零依赖、无构建成本、代码短、性能最高,只要浏览器支持 ES6 模板字符串就能跑。无论你是写简单的Utils.js,还是给老项目打补丁,都很合适。手写函数还可以随意扩展,比如想要YYYY年MM月DD日 HH:mm:ss,改一下拼串就行,不需要去学库的 token 规则。

缺点是可维护性完全靠自觉。如果一个项目里多个文件各写各的格式化函数,有人用了getMonth() + 1,有人忘记加,时间一长必然出问题。所以手写方案一定要配合“统一入口解析 + 统一工具函数”的工程规范来用。另外,手写方案默认取的是本地时区,如果后端返回的是 UTC 时间而你希望展示北京时间,就要在函数里额外处理时区偏移,这一点后面专门说。

3. 第二种办法:Intl.DateTimeFormat,被你低估的内置 API

3.1 它到底能干什么

很多前端写了几年代码,压根不知道Intl.DateTimeFormat的存在。这是 ECMAScript 国际化的内置构造器,专门做日期时间的本地化格式化。它能解决手写方案里最头疼的时区和本地化问题。你可以指定timeZone为'Asia/Shanghai'、'UTC'甚至'America/New_York',而不用自己去算时差,这对于服务端渲染或者跨国项目是刚需。

3.2 代码实现与关键参数说明

直接调format()的话,输出结果会带语言环境的分隔符。比如中文环境下可能是2024/1/5 12:30:00,英文环境下又可能是另一种格式。最稳的做法是用formatToParts(),它能把年月日时分秒拆成一个个独立的部分,让你自己决定拼法:

function formatTimestampIntl(input, timeZone = 'Asia/Shanghai') { const date = parseDate(input); const dtf = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', timeZone: timeZone }); const parts = dtf.formatToParts(date); const map = {}; parts.forEach(part => { map[part.type] = part.value; }); return `${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}:${map.second}`; }

这里有两个点要注意。第一,hourCycle: 'h23'是为了避免某些环境下午夜被格式化成24:00,加了这个配置之后保证是00到23。第二,timeZone一定要显式传,否则会跟随运行环境的系统时区。尤其 Node.js 服务部署在境外或 Docker 容器里,默认 TZ 经常是 UTC,不传这个参数你格式化出来的时间就会比北京时间慢 8 个小时。

3.3 性能真相:其实它没那么慢

很多人觉得Intl.DateTimeFormat是重型 API,性能一定差。实际上并没有那么夸张。真正有开销的是每次new Intl.DateTimeFormat(...)创建实例的过程。如果我们把实例提取出来复用,性能会好很多:

const dtf = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', timeZone: 'Asia/Shanghai' }); function formatTimestampIntlReuse(input) { const date = parseDate(input); const parts = dtf.formatToParts(date); const map = {}; parts.forEach(part => { map[part.type] = part.value; }); return `${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}:${map.second}`; }

在大量循环调用同一个格式时,这种写法的性能已经和手写函数非常接近。如果你只格式化一个固定格式、固定时区,用这个方案非常省心。但如果你要格式化的格式、时区频繁变化,Intl.DateTimeFormat的优势就会被创建实例的开销抵消,反而不如手写函数直接。

4. 第三种办法:用 dayjs / moment.js 一行搞定

4.1 库选型的现实逻辑:为什么大多数项目选 dayjs

第三方库现在是很多团队的首选。老牌moment.js功能全面、文档丰富、生态庞大,但它已经停止维护,而且体积动辄几百 KB,打包成本高。dayjs是一个更轻量的替代品,核心包只有 2KB 左右,API 和moment.js基本一致,通过插件机制可以按需引入功能。如果你的项目里已经用到了日期库,直接用dayjs是最省事的。

如果只是格式化一个时间戳,完全没必要为一个单纯需求引入整个库。但如果项目里有日期运算、时长计算、多时区、国际化等复杂需求,手写就太累了,这时候直接用库才是正确选择。

4.2 代码实现与工程化接入

用dayjs格式化时间戳非常简单:

npm install dayjs
import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; dayjs.extend(utc); dayjs.extend(timezone); function formatTimestampDayjs(input, timeZone = 'Asia/Shanghai') { const date = parseDate(input); return dayjs(date).tz(timeZone).format('YYYY-MM-DD HH:mm:ss'); }

如果只是单纯想格式化,不需要指定时区,可以省略 utc 和 timezone 插件,直接核心包就够了:

import dayjs from 'dayjs'; function formatTimestampDayjs(input) { const date = parseDate(input); if (isNaN(date.getTime())) return ''; return dayjs(date).format('YYYY-MM-DD HH:mm:ss'); }

注意这里我仍然调用了前面的parseDate做统一解析,因为dayjs对 13 位时间戳可以直接处理,对 10 位时间戳却会得到 1970 年。所以在传给它之前,先把 10 位转成 13 位或者先变成 Date 对象,是很有必要的。如果不想引入 parseDate,也可以这样处理:

function formatTimestampDayjs(input) { const ts = typeof input === 'number' && input < 1e12 ? input * 1000 : input; return dayjs(ts).format('YYYY-MM-DD HH:mm:ss'); }

4.3 什么场景下才值得引入库

我的建议是,一个项目里有以下任一情况,就可以放心引入日期库:

  • 需要大量格式化动作,但格式五花八门,比如YYYY-MM-DD、MM/DD、HH:mm、星期几等。
  • 需要做日期加减,比如“三天后”“上一周的周一开始”。
  • 需要比较两个日期的大小或差值,希望跳过手工计算 ms 的过程。
  • 需要处理多种时区,或者需要国际化显示。

只是想显示一个消息列表的时间,就别引库了,手写函数反而干净。

5. 三种方案横向对比:该选谁

5.1 一张表把账算清

对比维度手写函数Intl.DateTimeFormatdayjs 第三方库
代码量少,约 10 行中等,约 20 行最少,1 行核心调用
运行时体积00(浏览器内置)按需引入,核心约 2KB
性能最高高(复用实例时)中等,有函数调用开销
时区可控性需手动处理偏移强,直接指定 timeZone强,配合 timezone 插件
可维护性依赖团队规范较好最好,API 标准
自然语言理解直接需要知道配置项需要知道 token 规则
适用场景简单项目、单格式多时区、Node 服务端复杂日期逻辑、大型项目

从表格能看出,三种方案没有绝对的高下之分,核心区分点在于你项目的复杂度。如果只是内部系统展示几个时间,手写完全够用;如果部署环境涉及时区,Intl.DateTimeFormat很香;如果项目里到处是日期操作,那就别纠结了,上dayjs。

5.2 一个实用的选型流程

我通常在团队里按这个思路做选型:

  1. 先判断入口形态:前端页面展示,还是 Node.js 服务端?服务端优先考虑时区因素,推荐Intl.DateTimeFormat或dayjs。
  2. 再判断格式数量:只格式化一种格式,手写函数最稳;未来可能扩展多种格式,直接上dayjs。
  3. 看现有依赖:项目里已经有moment或dayjs,就别再自己写一套了,直接复用库方法。
  4. 考虑维护成本:工具函数要统一收口到utils/date.js,不管哪种实现,不要让每个页面各自实现一份。

这样选下来,大部分项目最后都会落到两种答案:要么是一个干净的手写函数,要么直接上dayjs。用Intl.DateTimeFormat反而多见于追求“零依赖”的库封装或 Node 服务端中间件。

6. 实战中常见的坑,踩一次就长记性

6.1 10位时间戳直接吞了,得到 1970 年

这是全项目里出现率最高的问题。接口返回1699999999,前端不假思索new Date(1699999999),于是页面上所有时间都显示1970-01-20 21:33:19之类的乱码。解决办法就一句话:判断小于 1e12 就先乘以 1000。这也是我为什么反复强调parseDate要放在最前面。

6.2 字符串日期在 Safari 上解析失败

new Date('2024-01-05 12:30:00')在 Chrome、Node 里都能得到正确结果,但在 iOS Safari 和一些旧版 WebView 中会返回Invalid Date。原因是 Safari 对非 ISO 格式的字符串支持不友好。稳妥的做法是把字符串里的-替换成/:

function stringToDate(str) { return new Date(str.replace(/-/g, '/')); }

new Date('2024/01/05 12:30:00')在各主流浏览器里都能解析。如果你遇到的字符串是 ISO 格式2024-01-05T12:30:00.000Z,那是严格标准格式,不需要替换也能解析。但这个坑很隐蔽,上线后发现只有 iPhone 用户看到的时间乱掉,排查起来相当头疼。

6.3 toISOString() 的时区陷阱

很多新手喜欢这样写:

new Date(timestamp).toISOString().slice(0, 19).replace('T', ' ');

看起来很短,也能得到YYYY-MM-DD HH:mm:ss,但toISOString()返回的是UTC 时间,不是本地时间。如果你的用户在中国,拿到的时间会比实际时间慢 8 小时。比如用户上传了一张照片,前端显示的上传时间是 04:00,实际是北京时间 12:00,这个 bug 如果不看时区很难发现。所以涉及到展示场景,建议用getFullYear()、getHours()这类本地方法,或者使用Intl.DateTimeFormat指定时区。

6.4 服务器部署在别的地方,时区全乱了

这个问题主要出现在 Node.js 服务端。本地开发时系统 TZ 是Asia/Shanghai,格式化一切正常。部署到 Docker 容器后,容器默认时区是 UTC,代码里所有new Date().getHours()拿到的都是 UTC 小时,最后接口返回的数据比实际时间慢 8 小时。解决办法有三个:

  • 在 Dockerfile 里设置环境变量ENV TZ=Asia/Shanghai。
  • 在项目启动入口执行process.env.TZ = 'Asia/Shanghai'(Linux 环境有效)。
  • 格式化时统一走Intl.DateTimeFormat或dayjs.tz,显式指定时区。

第三种最稳,因为不依赖运行环境配置。只要格式化函数内部把timeZone写死,不管部署到哪台机器,输出结果永远都是你指定的时区。

6.5 传参类型五花八门

接口数据有时候是数值,有时候是字符串。如果后端把timestamp字段定义成Long,序列化到 JSON 后可能被某些语言转换成字符串(比如 Java 的 Jackson 在配置不当时会把 bigint 输出为字符串),前端拿到的就是"1700000000000"。如果你直接用typeof input === 'number'判断 10 位或 13 位,字符串型时间戳就会被当成异常值处理。所以解析函数里一定得有一个“纯数字字符串转 number”的步骤,也就是我在parseDate里写的那段正则可以覆盖的场景。

6.6 补零和拼接的小技巧

最后说一个看起来不起眼但很影响体验的细节:很多人补零时写('0' + n).slice(-2),这个没问题,但如果 n 本身是undefined或NaN,得到的结果会是'0undefined'之类,页面上就是乱码。所以格式化之前最好先校验date.getTime()是否为NaN,无效输入直接抛错或返回空串,而不是把错误时间默默展示出来。

7. 一点工程化心得

7.1 把时间工具收口到一个文件里

不管你在项目里选了哪种方案,我都强烈建议把时间相关的工具函数收口到一个文件里,比如src/utils/date.js。对外只暴露一个formatDateTime(input, pattern?, timeZone?)之类的函数,内部再去判断用哪种实现。好处是显而易见的:需求变更时只改一个文件,全项目生效;单元测试也好写,把 10 位、13 位、字符串、Date、无效值全部测一遍,其他业务代码不会再被时间问题打扰。

7.2 建议长期保留的工具函数形态

我目前最常用的工具函数长这样:

import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; dayjs.extend(utc); dayjs.extend(timezone); const DEFAULT_TIME_ZONE = 'Asia/Shanghai'; function toDate(input) { if (input instanceof Date) return input; let ts = input; if (typeof ts === 'string' && /^\d+$/.test(ts.trim())) { ts = Number(ts); } if (typeof ts === 'number' && ts < 1e12) { ts *= 1000; } return dayjs(ts); } function formatDateTime(input, pattern = 'YYYY-MM-DD HH:mm:ss', timeZone = DEFAULT_TIME_ZONE) { const d = toDate(input); if (!d.isValid()) { return ''; } return d.tz(timeZone).format(pattern); } export { toDate, formatDateTime };

这个形态在复杂项目里已经足够稳定,同时兼容直接调用dayjs可能遇到的各种输入形态。如果你不想引库,把dayjs(ts)替换成new Date(ts),再把格式化逻辑换成手写拼接或Intl.DateTimeFormat也一样。

7.3 最后的个人经验

时间戳格式化看着是“小工具”,但它在所有系统里都无处不在。日志、消息、订单、审批、榜单……凡是有时间展示的地方就有它。我在实际项目里踩过最多的并不是“不会写格式化”,而是“各种来源的时间形态不统一”导致结果错乱。与其在业务代码里到处写new Date(...)再拼字符串,不如花十分钟把解析函数和格式化函数一次性整理好,后续所有页面直接复用。这个习惯很能减少线上问题,也让我在每次接到“时间显示不对”的工单时,都能快速定位到原因,而不是一家家业务页面去翻代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询