- 教程
- 文档
【免费下载链接】30-seconds-of-code
Coding articles to level up your development skills
导读
在原生 JavaScript 中处理日期经常让人头疼,但"获取某月的第一天 / 最后一天"这类高频需求,只需借助Date构造函数与月份零索引(zero-indexed)的特性,几行代码即可优雅解决。本文以 30-seconds-of-code 仓库中 first-last-date-of-month 文档为主线,深入讲解这两个日期工具函数的实现原理、day参数传0的巧妙技巧,并结合仓库内 days-in-month、is-leap-year 等姊妹文档,给出可直接复制使用的完整代码与实战扩展方案。读完本文,你将掌握一套不依赖任何第三方库的月份首末日期计算思路,并能将其自然迁移到日历组件、月度统计报表、月末对账等真实业务场景中。
先理解Date构造函数的两个关键特性
在动手写代码之前,先确认两个贯穿全篇的基础事实,它们是整个技巧成立的根基。
月份是从 0 开始计数的(zero-based)。在Date对象中,Date.prototype.getMonth()返回的 0 代表一月(January)、1 代表二月(February),以此类推,11 代表十二月。因此 first-last-date-of-month 中的代码用date.getMonth()取到的值必须原样传给构造函数,而不是先加 1。
Date构造函数会对超出合法范围的参数自动进位(overflow)。这是本技巧最核心的机制:
- 把
day设为0,会被解释为上个月的最后一天; - 把
month设为 12(例如getMonth() + 1后恰好为 12 且当天是 12 月),会被自动进位到下一年的 1 月; - 把
day设为32、-1等越界值,同样会被自动换算成相邻月份的合法日期。
仓库中 is-leap-year 也利用了这一机制:new Date(year, 1, 29).getMonth()在闰年返回1(仍停留在二月),在平年则因自动进位返回2(被归一为三月一日)。这证明"越界参数自动归一"是原生Date的稳定行为,可以放心依赖。
获取某月的第一天
给定任意Date对象,先用Date.prototype.getFullYear()和Date.prototype.getMonth()取出年份与月份,再创建一个新Date并把日期固定为 1,即可得到该月第一天:
const firstDateOfMonth = (date = new Date()) => new Date(date.getFullYear(), date.getMonth(), 1); firstDateOfMonth(new Date('2015-08-11')); // '2015-08-01'几点补充说明,让这段代码在实战中更稳妥:
- 默认参数:
date = new Date()让函数在没有传参时自动作用于"当前月份",符合 30-seconds-of-code 系列工具函数"开箱即用"的一贯风格; - 返回全新对象:
new Date(...)每次都创建一个新的Date实例,不会修改调用方传入的原始日期对象,避免了 add-minutes-hours-days-to-date 中反复强调的"setTime会原地修改对象"这类隐患; - 时间分量归零:由于构造函数只接收年、月、日三个参数,返回的日期对象时分秒会被重置为
00:00:00,非常适合直接用于日期范围过滤或数据库查询的起止边界。
获取某月的最后一天
获取最后一天无法靠"数日子"硬算,因为每个月天数不同(28、29、30、31 天不等)。first-last-date-of-month 给出的解法是一个巧妙技巧:
不把日期设为 1,而是设为0,就会得到上个月的最后一天;为此需要先把月份加 1。
const lastDateOfMonth = (date = new Date()) => new Date(date.getFullYear(), date.getMonth() + 1, 0); lastDateOfMonth(new Date('2015-08-11')); // '2015-08-31'拆解执行过程(以2015-08-11为例):
getFullYear()得到2015,getMonth()得到7(八月);month参数传7 + 1 = 8,即"九月";day参数传0,构造函数将其自动解释为九月的前一天,即2015-08-31。
这套逻辑对任意月份天然成立:10 月传入new Date(y, 10, 0)得到 9 月 30 日,2 月传入new Date(y, 2, 0)得到 2 月的最后一天(平年 28 日、闰年 29 日,由构造函数自动计算)。因此无需自己维护"每月天数表",也不用关心闰年判断——这正是该技巧的价值所在。
需要留意的是,"设为 0"技巧与"获取上个月最后一天"在语义上是同一件事,但用途不同:本函数通过getMonth() + 1把指针推进到下个月再回退一天,最终锁定的是当前月的最后一天。这与 date-yesterday-today-tomorrow 中用getDate()/setDate()增减天数属于同一类"利用越界自动进位"的日期运算思路,理解其一即可触类旁通。
技巧的延伸:一行代码算出某月有多少天
掌握了day = 0的语义后,计算"某个月有多少天"就变成了顺理成章的副产品。仓库姊妹文档 days-in-month 直接给出了单行实现:
const daysInMonth = (year, month) => new Date(year, month, 0).getDate(); daysInMonth(2020, 12); // 31 daysInMonth(2024, 2); // 29其原理与lastDateOfMonth完全同构:new Date(year, month, 0)得到的是本月第一天前一天,即上个月最后一天的时间戳,而Date.prototype.getDate()返回的就是这个日期的"几号",恰好等于上个月的天数;又因为月份零索引,month传入的值就是目标月的索引,例如2024, 2表示三月,day=0回退到二月最后一天29,从而自动识别出 2024 年 2 月是 29 天的闰年二月。
[!TIP]
如果你的入参是"某月某日"的
Date对象而非(year, month)组合,可以直接复用lastDateOfMonth的结果配合getDate():const daysInMonthFromDate = date => lastDateOfMonth(date).getDate(); daysInMonthFromDate(new Date('2015-08-11')); // 31
结合仓库源码验证:本篇在项目中的真实定位
该文档并非孤立存在,其在仓库中的组织方式进一步印证了内容的可靠性:
- 内容演进痕迹:在 redirects.yaml 中可以看到
/js/s/last-date-of-month以301状态重定向到/js/s/first-last-date-of-month,说明仓库曾把"获取最后一天"拆分为独立条目,后合并进本篇,本文是两个技巧合一的权威版本; - 集合归属:本篇属于 JavaScript Date 集合(
slug: js/date,通过tagMatcher: date自动聚合),与 date-comparison、date-difference、min-max-date 等构成一套完整的原生日期处理工具箱; - 与其他工具函数的组合潜力:仓库中的 date-range-generator 用
setDate(getDate() + step)逐日递增生成日期区间,将其与firstDateOfMonth/lastDateOfMonth组合,即可生成"整个自然月每一天"的序列,用于日历渲染或月度数据补全。
实战组合:构建"本月完整日期列表"
把前文知识组装起来,可以得到一个完整的月度工具函数集。以下代码综合运用了firstDateOfMonth、lastDateOfMonth与 date-range-generator 中的生成器思路:
// 1. 获取某月首末日期(默认当月) const firstDateOfMonth = (date = new Date()) => new Date(date.getFullYear(), date.getMonth(), 1); const lastDateOfMonth = (date = new Date()) => new Date(date.getFullYear(), date.getMonth() + 1, 0); // 2. 逐日遍历某月所有日期(复用时保证不污染原对象) const dateRange = function* (start, end, step = 1) { for ( let d = new Date(start); d <= new Date(end); d.setDate(d.getDate() + step) ) yield new Date(d); }; // 3. 输出本月全部日期 const monthDates = (date = new Date()) => [...dateRange(firstDateOfMonth(date), lastDateOfMonth(date))]; monthDates(new Date('2015-08-11')); // [2015-08-01, 2015-08-02, ..., 2015-08-31]在真实项目中,这类组合可以直接支撑:
- 日历组件:以
firstDateOfMonth为起点、lastDateOfMonth为终点铺满网格; - 月度报表:以首日为查询起点、末日 23:59:59 为查询终点,圈定无遗漏的数据范围;
- 月末结算:
lastDateOfMonth天然适配"当月最后一天到期/对账"等业务规则; - 日期差值统计:配合 date-difference 中的差值算法,可计算月份跨度。
注意事项与边界
使用上述技巧时有几点需要铭记,以免在生产环境中踩坑:
- 本地时区语义:
new Date(year, month, day)按运行环境本地时区解析。在中国时区(UTC+8)下通常无碍,但若服务器运行在 UTC,toISOString()输出的字符串可能比预期早/晚一天。需要跨时区稳定输出时,应改用Date.UTC()组合toISOString(),或按 date-comparison 中介绍的方式统一比较基准。 - 默认参数的副作用:
firstDateOfMonth()不传参时返回的是"当前月份"的首日,而 date-yesterday-today-tomorrow 中提醒过,Date对象是可变的。若下游代码在结果上调用setDate()等方法,会改变该实例,建议按需对返回值做一次浅拷贝。 - 月份零索引的换算:若你的数据源(如数据库返回)采用 1–12 表示月份,传给
new Date()前必须减 1,否则会出现月份偏移一整月的隐性错误。
总结
原生 JavaScript 的Date对象虽然 API 不够友好,但其"月份零索引 + 越界参数自动进位"两大特性,让"获取某月第一天和最后一天"这类任务有了极其简洁的解法:new Date(y, m, 1)与new Date(y, m + 1, 0)。以此为基石,days-in-month 中的天数计算、is-leap-year 中的闰年判断、date-range-generator 中的整月遍历,都能在不引入任何第三方日期库的前提下顺畅实现。把这几个十几行的工具函数收进你的代码库,日后的日历、报表与对账逻辑都会变得更加干净利落。
- 教程
- 文档
【免费下载链接】30-seconds-of-code
Coding articles to level up your development skills
相关推荐
30-seconds-of-code 实战:用一行 JavaScript 代码计算任意月份的天数
30 seconds of code 实战:用一行 JavaScript 代码计算任意月份的天数 导读 在 JavaScript 中处理日期向来不算直观,但"计
教程文档30-seconds-of-code:用数组解构提取 JavaScript 数组最后一个元素
30 seconds of code:用数组解构提取 JavaScript 数组最后一个元素 数组解构(destructuring assignment)是 E
教程文档30 Seconds of Code:用 slice 与 filter 取 JavaScript 数组的第 n 个元素
30 Seconds of Code:用 slice 与 filter 取 JavaScript 数组的第 n 个元素 数组按索引取值是 JavaScript
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考