1. 先从需求场景说起:为什么日期格式化值得单独研究
1.1 时间戳与“人能看懂的时间”之间隔着一层转换
做前端时间久了,你会发现日期格式化跟防抖节流一样,属于那种“看起来简单,但每个项目都要写一遍”的公共逻辑。后端接口返回的时间,要么是一串时间戳,要么是 ISO 字符串,比如2026-03-18T09:30:00.000Z,这玩意儿直接渲染到页面上,用户肯定看不懂。我们得把它变成“2026年3月18日 上午9:30”或者“2026-03-18 09:30”这种人类友好的格式。
问题是,这个转换并不像String(x)那么无脑。你至少要考虑:
- 如果时间戳是毫秒,直接
new Date(timestamp)就行;如果是秒,还得先乘 1000。 - 月份和星期在 JS 的 Date 对象里是从 0 开始编号的,
getMonth()返回 0 到 11,不处理的话一月份显示成 0 月,直接翻车。 - 数字补零。
getHours()返回 9,你想要的显示效果可能是“09”,得用padStart(2, '0')。 - 时区问题。后端存的可能是 UTC 时间,但用户在上海,直接显示会有 8 小时偏差。
这些细节单独看都不难,但凑到一起就容易出 bug。我见过不少线上问题,比如报表日期差一天、聊天记录时间显示成 1970 年,基本都是这三个坑里至少踩了一个。
1.2 直接引库的隐形成本:体积、维护、重复造轮子
很多团队遇到日期格式化,第一反应是“装个 dayjs”,因为文档清楚、API 友好,dayjs().format('YYYY-MM-DD HH:mm:ss')一行搞定。这个选择本身没问题,但我这几年看项目,逐渐意识到一个容易被忽略的事实:并不是所有场景都需要一个完整的日期库。
首先是体积。dayjs 压缩后大概 2KB,moment 则重得多,加上 locale 和插件动辄几百 KB。对于老项目或对首屏包体积敏感的工程,能省一点是一点。其次是维护成本。moment 官方已经宣告进入维护态,不再增加新功能,新项目再用它,就得考虑长期依赖问题。最后是重复造轮子的问题。就算引了 dayjs,很多团队还是会在工具函数里自己封装一层formatDate,本质上又写了一遍转换逻辑,那这层封装到底用原生 API 还是用库实现,其实值得按场景单独判断。
所以我的建议一直很明确:轻量场景优先原生,复杂场景用库。而这个“原生”方案里,Intl.DateTimeFormat就是最值得好好研究的一个 API。
2. Intl.DateTimeFormat 核心用法:一个构造器覆盖 80% 的格式化诉求
2.1 基础调用方式与返回结果
Intl.DateTimeFormat是 ECMAScript 国际化的一个内置对象。它不是把日期转成字符串那么简单,而是按照你指定的语言环境和选项,输出一个符合当地阅读习惯的日期时间字符串。
最基本的用法是这样:
const date = new Date('2026-03-18T09:30:00.000Z'); const formatter = new Intl.DateTimeFormat('zh-CN'); console.log(formatter.format(date)); // 输出类似:2026/3/18你可能觉得,就这?但注意一个细节:它默认会根据你传入的 locale 自动调整格式。换成en-US,输出就是3/18/2026;换成ja-JP,输出是2026/3/18。同一段代码,不同语言环境自动适配,这是手动拼接很难做到的。
locales参数支持多种写法,可以传字符串或数组,比如:
new Intl.DateTimeFormat('zh-CN', { dateStyle: 'full' }); new Intl.DateTimeFormat(['en-US', 'zh-CN'], { dateStyle: 'full' });数组形式表示“按优先级依次尝试”,第一个能匹配的语言环境会被使用。实际业务里如果你想做国际化但产品只支持中英文,直接把用户语言或浏览器语言传进来就行。
2.2 dateStyle / timeStyle:最省事的预设配置
Intl.DateTimeFormat最爽的一点是提供了dateStyle和timeStyle这两个“懒人选项”。它们不需要你挨个指定年月日时分秒,而是按语言环境自动生成四种不同风格的输出组合:
'full': 完整格式,通常包含星期、完整月份。'long': 长格式,通常包含完整月份。'medium': 中等长度。'short': 短格式,通常是纯数字。
看几个例子,我实测过:
const date = new Date('2026-03-18T09:30:00.000Z'); const full = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'full', timeStyle: 'long' }); console.log(full.format(date)); // 2026年3月18日星期三 GMT+8 17:30:00 const medium = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'medium' }); console.log(medium.format(date)); // 2026/3/18 17:30:00 const short = new Intl.DateTimeFormat('zh-CN', { timeStyle: 'short' }); console.log(short.format(date)); // 17:30注意,这里我用new Date('2026-03-18T09:30:00.000Z'),Z表示 UTC 时间,我在东八区,所以显示成 17:30。这个行为背后就是时区转换,后面我会专门讲。
实际开发里,如果你要给表格的时间列、文章的发布时间、操作记录的时间戳做格式化,用dateStyle: 'short'或'medium'往往就够了,不需要自己拼。
2.3 细粒度控制:weekday / year / month / day / hour / minute / second
dateStyle和timeStyle很好用,但缺点是你不能自由组合。比如你想只显示“3月18日 09:30”,不管年份,这时候就要用细粒度选项了。
Intl.DateTimeFormat支持以下字段:
weekday: 'long' | 'short' | 'narrow',控制星期几的显示。year: 'numeric' | '2-digit'。month: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'。day: 'numeric' | '2-digit'。hour: 'numeric' | '2-digit'。minute: 'numeric' | '2-digit'。second: 'numeric' | '2-digit'。hour12: true / false,控制是否使用 12 小时制。
举个例子,我想输出“2026年3月18日 星期三 下午5:30”这种偏口语化的时间,可以这样配置:
const formatter = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: 'numeric', minute: '2-digit', hour12: true, }); console.log(formatter.format(date)); // 2026年3月18日星期三 下午5:30hour12这个字段特别实用。默认情况下,中文环境的hour12行为可能跟你预想的不一样,你发现输出变成“24:00”还是“上午12:00”都能用这个字段控制。我建议在需要展示给用户看的时间上,明确设置hour12,别依赖默认行为。
另外要注意,细粒度选项不能和dateStyle/timeStyle混用。如果同时出现,引擎的规则是:date 相关字段(year/month/day)和dateStyle冲突时会抛TypeError,时间字段也有相同限制。所以你在配置对象里要么用 style 系,要么用字段系,不要混在一起写。
3. 进阶能力:时区处理、formatToParts 与实例复用
3.1 timeZone:把时区转换这件事交给运行时
这是Intl.DateTimeFormat我觉得最值钱的一个能力——时区转换。
在做国际业务、或者用户时区不固定(比如全球性的工具类产品)时,时间显示经常要对齐到某个固定时区。以前我写过不少“手动加 8 小时”之类的代码,后来发现全是坑,因为夏令时、时区偏移不是固定不变的。
Intl.DateTimeFormat直接提供timeZone选项,你把时区名字传进去,它自动帮你算好:
const date = new Date('2026-03-18T09:30:00.000Z'); const shanghai = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', dateStyle: 'full', timeStyle: 'long', }); console.log(shanghai.format(date)); // 2026年3月18日星期三 中国标准时间 17:30:00 const newYork = new Intl.DateTimeFormat('zh-CN', { timeZone: 'America/New_York', dateStyle: 'full', timeStyle: 'long', }); console.log(newYork.format(date)); // 2026年3月18日星期三 北美东部夏令时间 05:30:00同一个 UTC 时间戳,按不同时区输出,自动化程度非常高。你不需要手动计算偏移量,也不用担心夏令时问题。对于后台管理系统,如果全局要统一显示某个时区的业务时间,你甚至可以在项目初始化时创建一个带timeZone的 formatter 实例,全局复用。
这里要注意的是,timeZone的取值必须是 IANA 时区名,比如Asia/Shanghai、America/New_York、Europe/London。常见的两个字母时区CST、EST这种不推荐,歧义太大,而且部分环境不支持。
3.2 formatToParts:拆分格式化结果的“零件模式”
format()返回的是纯字符串,适合直接展示。但有些场景你需要“知道”格式化结果里哪段是月份、哪段是星期,比如你想把日期里的某个部分包一层特殊样式,单纯靠字符串split去切,代码会非常脆弱。
这时候用formatToParts(),它返回一个数组,每个元素是一个{ type, value }对象,相当于把格式化结果拆成了零件:
const formatter = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short', }); const parts = formatter.formatToParts(new Date('2026-03-18T09:30:00.000Z')); console.log(parts); // [ // { type: 'weekday', value: '周三' }, // { type: 'literal', value: ' ' }, // { type: 'year', value: '2026' }, // { type: 'literal', value: '年' }, // { type: 'month', value: '3' }, // { type: 'literal', value: '月' }, // { type: 'day', value: '18' }, // { type: 'literal', value: '日' }, // ]这个 API 在“日期中某个部分要高亮”的场景特别有用。比如日历组件里,周日那一列的日期标红;或者报表里,把月份数字单独加粗。你不需要去猜字符串的哪个位置是“月”,直接用type字段判断即可。
我自己的习惯是:只要遇到需要“部分样式定制”的日期展示,一律用它,代码比正则匹配字符串干净得多。
3.3 缓存实例:别在循环里反复 new
Intl.DateTimeFormat虽然用起来简单,但有一个性能点值得注意:构造器本身是有开销的,尤其是 locale 匹配和选项解析。如果你在一个大数组循环里每行都new一个实例,肉眼可能看不出卡顿,但 profiling 出来会很难看。
正确做法是,把不依赖循环变量的实例提取出来,复用:
// 推荐:实例提取到循环外 const formatter = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); const rows = list.map((item) => ({ ...item, timeText: formatter.format(new Date(item.timestamp)), }));如果项目里多个模块都要格式化时间,可以封装成一个工具函数模块,内部维护一个缓存 Map:
const formatterCache = new Map(); function getDateTimeFormatter(locale, options) { const key = `${locale}-${JSON.stringify(options)}`; if (!formatterCache.has(key)) { formatterCache.set(key, new Intl.DateTimeFormat(locale, options)); } return formatterCache.get(key); }这样既保留了调用时的灵活性,又避免了重复构造。如果你做的是组件库或者工具库,这个缓存思路可以直接用。
4. 其他可行性方案盘点:手动拼接、toLocaleString、dayjs/moment
4.1 原生 Date 方法手动拼接:最原始也最可控
在没有Intl.DateTimeFormat或者面对老浏览器的时候,很多项目是这么写日期格式化的:
function formatDate(date) { const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const hour = String(d.getHours()).padStart(2, '0'); const minute = String(d.getMinutes()).padStart(2, '0'); const second = String(d.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hour}:${minute}:${second}`; }优点是逻辑透明、零依赖、输出格式完全可控。缺点是:代码里全是padStart,不优雅;而且它用的是运行环境的本地时区,想做时区转换得自己算偏移,非常容易出 bug。我还见过有人用getTimezoneOffset硬算的,代码又长又绕,结果夏令时一到就蹦出各种对不上的问题。
所以我的结论是:手动拼接适用于“系统内部使用、固定格式、不考虑时区”的简单场景。比如后端管理系统的导出文件名、日志打印、接口入参拼串,这些地方手动拼接完全够用,也不一定要上Intl。
4.2 toLocaleString 与 toLocaleDateString:介于原生与手写之间
Date.prototype.toLocaleString()和toLocaleDateString()、toLocaleTimeString()其实底层走的也是Intl那套规则,但调用方式更短。它接受两个参数,第一个是 locale,第二个是 options:
const date = new Date('2026-03-18T09:30:00.000Z'); console.log(date.toLocaleString('zh-CN', { hour12: false })); // 2026/3/18 17:30:00看起来比new Intl.DateTimeFormat(...).format(date)少写一行,但有一个关键区别:toLocaleString每次调用都会重新解析一次 options。如果你在循环里大量使用它,性能会比复用实例的Intl.DateTimeFormat差一些。而且它无法像Intl.DateTimeFormat那样缓存可复用的 formatter。
如果只格式化一两个时间点,用toLocaleString没关系;但如果是对大量数据做格式化,我建议还是走Intl.DateTimeFormat实例复用的路子。
4.3 dayjs / moment / date-fns:库方案的价值与代价
第三方日期库在前端生态里依然很重要,所以我把它放在“可行性方案”里一起对比。
先说 dayjs。它很轻,体积小,API 几乎是 moment 的现代版,dayjs().format('YYYY-MM-DD HH:mm:ss')这种写法大家都熟悉。它还有一个强大之处:插件体系。比如相对时间插件relativeTime,一行就能输出“3 天前”,这个是原生 API 没有的,至少不能这么优雅地实现。
moment 则是老牌库,功能最全,但已经进入维护态,官方建议新项目不用它。如果老项目已经在用,倒也没必要急着全量替换,毕竟它稳定。
date-fns 是函数式风格,按需引入,format(new Date(), 'yyyy-MM-dd')。体积和使用方式都比较现代,适合在意 tree-shaking 的团队。
它们的共同代价是:多一个依赖,多一份需要熟悉 API 的心智。很多简单场景下,你用原生 API 其实已经能拿到完全可读的字符串,没必要额外引库。
4.4 不同场景的选型建议(含对比表格)
我把这些方案按项目场景列了个参考表,方便你直接对号入座:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 管理后台、工具链等内部系统,固定格式 | 手动拼接或 Intl.DateTimeFormat | 简单、无额外依赖,输出可控 |
| 国际化站点,需要按用户语言展示 | Intl.DateTimeFormat | 原生支持 locale,按环境自动适配 |
| 多个时区的时间展示 | Intl.DateTimeFormat + timeZone | 自动处理时区偏移和夏令时 |
| 需要“3 天前”“刚刚”等相对时间 | dayjs + relativeTime 插件 | 原生 API 没有这个能力,手写太啰嗦 |
| 老项目已在用 moment | 不着急替换 | 稳定,直接迁移不一定划算 |
| 组件库、工具库(公共依赖) | Intl.DateTimeFormat | 不增加外部依赖,避免和宿主工程库版本冲突 |
这张表不是绝对的,但它代表了我做技术选型时的一个基本思路:先看会不会频繁变动,再看有没有时区和国际化需求,最后看项目能不能接受新增依赖。
5. 实战与面试:几个高频场景的落地方案
5.1 列表页时间列与“刚刚/几分钟前”处理
列表页是日期格式化最常见的老家,比如操作日志、订单列表、消息通知。以我的经验,这类需求一般是“相对时间 + 具体时间”双模式:很久之前的数据显示具体时间,最近的数据显示“刚刚”“5 分钟前”。
如果是原生方案,可以这样拆分:
- 时间差小于 60 秒,用“刚刚”。
- 时间差小于 1 小时,用“x 分钟前”。
- 超过 1 小时但在当天,用
Intl.DateTimeFormat格式化出“今天 14:30”。 - 更早的一律显示完整日期。
相对时间的计算不复杂,难点在于如何让代码整洁。如果你不想引 dayjs,可以封装成独立函数:
function formatSmartTime(input) { const diff = Date.now() - new Date(input).getTime(); if (diff < 60_000) return '刚刚'; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`; // 其他场景交给 Intl.DateTimeFormat return new Intl.DateTimeFormat('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false, }).format(new Date(input)); }这里有个细节:不要每次调用都newformatter,可以直接放在模块顶层,或者用我前面说的缓存函数。在工具函数模块里,这种固定配置完全可以提升为模块常量。
5.2 版本兼容与降级处理
用原生 API 最担心的就是兼容性。Intl.DateTimeFormat的兼容性在现代浏览器上已经很好了,但如果你负责的 WebView 内核比较老,或者要兼容旧版 Safari,就得留一手。
我常用的降级思路是:先检测Intl.DateTimeFormat是否存在,不存在就退回手动拼接。这样可以保证在不支持的环境里依旧能显示时间,只是格式可能没那么好看。
function safeFormat(date, options) { if (typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat === 'function') { return new Intl.DateTimeFormat('zh-CN', options).format(new Date(date)); } // 降级方案 const d = new Date(date); const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; }如果你在 Node.js 服务端做格式化,还要注意 Node 版本。旧版 Node 对完整 ICU(International Components for Unicode)支持不完整,可能导致某些 locale 输出异常。现在大多数 Node LTS 版本已经内置 full-icu,但有些精简镜像编译时可能关闭了。我的习惯是,服务端输出日期之前先执行一个简单的自测,比如格式化几个不同 locale 的日期,确认输出正常再上线。
5.3 面试追问怎么答:formatToParts、resolvedOptions 与手写格式化
这个标题进入了前端面试题的热搜词圈子,说明确实有相当多的同学在看日期格式化相关考点。我在模拟面试和看面经的时候发现,面试官很少直接问“Intl.DateTimeFormat 是什么”,更多是让你现场写一个日期格式化函数,然后顺着你写的代码追问优化方案。
如果你是面试者,我建议准备这几个点:
第一,手写格式化函数要稳。至少能把年份、月份补零、日期、时分秒都处理对,并且考虑到getMonth()从 0 开始。
第二,会主动提到Intl.DateTimeFormat。在写完手动拼接后,可以补一句“这个写法在固定格式下没问题,但如果要考虑多语言和时区,我会优先用 Intl.DateTimeFormat”,这会让面试官觉得你有全局视野。
第三,能说清楚formatToParts和resolvedOptions。resolvedOptions()是我刚才没细说的一个方法,它能返回格式化器实际生效的配置:
const formatter = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', hour: '2-digit', }); console.log(formatter.resolvedOptions()); // { // locale: "zh-CN", // calendar: "gregory", // numberingSystem: "latn", // year: "numeric", // month: "long", // hour: "2-digit", // ... // }它能帮你排查“为什么我传了选项,输出却不是预期”的情况,非常有价值。面试时能提到这个方法,说明你不是只会调 API,而是理解整个国际化处理链路。
第四,能对比Intl.DateTimeFormat和 dayjs/moment 的优劣。面试官如果继续追问“那你是不是所有项目都该用原生”,你可以回答:原生适合轻量场景,减少依赖和体积;但相对时间、复杂历法、复杂格式化模板,库方案更成熟方便。这是方案选型题,不是非此即彼。
第五,知道locale不是随便传两个字母那么简单。比如zh、zh-CN、zh-Hans-CN是有区别的。zh-CN包含地区和文字类型信息,格式化结果会按中国大陆习惯输出;而zh-TW的日期表达习惯就不同。业务上如果要精细化,建议直接传zh-CN、en-US这种完整 locale,不要只传zh或en。
5.4 国际化站点与多语言切换的具体实践
如果项目要支持多语言,日期显示往往不只是“翻译”而已,还有格式差异。比如中文习惯2026年3月18日,英文习惯March 18, 2026,日文习惯2026年3月18日,但年份进度和中文基本一致。用dateStyle: 'full'或long,这些差异引擎会自动处理,你只需要传入当前语言。
下面是一个最简单的多语言切换示例:
const localesMap = { zh: 'zh-CN', en: 'en-US', ja: 'ja-JP', }; function formatDateByLocale(date, lang = 'zh') { const locale = localesMap[lang] || 'zh-CN'; const formatter = new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeStyle: 'short', hour12: lang === 'en', // 中文环境通常用 24 小时制 }); return formatter.format(new Date(date)); }这里有个小陷阱:如果你全局统一用hour12: false,那么英文环境可能显示2026年3月18日 14:30,但英文用户未必习惯 24 小时制。更稳妥的做法是按语言决定hour12,或者干脆不设置,让其跟随 locale 默认值。这种细节在国际化站点里非常影响体验。
还需要注意“后端返回的日期字符串是否带时区标识”。如果后端返回的是纯本地时间2026-03-18 09:30:00,它没有时区信息,你直接new Date('2026-03-18 09:30:00')在不同环境下会被当成不同时区,结果可能出乎意料。这种情况下最好约束后端返回带T和Z的 ISO 字符串,或者统一返回 UTC 时间戳;如果后端无法修改,那就要在前端约定好“这个字符串是哪个时区的时间”,再做转换。
6. 最后再分享一个实际踩坑经历
有次我维护一个数据报表项目,表格里有一列是“统计时间”。最初用 dayjs 格式化,一切正常。后来产品说为了减小包体积要把 dayjs 去掉,我就顺手换成了Intl.DateTimeFormat。改完自测没发现问题,结果上线第二天就有人反馈:部分浏览器上时间格式不一致。
排查后发现问题出在weekday选项上。我在 options 里传了weekday: 'short',中文环境输出了“周三”,但英文环境由于用户切换了浏览器语言,输出的是 “Wed”。看起来是小问题,但在报表这种密集数据场景里,格式不统一会让老板觉得是 bug。
从那以后我养成了一个习惯:凡是面向固定业务场景的格式化,不要依赖浏览器语言环境,明确传死 locale。比如这个报表,就用zh-CN,不管用户浏览器是什么语言,都输出同一套格式。数据展示的一致性,有时候比“符合用户语言习惯”更重要,尤其是内部后台系统。
后来我又把这个逻辑固化成了工具函数,并且加了缓存,整个项目里所有时间格式化都走同一个入口。这样既统一了风格,以后想调整格式,也只改一个文件。
关于Intl.DateTimeFormat,我的最终体会是:它是前端原生能力里被严重低估的一类 API。你不需要背下所有选项,只需要理解三件事——locale 控制语言习惯、options 控制显示粒度、timeZone 控制时区。把这三点想清楚,再配合formatToParts和实例复用的技巧,大多数业务里的日期格式化需求都能用原生代码干净地解决。