在 Web 前端开发里,日期格式化几乎是一个绕不开的需求。后台接口返回的时间戳或者 ISO 字符串,需要转换成用户能看懂的「2024年3月15日 14:30」或者「2024-03-15 14:30:00」。然而 JavaScript 的 Date 对象本身并没有提供像 C# 的 ToString("yyyy-MM-dd") 那样直接的方法,新手经常会写出拼接字符串的土办法,老手则会纠结于 toLocaleString 的兼容性和 Intl API 的学习成本。接下来我们就从最基础的原生方法讲起,逐步深入到自定义格式化和国际化处理,让你彻底搞清楚在 JS 里如何优雅地格式化日期。

一、Date 对象自带的格式化方法
先来看 JavaScript 内置的几种格式化输出方式,它们使用简单但各有局限。最常用的有 toString()、toISOString() 和 toLocaleString() 系列。其中 toISOString() 返回 ISO 8601 格式的 UTC 时间,比如 2024-03-15T06:30:00.000Z,这种格式非常适合存储和跨端传输,但直接展示给用户就不太友好,因为末尾带 Z 表示零时区,普通用户难以理解。
而 toLocaleString() 系列(包括 toLocaleDateString() 和 toLocaleTimeString())则可以接受两个参数:locales 和 options。通过 options 可以指定年、月、日、时、分、秒等部件的显示方式。下面这段代码展示了如何输出中文习惯的日期时间:
const date = new Date();
const formatted = date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
console.log(formatted); // 输出类似:2024/03/15 14:30:00
但这里有个问题:不同浏览器对 options 的实现细节并不完全一致。例如早期的 iOS Safari 对 hour12: false 的支持就有偏差,可能导致小时显示成 12 小时制。另外 toLocaleString 默认会根据用户系统语言环境输出,如果用户系统语言不是中文,即使指定了 'zh-CN',某些浏览器的旧版本也可能失效。因此如果项目需要严格固定的格式(比如数据库存储),这种方法就不够可靠。
二、手动构建格式化函数
要完全控制输出格式,最直接的方式是自己写一个格式化函数,利用 Date 对象的 getFullYear、getMonth、getDate 等 getter 方法取出各个时间字段,然后拼接。这个方法没有任何兼容性问题,逻辑透明,还能轻松扩展成支持占位符模板的形式,比如传入 'YYYY-MM-DD HH:mm:ss' 返回对应格式。核心在于处理月份(getMonth 返回 0-11,需要 +1)和补零操作。
补零可以用 ES2017 引入的 String.prototype.padStart 方法,或者自己写一个简单的 pad 函数。下面是一个支持占位符的格式化函数示例,它通过正则替换实现灵活定制:
function formatDate(date, fmt) {
const o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'H+': date.getHours(), // 小时(24小时制)
'h+': date.getHours() % 12 || 12, // 小时(12小时制)
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
'S': date.getMilliseconds() // 毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
}
for (let k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)));
}
}
return fmt;
}
console.log(formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss')); // 输出:2024-03-15 14:30:00
console.log(formatDate(new Date(), 'yyyy/MM/dd')); // 输出:2024/03/15
这个函数非常经典,在很多工具库中都能看到类似实现。它的优点是体积小、零依赖,适合简单页面或对打包体积敏感的项目。需要注意的是,年份占位符 y 的长度控制:如果传入 'yy' 则只取后两位,'yyyy' 取完整四位。月份、日期等则根据占位符长度决定是否补零,例如 'M' 输出 3,'MM' 输出 03。这种设计让调用方可以自由组合格式。
不过手动函数也有短板:它只处理本地时间,如果需要处理 UTC 时间或者带时区偏移的显示,就需要额外传入时区参数或者改用 getUTC 系列方法。另外对于国际化需求(如输出「2024年3月15日 星期五」),手动拼接会变得很繁琐,此时就该考虑下一节的 Intl API。
三、Intl.DateTimeFormat 国际化格式化方案
Intl.DateTimeFormat 是 ECMAScript 国际化 API 的一部分,从 ES2015 开始标准化,现代浏览器已经全面支持。它和 toLocaleString 师出同门,但设计上更面向可复用和高性能场景。我们可以先创建一个 DateTimeFormat 实例,然后多次调用 format 方法,避免了每次格式化都重新解析 options 的开销。下面是一个基础用法:
const date = new Date();
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
});
console.log(formatter.format(date)); // 输出:2024/03/15 14:30:00
Intl.DateTimeFormat 还可以通过 dateStyle 和 timeStyle 快捷指定预设样式,例如 { dateStyle: 'full', timeStyle: 'long' } 会输出「2024年3月15日星期五 GMT+8 14:30:00」这样的完整描述。此外它还支持 hourCycle 选项(例如 'h23' 强制 24 小时制),比 hour12 更加精确。对于需要同时显示多个区域时间的情况,还可以使用 timeZone 选项指定时区,例如 timeZone: 'America/New_York'。
然而 Intl.DateTimeFormat 也有局限性:它返回的格式仍然依赖于实现细节,不同 JavaScript 引擎可能对某些 locale 数据的支持存在差异,比如同样的 options 在 Node.js 和 Chrome 中输出的空格和标点可能略有不同。另外如果希望得到纯数字的 'YYYY-MM-DD HH:mm:ss' 格式,Intl 默认输出的分隔符通常是斜杠或本地化符号,无法直接指定连字符。这时可以使用 formatToParts 方法提取各个部分再手动拼接,这算是 Intl 提供的进阶玩法,能兼顾标准性和灵活性。
const parts = new Intl.DateTimeFormat('en-US', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).formatToParts(date);
const result = parts.map(p => p.value).join('');
console.log(result); // 输出:03/15/2024, 14:30:00(逗号是默认分隔符,需自行处理)
总的来说,Intl.DateTimeFormat 适合需要本地化、多语言支持,且不要求固定分隔符的场景。如果既想获得 Intl 的标准化能力,又要输出固定格式,可以结合 formatToParts 写一个轻量封装,但代码量已经不亚于一个手动格式化函数了。
四、第三方库的选择与替代思路
在 ES2017 之前,很多项目习惯使用 moment.js 处理日期,它的 API 丰富易用,但体积较大(gzip 后也超过 60KB),而且 moment 官方已经宣布进入维护状态,不再推荐新项目使用。目前更主流的替代品有 day.js(2KB 左右,API 与 moment 兼容)和 date-fns(函数式、支持 tree-shaking)。如果你的项目本来就在用这些库,那可以直接利用它们提供的格式化能力。
以 day.js 为例,安装后可以这样格式化日期:
import dayjs from 'dayjs';
dayjs().format('YYYY-MM-DD HH:mm:ss'); // 输出:2024-03-15 14:30:00
day.js 的 format 方法使用了和 moment 相同的占位符规则,对于熟悉 moment 的开发者来说迁移成本极低。而且它支持插件扩展,比如 UTC 插件、时区插件等,能覆盖复杂需求。date-fns 则采用纯函数风格,例如 format(new Date(), 'yyyy-MM-dd HH:mm:ss'),配合 tree-shaking 可以只打包用到的函数,性能与体积更优。
但引入第三方库毕竟增加了依赖,如果项目只是偶尔格式化一次日期,手动函数或 Intl API 完全够用。当代前端框架(React、Vue)的生态中也有许多针对性的日期组件或工具函数,但在决定引入之前,先评估一下原生方案是否已经满足 90% 的需求,避免为了一个小功能增加几十 KB 的包体积。
五、选择建议与常见坑点
综合来看,没有一种方法是绝对的最佳选择,需要根据具体场景权衡。如果是简单的展示且不要求跨语言一致格式,用 toLocaleString 最省事;如果格式固定且应用于大量数据(比如表格渲染),手动格式化函数性能最好;如果需要多语言支持和标准化的本地化输出,Intl.DateTimeFormat 是正道;如果项目已经引入了 day.js 或 date-fns,则没必要重复造轮子。
最后提醒几个容易踩坑的地方:一是月份从 0 开始,手动拼接时忘记 +1 会导致月份少一个月;二是直接使用 toISOString 得到的 UTC 时间与中国时区相差 8 小时,直接展示会误导用户;三是在低版本浏览器或某些移动端 WebView 中,Intl API 可能缺失或行为异常,必要时需要 polyfill;四是使用 padStart 时注意其返回新字符串,不会修改原值,所以别忘了重新赋值。把这些细节注意到,日期格式化这个看似简单的问题就能处理得游刃有余。

