拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaScript日期格式化:从Date对象到工具函数封装全解析

JavaScript日期格式化:从Date对象到工具函数封装全解析

1. 核心需求与方案选型

1.1 原始需求拆解

“获取当前年月日,格式化成 YYYY-mm-dd 和 YYYY年mm月dd日”——这个需求看起来简单到不行,几乎每个前端人都写过,但它恰恰是面试高频题、日常开发高频工具函数,也是新手最容易写出“能用但有隐患”代码的地方。

先说清楚两个关键点:第一,JavaScript 里Date对象是唯一的原生日期时间处理入口;第二,YYYY-mm-dd这种格式其实并不是 JavaScript 内置能力,它需要你自己拼接或者借助工具库格式化。很多新手以为Date对象可以直接toString出来一个“2026-05-18”这样的字符串,实测会发现完全不是那么回事——默认输出是Mon May 18 2026 14:30:00 GMT+0800 (中国标准时间)这种鬼样子,根本没法直接用。

所以这个项目标题背后,真正考验的是三件事:你能不能拿到准确的年、月、日数值;你能不能按指定分隔符拼接出目标字符串;你知不知道格式化过程中有哪些天坑(比如月份从 0 开始计数、getMonth()需要 +1、两位数补零之类)。

1.2 方案对比与选型思路

实现“获取当前年月日并格式化”这件事,目前市面上有四种主流路径:

方案写法复杂度灵活性运行时体积适用场景
原生getFullYear/getMonth/getDate拼接低高0绝大多数业务场景
toISOString().slice(0, 10)极低低0仅需日期且不担心时区偏移时
Intl.DateTimeFormat中高0需要本地化、多语言格式化
dayjs / date-fns 等工具库极低极高2KB~7KB项目里已引入或用到了复杂日期运算

我个人的建议是:不要一上来就上第三方库。日期格式化这个需求,原生 API 完全可以搞定,而且代码量不超过十行。工具库能提供的能力(时区处理、相对时间、复杂运算)在纯“获取当前年月日”这个场景下是杀鸡用牛刀。等真到了需要处理“N 天前”“本周一”“下个月最后一天”这类复杂逻辑时,再考虑引入 dayjs 也不迟。

2. Date 对象基础与格式化原理

2.1 从时间戳到年月日的转换链路

理解日期格式化,先得搞清楚 JavaScript 里时间到底是怎么存储的。Date对象的内部实现是一个数字——自 1970 年 1 月 1 日 00:00:00 UTC(协调世界时)以来的毫秒数,也就是时间戳。你在控制台执行Date.now(),返回的是1779100000000这样的值,这个值本质上跟“人类可读的年月日”毫无关系,它只是时间轴上的一个精确刻度。

要从这个时间戳得到你熟悉的年月日,需要调用Date实例的 getter 方法:

  • getFullYear():返回四位数年份,比如 2026。注意,不是getYear(),getYear()返回的是“当前年份减去 1900”,浏览器里会得到 126 这种莫名其妙的值,已经被废弃,不要用。
  • getMonth():返回 0~11 的月份值,0 代表一月。这是新手最容易踩的坑,没有之一。输出前必须 +1。
  • getDate():返回 1~31 的日期值。有些资料里会把它和getDay()(返回 0~6 的星期值,0 代表周日)搞混,getDate才是你要的“几号”。

这三个 getter 拿到的都是本地时区的值。所谓“本地时区”,就是运行这段代码的设备系统里设置的时区,在中国通常是东八区(UTC+8)。这跟后面要讲到的toISOString()的“UTC 时间”有本质区别,也是很多格式化 bug 的根源。

2.2 最原始的拼接写法

先看一段没有任何语法糖、完全符合 ES5 规范的写法,这也是最稳、最容易透底的版本:

function formatDate(date) { var year = date.getFullYear(); var month = date.getMonth() + 1; var day = date.getDate(); // 月份和日期补零到两位 if (month < 10) { month = '0' + month; } if (day < 10) { day = '0' + day; } return year + '-' + month + '-' + day; } console.log(formatDate(new Date())); // 输出:2026-05-18

这段代码的关键就一句话:getMonth()返回的月份必须 +1,否则你会惊喜地发现 5 月 18 日变成了“2026-04-18”。补零的逻辑用if判断是给新手看的,让你理解它是怎么一回事。如果你嫌弃if太啰嗦,可以换成三元表达式:

var month = date.getMonth() + 1; month = month < 10 ? '0' + month : month;

写到这里你会发现,所有逻辑本质上就是“取数值 → 补零 → 拼接”三步,没有任何黑魔法。

2.3 用 padStart 补零与模板字符串优化

现代 JavaScript(ES2017+)提供了更优雅的补零方案:String.prototype.padStart()。它的作用是把字符串填充到指定长度,填充字符由第二个参数指定。比如'5'.padStart(2, '0')会返回'05','12'.padStart(2, '0')会返回'12',因为原长度已经达标,不会强制截断。

配合模板字符串,整个格式化函数可以压缩到几行:

function formatYMD(date, separator = '-') { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}${separator}${month}${separator}${day}`; } console.log(formatYMD(new Date())); // 2026-05-18 console.log(formatYMD(new Date(), '/')); // 2026/05/18 console.log(formatYMD(new Date(), '年')); // 2026年05月18日(这不符合中文习惯,仅示意)

注意一个细节:padStart是字符串方法,所以必须先String()或者用toString()把数字转成字符串,直接对数字调用会报date.getMonth().padStart is not a function。

如果你要输出“YYYY年mm月dd日”这种中文格式,直接改分隔符为'年'会得到2026年05月18日,但标准的“YYYY年mm月dd日”通常指的是2026年5月18日,月份不补零。两种风格都有人用,你要根据产品需求确定要不要补零。如果不想补零,只去掉padStart那一步就行:

function formatYMDChinese(date) { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); return `${year}年${month}月${day}日`; } console.log(formatYMDChinese(new Date())); // 2026年5月18日

3. 进阶方案与工具函数封装

3.1 用 toISOString 实现一行格式化

网上流传非常广的一行代码方案是:

const today = new Date().toISOString().slice(0, 10); console.log(today); // 2026-05-18

原理是toISOString()会把 Date 对象转成 ISO 8601 格式的 UTC 时间字符串2026-05-18T06:30:00.000Z,slice(0, 10)截取出前十个字符,正好是2026-05-18。

这行代码看起来优雅无比,但它有一个隐藏的坑:时区偏移。toISOString()返回的是 UTC 时间,不是本地时间。中国在东八区,本地时间比 UTC 快 8 小时。如果当前本地时间是2026-05-18 07:30:00,那么 UTC 时间还是2026-05-17 23:30:00,toISOString().slice(0, 10)返回的就是2026-05-17,整整少了一天。

这意味着什么?你的用户在中国,后端服务器部署在新加坡或美国,或者你的设备时区设置错误,就会在每天早晨 8 点之前出现“日期不对”的诡异 bug。用户明明感觉自己已经在 5 月 18 日了,页面却显示 5 月 17 日。

所以我的建议非常明确:如果你要格式化的目标是“用户本地所见”的日期,不要用toISOString()。它只适合纯 UTC 场景,比如日志记录、服务器端统一时间、两个时区无关的技术字段。

3.2 封装一个健壮的格式化工具函数

实际开发中,你不会只想格式化“当前时间”,你还得格式化任意 Date 对象;你也不会只想要YYYY-mm-dd一种格式。最好的做法是封装一个通用工具函数,暴露多种格式选项:

function formatDate(date, pattern = 'YYYY-mm-dd') { if (!(date instanceof Date)) { date = new Date(date); } if (isNaN(date.getTime())) { throw new Error('Invalid date'); } const map = { YYYY: date.getFullYear().toString(), mm: String(date.getMonth() + 1).padStart(2, '0'), m: String(date.getMonth() + 1), dd: String(date.getDate()).padStart(2, '0'), d: String(date.getDate()), }; return pattern.replace(/YYYY|mm|dd|m|d/g, (match) => map[match]); } console.log(formatDate('2026-05-18T10:00:00')); // 2026-05-18 console.log(formatDate(Date.now(), 'YYYY年mm月dd日')); // 2026年05月18日 console.log(formatDate(undefined, 'YYYY/mm/dd')); // 2026/05/18(传入 undefined 会 new Date())

这个函数的设计有几个考虑:

第一,入参兼容 Date 对象、时间戳字符串、时间戳数字,统一在内部转成 Date 实例,避免调用方传参类型不一致导致getFullYear is not a function的报错。

第二,加入非法日期校验。new Date('abc')不会抛异常,只会生成一个 Invalid Date,后续所有 getter 返回 NaN,拼接出来一个NaN年NaN月NaN日的垃圾字符串。所以要在入口处用isNaN(date.getTime())拦截,这是很多三五年经验的人都容易忽略的细节。

第三,用正则替换的方式支持多种 pattern,而不是写死几种返回值。这样后续如果冒出来一个YYYY-mm-dd HH:mm:ss的格式需求,只需要在 map 里加HH: String(date.getHours()).padStart(2, '0'),再在正则里加上HH即可,函数不必推翻重写。

3.3 是否需要引入第三方库

如果你在项目里已经装了 dayjs、moment.js 或 date-fns,那直接用现成 API 是最省事的:

// dayjs 写法 import dayjs from 'dayjs'; console.log(dayjs().format('YYYY-MM-DD')); // 2026-05-18 console.log(dayjs().format('YYYY年MM月DD日')); // 2026年05月18日 // date-fns 写法 import { format } from 'date-fns'; console.log(format(new Date(), 'yyyy-MM-dd')); // 注意 date-fns 用小写 yyyy

但如果你只是需要一个日期格式化,我不建议单纯为这一行功能引入依赖。原因有三:一是包体积虽小但没必要;二是原生方案足够稳定且不存在 API 过期风险;三是不依赖任何外部库的代码在任何环境(浏览器、Node.js、小程序、Web Worker)都能直接复制使用。等你的项目里出现了类似“计算两个日期相差多少天”“获取上个月最后一天”这类原生 API 处理起来比较麻烦的需求时,再引入 dayjs 才是合理的时机。

4. 高频问题与避坑指南

4.1 时区偏移导致的日期错误

这是最常见的坑,前面已经详细展开过toISOString()的时区问题,这里再补充一个场景:后端返回的时间字符串在格式化时出现偏移。

假设接口返回2026-05-18T00:00:00Z,这是 UTC 时间。你用new Date('2026-05-18T00:00:00Z')解析后,在东八区会得到本地时间2026-05-18 08:00:00,格式化后依然是 5 月 18 日,没问题。但假设接口返回2026-05-18T00:00:00(不带 Z,也没有时区偏移标记),不同浏览器的解析行为可能不一致,有的按本地时间解析,有的按 UTC 解析,这就可能导致日期差一天。处理办法是:确保接口层返回的时间字符串带明确时区标识,或者在解析时手动指定new Date('2026-05-18T00:00:00+08:00')。

4.2 月份从 0 开始计数的连环坑

getMonth()返回值范围是 0~11,这个设计继承自 Java 的Date类,坑了很多代前端。据统计,新手在日期格式化中遇到的报错,第一步十个有八个是忘记 +1。

更隐蔽的是跨年场景。比如你在 2026 年 1 月 1 日执行:

const date = new Date(); console.log(date.getFullYear()); // 2026 console.log(date.getMonth() + 1); // 1,完全没问题 console.log(date.getDate()); // 1

看起来没问题,但如果你的代码里不小心多写了一个var month = date.getMonth();(忘记 +1),输出就成了2026-00-01,这个00月份在大多数日期解析函数里会被抛到上一年 12 月,于是一顿操作之后日期变成了2025-12-01。这种 bug 因为出现在月初时分,非常难排查。建议在封装函数时把 +1 这一步放在一个显眼的位置,并加上注释,让维护的人不容易再踩。

4.3 非法日期与浏览器兼容性

new Date('2026-13-45')这种非法日期,不同浏览器行为不同。Chrome 通常返回 Invalid Date,Safari 在部分场景下可能解析失败直接返回 NaN,Firefox 也各有细微差异。你的函数必须具备“非空校验 + isNaN 拦截”的能力,才能在异常数据面前不崩。

另外注意padStart在旧版浏览器(IE 11)里不支持,如果你还在维护老项目,需要在入口处做兼容,或者干脆回退到手动补零逻辑。虽然 IE 已经退出历史舞台,但企业内网场景偶尔还会遇到老内核的 WebView,这个兼容性判断仍需保留到需求不再支持旧环境为止。

4.4 常见问题速查表

问题现象根本原因解决方案
输出月份总比实际少 1 个月getMonth()从 0 开始取值后 +1
每天早 8 点前日期显示成前一天toISOString()返回 UTC 时间改用getFullYear/getMonth/getDate取本地时间
输出NaN年NaN月NaN日传入非法日期字符串用isNaN(date.getTime())校验
数字没有补零,输出2026-5-8直接拼接数字,未做 padStart转字符串后padStart(2, '0')
date.getMonth().padStart is not a function数字类型没有 padStart 方法先String()再调用
getYear()返回 126 这样的值使用了废弃 API改用getFullYear()

5. 实用扩展:从“当前年月日”到完整时间处理

5.1 获取其他常用时间维度

掌握了getFullYear/getMonth/getDate这三个 getter 之后,稍微扩展一下就能获取完整的时间维度:

function getTimeParts(date = new Date()) { return { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), hour: date.getHours(), minute: date.getMinutes(), second: date.getSeconds(), weekday: ['日', '一', '二', '三', '四', '五', '六'][date.getDay()], }; } const t = getTimeParts(); console.log(`${t.year}年${t.month}月${t.day}日 星期${t.weekday} ${t.hour}:${t.minute}:${t.second}`); // 输出示例:2026年5月18日 星期一 14:30:25

这里注意getDay()返回的星期数值 0~6,0 对应周日,这跟中国习惯里“周一是一周第一天”不同。上面代码里用了一个映射数组['日', '一', '二', '三', '四', '五', '六'],下标 0 对应“周日”,日常使用没有问题。如果你的业务要求“周一”为一周第一天,就需要做取模转换:(date.getDay() + 6) % 7。

另外getHours/getMinutes/getSeconds同样不会自动补零,你可以复用前面padStart的思路:

const hour = String(date.getHours()).padStart(2, '0');

5.2 日期运算与时间戳互转

格式化只是日期处理的入口,实际开发中往往还需要“今天 + 3 天是哪天”这类运算。原生 Date 对象的核心玩法是把日期转成毫秒时间戳再做加减:

function addDays(date, days) { const result = new Date(date); result.setDate(result.getDate() + days); return result; } // 三天后的日期 const after3Days = addDays(new Date(), 3); console.log(formatDate(after3Days)); // 2026-05-21 // 一周前的日期 const lastWeek = addDays(new Date(), -7); console.log(formatDate(lastWeek)); // 2026-05-11

setDate会自动处理跨月和跨年。比如 5 月 31 日加 3 天,内部会自动进位到 6 月 3 日,不需要你手动判断“6 月有 30 天还是 31 天”之类的逻辑。这也是为什么原生 Date 虽然 API 设计得有些别扭,但处理日期运算的能力并不弱的底气所在。

获取当前时间戳则有两个常用方法:Date.now()返回当前时间戳毫秒数;new Date().getTime()返回新建实例的时间戳。二者的区别在于Date.now()是静态方法,无需创建实例,性能更好,日常获取当前时间戳优先用它。

5.3 月度第一天与最后一天

还有一个在报表类项目里非常高频的需求:某个月的第一天和最后一天。

function getMonthRange(year, month) { // month 是自然月,1~12 const firstDay = new Date(year, month - 1, 1); const lastDay = new Date(year, month, 0); // 下个月的第 0 天,就是本月最后一天 return { firstDay: formatDate(firstDay), lastDay: formatDate(lastDay), }; } console.log(getMonthRange(2026, 5)); // 输出:{ firstDay: '2026-05-01', lastDay: '2026-05-31' }

这里利用了Date构造函数一个不太起眼的特性:如果传入的日期参数为 0,会自动回退到上个月的最后一天。new Date(2026, 5, 0)里的月份是 5(代表 6 月),第 0 天就代表 5 月 31 日,或者理解为 6 月 1 日减一天。这段代码在计算“每月最后一天”时会自动处理大小月和二月,非常实用。

6. 从实践出发的几点体会

第一,日期格式化这种看似基础的功能,为什么值得专门写一篇文章?因为项目里见过太多因为时区问题、月份不 +1、忘记补零导致的线上事故。其实这事的核心就是“取值 + 格式化 + 校验”三步,但它考验的是你对 Date 对象底层机制的理解。把这个基础吃透,你之后处理复杂日期逻辑会少踩很多坑。

第二,我在实际开发里的惯用做法是:把formatDate函数放进一个独立的utils/date.js文件,项目里所有页面统一引用,而不是每个页面各自写一份格式化逻辑。这样一旦将来出现新的格式要求或必须修复某个时区 bug,只需要改一个文件。另外,我习惯在函数注释里写明“入参支持 Date 对象、时间戳、日期字符串”“输出为本地时区的年月日,不受 UTC 偏移影响”这两条关键信息,帮助后来维护的人快速理解函数行为。

第三,如果你的项目是从零开始,且未来大概率会涉及复杂日期处理,我建议安装 dayjs 但也不要放弃原生方案的练习——很多轻量场景下,原生几行代码就能解决问题,加载一个库反而显得冗余。工具库是备选项,不是必选项。

最后再分享一个小技巧:开发调试时,如果想看某个 Date 对象内部的时间戳和格式化结果,可以直接在控制台打印一个对象:

const d = new Date(); console.log({ timestamp: d.getTime(), localString: d.toString(), formatted: formatDate(d), });

这样一眼就能看到时间戳、标准字符串、目标格式三者的关系,排查问题效率会高很多。

返回列表