☰
JavaScript获取当前年月日:避开getMonth与时区的那些坑
2026/9/30 10:15:25 网站建设 项目流程

你没看错,一个简单的“JS获取当前年月日”,居然能在项目里翻车。前两周我还在帮同事查一个线上 bug:表单里默认时间是“2025-9-5”,用户没手动改就提交,后端解析时以为中间是两位月份,结果日期错乱。你说这算程序员的疏忽吗?是,也不是。因为 JavaScript 里getMonth()返回的是从 0 开始计数的索引,9 月实际上是8,而不少新手甚至老手,都会在字符串拼接时漏掉这一层转换。再加上月份和日期如果是单数,还要考虑补零,补零的方式又五花八门。这篇文章不聊框架,不聊 Vue/React 的封装,就老老实实把 JavaScript 获取当前年月日、输出YYYY-mm-dd和YYYY年mm月dd日这件事讲透——包括每种写法的原理、隐藏的边界情况、项目里真正该注意的坑,我会按自己的实操经验一步步拆给你看。

适用读者:刚接触 JavaScript 的前端新手、写工具函数想复用一段时间的开发者、以及对日期处理“总觉得哪里不对但说不清”的中间水平同学。如果你已经熟练到闭眼能写出toISOString().slice(0,10),也不妨读读时区那段,说不定能帮你规避几个线上隐患。

1. 先拆开 Date 对象:为什么 getMonth() 会少 1 个月

1.1 四个“老演员”:getFullYear、getMonth、getDate、getDay

JavaScript 里所有日期相关操作,追根溯源都来自内置的Date对象。创建一个表示当前时刻的实例很简单:

const now = new Date(); console.log(now); // 输出类似:Thu Jan 11 2025 09:30:00 GMT+0800 (中国标准时间)

这个now对象身上带着年月日时分秒、毫秒、星期几、时区等一堆信息。我们最常用的四个方法分别是:

  • getFullYear():返回四位数的年份,比如 2025。注意不是getYear(),getYear()实际返回的是“当前年份减 1900”,早就被弃用了,别被老代码带沟里。
  • getMonth():返回月份,范围是 0-11,也就是 0 代表 1 月,11 代表 12 月。
  • getDate():返回月份中的第几天,范围 1-31。这个没什么幺蛾子,但注意方法名是getDate,不是getDay。
  • getDay():返回星期几,范围 0-6,0 是周日。这个今天用不到,但经常被拿来和getDate()混淆,我顺便提一嘴。

我见过无数次这种情况:

// 错误示范 const month = now.getMonth(); // 如果今天是 9 月,返回的是 8 console.log(`现在是 ${month} 月`); // 输出:现在是 8 月

只要没做+ 1,那么 9 月 1 日输入到表单里就会变成 8 月 1 日,全部错位一个月。轻则表单显示不对,重则业务统计月报对不上,这种情况在月初尤其隐蔽,因为 1 月的返回值是 0,反而不会引起注意。

1.2 一个“看起来没问题”的错误示例

再给大家看一个更阴间的写错姿势。有些人知道要加 1,也写了补零,但还是错了:

const now = new Date('2025-01-05T00:00:00'); function formatDateWrong(date) { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate() + 1; // 这里画蛇添足 return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; } console.log(formatDateWrong(now)); // 2025-01-06

看到没有,问题出在getDate() + 1。有些同学可能把“月份索引从 0 开始”这件事记混了,连带着觉得“日期也要加 1”,结果就是 5 号变成 6 号。这个错误,如果不配合日期断言测试,根本发现不了,还特别容易在月末、季度末这种时间节点酿成大祸。

所以在真正写封装之前,先把Date这几位“老演员”的脾气摸清楚:年份四位数直接拿,月份必须加 1,日期按字面拿,星期索引另算。这样后面写格式化函数时,你才不会被突如其来的“偏移 1”搞崩溃。

2. 两种目标格式的完整实现:从字符串拼接到防坑封装

2.1 最容易理解的字符串拼接版

既然要输出YYYY-mm-dd和YYYY年mm月dd日,最朴素也最直观的思路就是先把年月日分别取出来,再用模板字符串拼起来:

function formatDateSlash(date = new Date()) { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); return `${year}-${month}-${day}`; } function formatDateChinese(date = new Date()) { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); return `${year}年${month}月${day}日`; } console.log(formatDateSlash()); // 2025-9-5 console.log(formatDateChinese()); // 2025年9月5日

到这里已经能跑了。但你想过没有——标题里写的格式是YYYY-mm-dd,也就是月份要两位,2025-09-05而不是2025-9-5。上面这版直接拼接,单数月份和单数日期都不会补零。如果只是给人看,2025年9月5日完全没问题,中文习惯本来就不要求补零。但如果是给机器看、存数据库、做日期字符串比较,2025-9-5就统一不了,你还要在别处再判断长度。所以接下来必须处理补零。

2.2 用 padStart 做两位数补零:为什么它是最稳妥的

字符串方法padStart是从 ES2017 开始普及的,作用是让一个字符串填充到指定长度。它接收两个参数:目标长度和填充字符。

'5'.padStart(2, '0'); // '05' '12'.padStart(2, '0'); // '12' '2025'.padStart(2, '0'); // '2025'(长度已经超过 2,不生效)

用它做单数月和单数日期补零,几乎是当前最优雅的方案。因为月份最大 12,日期最大 31,目标长度都是 2,填充字符是'0',天然合适:

function padZero(num) { return num.toString().padStart(2, '0'); } function formatDateWithDash(date = new Date()) { const year = date.getFullYear(); const month = padZero(date.getMonth() + 1); const day = padZero(date.getDate()); return `${year}-${month}-${day}`; } function formatDateChineseCN(date = new Date()) { const year = date.getFullYear(); const month = padZero(date.getMonth() + 1); const day = padZero(date.getDate()); return `${year}年${month}月${day}日`; } console.log(formatDateWithDash()); // 2025-09-05 console.log(formatDateChineseCN()); // 2025年09月05日

这里有个小分叉:YYYY年mm月dd日这种格式,月份和日期到底该不该补零?我翻了大量国内站点,两种写法的支持者都有。但既然格式定义里写明了mm和dd,我倾向于补零。原因很简单——格式的意义就是长度统一,否则你没法保证后续做字符串排序或者正则匹配时不会出问题。你可以在项目内部统一定义,但最好一开始就跟格式描述保持一致。

另外提一个更老的补零技巧:('0' + month).slice(-2)。它在 ES5 时代很流行,现在也能用。原理是把字符串左边拼一个0,再从右边截两位。比如月份是 5,('05').slice(-2)得到'05';月份是 12,('012').slice(-2)得到'12'。判断逻辑完全均匀,没有任何边界问题。如果你的项目还在兼容非常老的环境,或者你干脆想炫技,这种写法仍然成立。

const month = String(date.getMonth() + 1); const paddedMonth = ('0' + month).slice(-2);

但坦白讲,项目里没有硬性兼容 IE 的话,我用padStart更多,语义更清晰,别人接手代码时一眼就能读懂意图。

2.3 把两种格式合并:一次封装,自由切换

实际工作中,你可能不止需要两种固定格式。比如接口要求YYYY-mm-dd,页面展示要求YYYY年mm月dd日,还有导出 CSV 时要求YYYY/mm/dd。与其写三个几乎一样的函数,我习惯封装成一个带格式化参数的通用函数:

function formatDate(date = new Date(), pattern = 'YYYY-mm-dd') { const year = date.getFullYear(); const month = padZero(date.getMonth() + 1); const day = padZero(date.getDate()); return pattern .replace('YYYY', year) .replace('mm', month) .replace('dd', day); } console.log(formatDate(new Date(), 'YYYY-mm-dd')); // 2025-09-05 console.log(formatDate(new Date(), 'YYYY年mm月dd日')); // 2025年09月05日 console.log(formatDate(new Date(), 'YYYY/mm/dd')); // 2025/09/05

这个封装的巧妙之处在于,它没有用switch去枚举所有格式,而是利用字符串替换把模式里的占位符按顺序换掉。只要pattern里包含YYYY、mm、dd这三个占位符,任意组合都能输出。以后产品有新格式要求,比如YYYY年dd日mm月(虽然不太可能),你也不需要动函数体,只传新的 pattern 就行。

不过要提醒一句:.replace不加正则全局标志时,只替换第一个匹配项。所以模式里别出现两个YYYY或两个mm,否则第二个不会被替换。如果要支持YYYY年mm月dd日这种正常场景,完全没问题。真要支持重复占位符,那就需要replaceAll或者正则/YYYY/g写法:

return pattern .replaceAll('YYYY', year) .replaceAll('mm', month) .replaceAll('dd', day);

replaceAll是 ES2021 的方法,现代浏览器和 Node.js 15+ 都支持。从我的经验看,字符串替换版本已经足够应付 90% 以上的项目需求,不需要再引入额外依赖。

3. 时区、UTC、本地时间:getFullYear 系列背后的大坑

3.1 为什么toISOString().slice(0, 10)可能在下午 8 点变成“明天”

网上流传着一种极简写法:

const today = new Date().toISOString().slice(0, 10); // 得到 '2025-09-05'

原理是toISOString()会把当前时间转换为 ISO 8601 格式的 UTC 字符串,类似2025-09-05T08:30:00.000Z,然后从开头截 10 个字符,正好是YYYY-MM-DD。这段代码在纯前端、位于 UTC+8 时区的中国浏览器里,绝大多数情况运行正常,但有个隐藏雷点:

  • toISOString()永远返回UTC 时间,而不是用户的本地时间。
  • 假设你在北京时间(UTC+8)的晚上 20:00 运行new Date(),本地时间其实是2025-09-05 20:00,但 UTC 时间才2025-09-05 12:00,截出来的日期还是 9 月 5 日,没问题。
  • 可如果你在晚上 23:30 运行,本地时间2025-09-05 23:30,UTC 时间则是2025-09-05 15:30,也没问题。
  • 但是!假设你在UTC+8 时区的凌晨 6:30运行,本地时间2025-09-05 06:30,UTC 时间却是2025-09-04 22:30,截出来就是2025-09-04——日期整整倒退一天。

同理,你在西半球,比如 UTC-5 的纽约,晚上 20:00 运行这段代码,UTC 时间已经是第二天的凌晨 01:00,截出来就是“明天”的日期。

所以toISOString().slice(0, 10)这个写法本身没问题,问题是它代表的是 UTC 日期,不是用户所在时区的本地日期。中国用户还好,只有凌晨那几个小时会出偏差,但如果你面向全球用户,或者你的服务器日志用这段代码记录日期,那排查问题时会怀疑人生。

3.2 getFullYear 系列为什么没有这个问题

getFullYear()、getMonth()、getDate()这一组方法,返回的都是基于本地时区的值。也就是说,new Date()内部的时刻值包含了时间戳,当你调用getFullYear()时,JavaScript 会先根据运行时环境(通常是操作系统)的时区设置,把这个时刻换算成本地日历时间,再返回年份。

举例:

  • 北京时间 2025-09-05 06:30,本地时刻的new Date().getFullYear()返回2025。
  • 同一个时刻,在纽约时区,如果你在本地运行new Date(),得到的也是本地时间,可能已经是 2025-09-04 的时候了(因为时区差),getFullYear()返回2025,但如果跨年边界,它就返回2024。

所以这两类 API 的本质区别是:

API 系列基准时区适用场景
getFullYear()/getMonth()/getDate()运行时本地时区业务展示、表单提交、本地日期逻辑
getUTCFullYear()/getUTCMonth()/getUTCDate()UTC 通用协调时服务器时间戳、日志、跨时区对比
toISOString()/toJSON()UTC网络传输、存储标准格式

如果你的目标是“用户看到的日期就是本地日期”,那安心用getFullYear()系列,这篇文章后面的代码也都是基于这一组方法。如果后端接口和前端约定用 ISO 标准时间传输,那toISOString()反而是更好的选择,只是要清楚它的 UTC 属性,别误拿来做表单默认值。

3.3 夏令时会不会影响日期格式化?

夏令时(DST)主要影响的是“当天的小时数”,比如某些地区春夏时会少一个小时,导致当天只有 23 小时。但注意,getFullYear()、getMonth()、getDate()这些方法反映的是日历日期,不受“一小时偏移”的影响。因为Date对象在内部存储 UTC 时间戳,而本地化方法在执行时已经做了时区换算。夏令时只会让“当天有多少小时”变化,不会让日期本身跳变。唯一需要小心的是,夏令时切换日的午夜可能有 00:30 之类的时间偏移,但如果你只取年月日,不取时分秒,基本不会碰到这个坑。

4. 我踩过的坑:从“表单默认值自动变前一天”到“月份没补零导致后端报错”

4.1 排查链路一:表单默认日期的时区陷阱

有一年我维护一个后台管理系统,用户反馈“新增订单时,订单日期默认显示的是昨天”。我第一反应是代码里哪里写死了减一天,结果翻遍了整个表单组件都没找到。后来我仔细看初始值:

const orderDate = new Date().toISOString().slice(0, 10); this.formData.order_date = orderDate;

表面看没有任何问题,对吧?但用户环境是北京时间,而且他们在晚上 8 点以后打开表单。按照前面讲的时区逻辑,21:00 的 UTC 时间是当天 13:00,还没跨日;但23:00 的 UTC 时间是当天 15:00,也还没跨日。等等,那什么时候会变昨天?我重新算了一下:北京时间凌晨 0 点到 7:59 之间,UTC 时间还在前一天。用户通常不会凌晨 7 点前去打开后台,所以这个 bug 理论上不太容易复现。

但我还是决定不赌,直接把代码改成getFullYear()系列。改为本地时间后,不管几点打开,日期都是当天的。这种问题不出现则已,一出现就是大规模反馈,而且非常难定位根源。后来我在团队里立了个规矩:业务系统里凡是“用户视角的日期”,一律不准用toISOString()取年月日,全用本地时间方法。

如果你接手了类似代码,可以先在控制台跑一下:

console.log(new Date().toISOString().slice(0, 10)); console.log(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate()).padStart(2, '0')}`);

对比输出,再结合当前时区判断,就能确认是不是时区问题。

4.2 排查链路二:从getMonth()少一个月到埋点数据错月

另一个项目里的问题更隐蔽:数据看板里“本月新增用户数”总是比预期少一点。我一开始以为 SQL 写错了,后来查到这里:

const startOfMonth = `${new Date().getFullYear()}-${new Date().getMonth()}-01`;

注意,这里没有+ 1。也就是说,9 月 15 日运行时,startOfMonth是2025-8-01,等于把整个 8 月当成“本月”的开头,拉取的数据自然不对。这种 bug 在高流量的仪表盘页面上,会直接影响业务决策,比表单展示严重得多。排查过程倒不复杂:在控制台打印new Date().getMonth(),发现输出是8,再对照文档确认索引从 0 开始,所有疑惑瞬间就通了。

这个坑给我留下的教训是:凡是涉及月份的输出,第一件事写注释// getMonth() 从 0 开始,所以 +1。别小看这一行注释,两个月后再看代码,你十有八九会感谢它。

4.3 排查链路三:字符串比较时2025-9-30排在2025-10-01后面

还有一个我记忆犹新的案例,是关于“补零”的。那时项目里有一个日期排序功能,前端把日期字符串交给后端排序。本来以为后端能处理,结果发现某几个订单排序总是不对。查下来发现,前端提交的日期字段是2025-9-30这种非补零格式,后端按字符串排序,'2025-9-30'和'2025-10-01'比较时,因为'9'的字符编码比'1'大,所以 9 月 30 日反而排在 10 月 1 日后面。

解决方法就是前端统一用padStart补零,让所有日期字符串都是定长的YYYY-MM-DD。这个案例说明一个道理:格式定义里的mm、dd不是摆设,它是为了让字符串排序、正则匹配、存储统一而存在的。如果你输出的日期长度不一,后续处理方很可能埋雷。

5. 在真实项目里怎么落地:默认值、日志、上报、校验

5.1 表单默认值:用户看到今天就是今天

说回开头那个表单场景,正确的做法是把上面封装的函数放进一个公共utils/date.js文件:

// utils/date.js export function padZero(num) { return String(num).padStart(2, '0'); } export function getTodayDash(date = new Date()) { return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())}`; } export function getTodayChinese(date = new Date()) { return `${date.getFullYear()}年${padZero(date.getMonth() + 1)}月${padZero(date.getDate())}日`; }

然后在组件里这样用:

import { getTodayDash } from '@/utils/date'; this.formData.date = getTodayDash();

这段代码没有任何时区依赖,用户在任何时区打开这个页面,看到的都是他自己的本地日期。对表单类需求来说,这是最符合直觉的表现。

5.2 日志时间戳:毫秒级唯一标识与日期分开

如果你写前端日志埋点,需要给每一条日志打时间戳,我建议把“日期”和“完整时间戳”分开处理。日期用getTodayDash(),完整时间戳用Date.now()或者new Date().toISOString()传给后端。为什么这样分?因为日志排查经常需要按天分目录,一个纯日期字段可以方便地做聚合;而完整时间戳保留着毫秒级精度,能精确到单条请求的前后顺序。两者混在一个字符串里虽然也能看,但查询时往往还要再 split,反而麻烦。

const logPayload = { date: getTodayDash(), timestamp: new Date().toISOString(), message: 'user clicked submit' };

我之前碰到过一个日志平台,它的索引按天滚动,如果前端上报的date字段不是严格YYYY-MM-DD格式,会生成错误的索引分区,查日志时怎么都查不到当天记录。最后回溯才发现是前端某个地方用了new Date().toLocaleDateString('zh-CN'),在 Chrome 下输出的是"2025/9/5",斜杠分隔、没有补零,完全不符合日志平台约定。这类问题不是“跑不跑得通”,而是“隐式地不兼容”,最容易坑到人。

5.3 跨时区对比:什么时候用 ISO,什么时候用本地日期

后端如果要求你这样传:

{ "date": "2025-09-05T16:00:00.000Z" }

这就不是年月日展示问题了,而是完整的时间点。这时候前端可以直接用new Date()传给后端,后端自行转换时区。如果你的业务是按“用户本地日期”去查询某一天的数据,那后端应该接收YYYY-MM-DD,并且接口文档里明确“该字段代表用户本地日期”。在这个前提下,前端用getTodayDash()完全合理。反之,如果你要记录“操作发生的绝对时间点”,请用 ISO 字符串,不要用本地日期拼出来的字符串。

我建议在任何传日期的接口旁边写清楚语义,哪怕只是注释:

// 这个字段表示订单创建当天的用户本地日期,不是 UTC orderDate: getTodayDash()

这种细节,是避免“前后端互相甩锅”的良药。

6. 从格式化到日期工具库:该不该用 dayjs 或 date-fns?

6.1 只做一件事,原生就够

如果只是获取当前年月日、转两种格式,我个人不会引入任何第三方库。原因很简单:原生Date的三四个方法足以实现,而且代码短、无依赖、不会出现库版本升级导致的 breaking change。打包体积也少那么几十 KB,对首屏性能有一点帮助。

但如果你的项目和日期打交道特别多——比如要做日期加减、两个日期相差天数、每月的第几个星期几、按周聚合、农历转换——那我建议直接上dayjs。它体积只有 2KB 左右(gzip 后),API 与moment.js高度一致,但不再有moment.js那种“全部打包进来”的沉重感。

示例:

import dayjs from 'dayjs'; dayjs().format('YYYY-MM-DD'); // 2025-09-05 dayjs().format('YYYY年MM月DD日'); // 2025年09月05日 dayjs().subtract(7, 'day').format('YYYY-MM-DD'); // 上周同一天

dayjs内部也严格遵循本地时区,format('YYYY-MM-DD')不会出现 UTC 偏移问题。所以如果你要频繁做日期运算,它确实是更省心的选择。但注意,dayjs的format并不在默认核心包里?不对,其实默认核心包已经包含基础格式化能力,不需要额外插件。真正需要插件的是weekday、advancedFormat、duration这些扩展能力。

6.2 我的取舍经验:三行原生函数远好过一百行“通用工具”

有些团队喜欢把日期格式化函数做成一个超大的formatDate(date, 'YYYY年MM月DD日 HH:mm:ss')万能方法,支持各种占位符。做得好确实好用,但做不好就变成“需求一变不敢动”的代码。

我自己经历过的最糟糕的版本,是一个大约 200 行的格式化工具,支持YY、YYYY、M、MM、D、DD、H、HH、m、s等十几种占位符,还支持相对时间输出。最后项目交接时,新同事完全不敢改,因为每个分支相互嵌套太多。后来我把它拆成三四个简单函数:getTodayDash()、getTodayChinese()、formatTimestamp(),每个函数心智负担都低,反而用得更顺手。

所以我的建议是分几步走:

  1. 项目初期,只要getTodayDash()和getTodayChinese()两个函数,精确满足当前需求。
  2. 需求扩展后,出现多种模式时,再引入带 pattern 参数的formatDate(),仿照上面的字符串替换版本。
  3. 需要大量日期运算时,才考虑dayjs或date-fns,但用法要统一封装到utils/date.js里,避免业务代码到处引。

6.3 一个容易被忽略的隐藏接口:Intl.DateTimeFormat

最后分享一个我最近偏爱的现代写法,它也不依赖第三方库,而是利用 JavaScript 内置的国际化 API:

function getTodayDash(date = new Date()) { const parts = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', }).formatToParts(date); const map = {}; parts.forEach((part) => { if (part.type !== 'literal') map[part.type] = part.value; }); return `${map.year}-${map.month}-${map.day}`; }

这里借助Intl.DateTimeFormat的能力,让浏览器根据 locale 和指定选项来格式化和分割日期。它会自动补零,也严格使用本地时区。好处是不用手动padStart,缺点是对新手而言formatToParts的返回值结构需要一点理解成本。我在项目里常用它处理“多语言格式化需求”,因为只需要替换 locale 字符串,就能拿不同的本地化输出,比写一堆替换规则更可靠。

对比一下四种常见输出的差异:

实现方式输出示例时区基准推荐场景
原生getFullYear()拼接2025-09-05本地业务表单、展示
padStart补零封装2025年09月05日本地要求定长字符串
toISOString().slice(0,10)2025-09-04UTC服务器时间、日志标准格式
Intl.DateTimeFormat2025-09-05本地多语言国际化

这几条路没有绝对的对错,关键是团队约定和接口契约。无论如何,我建议把日期格式化收敛在一个文件里,所有同事共同使用,而不是每个页面写一遍自己的拼接逻辑——这是我踩过无数坑之后,最想对你说的经验。


我自己在实际业务里写日期格式化最频繁的场景反而不是输出到页面上,而是给生成的文件命名、给批量任务生成批次号。比如导出报表时,文件名往往会拼上日期,这时候同一个函数就能派上用场:

const fileName = `订单数据_${getTodayDash()}.xlsx`; // 订单数据_2025-09-05.xlsx

这类需求看起来不起眼,但一个稳定、无时区坑、定长格式的日期函数会让它们变得极其省心。如果你再往深走一步,可以把getTodayChinese()用在页面标题、浏览器document.title里,直接展示给用户看。代码都不长,但细节里的坑是实打实的。希望这篇整理能帮你避开我当年走过的弯路。

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

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

立即咨询