本文详细讲解如何使用轻量级库 Day.js 简化 JavaScript 日期与时间的加减计算,对比原生 Date 对象的痛点,深入掌握 add 与 subtract 语法、链式操作、月末与闰年边界智能处理及常见避坑指南。
怎么使用 day.js 简化 JavaScript 日期加减运算
时间:2026-11-03
浏览:144
文章目录
在日常 JavaScript 开发中,日期加减是非常高频的业务需求,例如在数据看板中筛选“过去 7 天”、为订单设置“30 分钟支付倒计时”,或是为用户续期“1 年会员资格”。原生 JavaScript 提供的 Date 对象虽然内置了时间戳计算与取值方法,但在处理加减运算时非常容易引发隐蔽的业务 Bug:不仅原变量会被原地篡改(In-place Mutation),而且在跨月运算时极易因月份天数不同而发生“月末越界溢出”。
解决这一问题的最佳实践是引入仅约 2KB 的轻量日期库 Day.js。Day.js 采用了类似 Moment.js 的直观 API 设计,但全面拥抱了函数式编程的“不可变设计”(Immutable)。通过调用 .add() 和 .subtract() 方法,你可以安全、精确地按天、周、月、年或时分秒进行链式增减,同时借助其内置的月末智能对齐机制彻底规避跨月计算陷阱。本文将从原生痛点剖析、环境引入、核心加减语法、链式操作、月末边界处理到常见错误排查,完整讲解如何在项目中高效落地 Day.js 日期加减运算。
一、原生 Date 的计算痛点与 Day.js 的解决之道
为了深刻理解 Day.js 带来的便利,有必要先审视原生 Date 在做加减运算时的三大核心痛点:
- 原地修改产生数据副作用:原生
Date对象的setDate()、setMonth()等方法是破坏性的(Mutable)。一旦在某个函数中执行了加减,所有引用该日期变量的地方都会同步改变,这在 React 状态管理、Vue 响应式数据或公共工具函数中极易引发数据漂移。 - 单位换算冗长且容易出错:如果通过毫秒戳
getTime()来手动做加减,开发者必须手动编写1000 * 60 * 60 * 24这类乘法算式,代码不仅可读性差,而且在面对跨夏令时(DST)或自然月天数不固定(28至31天)时完全无能为力。 - 月末越界自动溢出至下下月:原生
setMonth()在遇到源月份是大月(31天)而目标月份天数较少(如 2 月)时,会自动把超出天数累加至下下个月。例如 1 月 31 日加 1 个月,原生 Date 会计算为 3 月 2 日或 3 日,严重违反业务直觉。
Day.js 正是为彻底解决这些问题而生。它保证所有加减运算均返回全新的 Day.js 实例,原对象始终保持只读安全;同时提供统一的语义化参数,支持任意时间维度的加减,并内置了边界自动修正机制。
二、准备工作:引入 Day.js 与创建基准实例
Day.js 是一个零依赖的极简库,可以在浏览器端和 Node.js 环境中开箱即用。你可以根据当前项目的技术栈选择合适的安装方式。
1. 在工程化项目中安装与导入
在 npm、Yarn 或 pnpm 项目中,执行以下安装命令:
# 使用 npm 安装
npm install dayjs
# 或者使用 yarn 安装
yarn add dayjs
# 或者使用 pnpm 安装
pnpm add dayjs
在代码中支持 ES Module 规范或 CommonJS 规范导入:
// ES Module 方式(推荐用于 Vue、React、Vite 项目)
import dayjs from 'dayjs';
// CommonJS 方式(推荐用于传统 Node.js 服务端)
const dayjs = require('dayjs');
2. 在浏览器原生环境中通过 CDN 引入
如果不使用打包工具,可直接在 HTML 页面中使用 <script> 标签从公共 CDN 加载,加载后全局变量 window.dayjs 即可直接使用:
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
<script>
const now = dayjs();
console.log(now.format());
</script>
3. 创建基准日期实例
进行加减运算前,首先需要创建一个 Day.js 包装对象作为运算基准。Day.js 支持多种初始输入源:
// 1. 以当前本地时间创建实例
const current = dayjs();
// 2. 从 ISO 日期字符串或标准日期字符串解析
const specificDate = dayjs('2026-11-01');
// 3. 从毫秒时间戳解析
const fromTimestamp = dayjs(1761955200000);
// 4. 包装既有的原生 JS Date 对象
const fromNative = dayjs(new Date());
三、核心语法:使用 add 与 subtract 执行日期加减
Day.js 提供了两个核心方法来处理日期的增加与减少:.add(value, unit) 和 .subtract(value, unit)。它们的语法规则非常直观统一。
1. 方法签名与时间单位对照
两个方法的签名定义如下:
dayjsInstance.add(value: number, unit: string): Dayjs
dayjsInstance.subtract(value: number, unit: string): Dayjs
其中 value 为要加减的数值(整数),unit 为时间单位。Day.js 同时支持全称与简写,单位名称是不区分大小写的,但简写中的月份大写 'M' 与分钟小写 'm' 具有严格区分。下表列出了常用的时间单位映射:
| 时间维度 | 完整单位参数 (全称) | 常用简写 (Shorthand) | 应用场景举例 |
|---|---|---|---|
| 年 | 'year' / 'years' |
'y' |
计算会员年费到期、周年纪念日 |
| 月 | 'month' / 'months' |
'M' (严格大写) |
月度账单日推算、季度报表 |
| 周 | 'week' / 'weeks' |
'w' |
周计划、7天统计周期 |
| 日 / 天 | 'day' / 'days' |
'd' |
倒计时、履约延期、数据看板跨度 |
| 小时 | 'hour' / 'hours' |
'h' |
工单响应 SLA、时限超时检测 |
| 分钟 | 'minute' / 'minutes' |
'm' (严格小写) |
验证码有效时限、支付倒计时 |
| 秒 | 'second' / 'seconds' |
's' |
短定时器校准、精确日志标记 |
| 毫秒 | 'millisecond' / 'milliseconds' |
'ms' |
微秒级性能基准计算 |
2. 基础加减运算代码示例
以下示例展示了天数增加、月份扣减及时间顺延的完整调用:
import dayjs from 'dayjs';
const base = dayjs('2026-11-01 10:00:00');
// 1. 增加 7 天
const after7Days = base.add(7, 'day');
console.log(after7Days.format('YYYY-MM-DD HH:mm:ss'));
// 2. 减去 2 个月(使用简写 'M')
const before2Months = base.subtract(2, 'M');
console.log(before2Months.format('YYYY-MM-DD HH:mm:ss'));
// 3. 增加 45 分钟(使用简写 'm')
const after45Minutes = base.add(45, 'm');
console.log(after45Minutes.format('YYYY-MM-DD HH:mm:ss'));
// 4. 传负数参数:add(-3, 'd') 等价于 subtract(3, 'd')
const minusViaAdd = base.add(-3, 'day');
console.log(minusViaAdd.format('YYYY-MM-DD HH:mm:ss'));
2026-09-01 10:00:00
2026-11-01 10:45:00
2026-10-29 10:00:00
3. 链式调用(Chaining)复合计算
由于每个运算方法返回的都是一个新的 Day.js 实例,你可以将多个不同维度的加减操作通过点语法直接连缀起来。例如计算“1 年后、再减去 1 个月、再顺延 15 天的时间点”:
const start = dayjs('2026-03-10');
// 连缀执行:加 1 年 → 减 1 个月 → 加 15 天
const complexResult = start
.add(1, 'year')
.subtract(1, 'month')
.add(15, 'day');
console.log(complexResult.format('YYYY-MM-DD'));
四、实战场景与月末边界陷阱处理
理解了基础 API 后,我们来看看在真实业务中最为常见的实战场景,以及 Day.js 是如何优雅规避原生 Date 的边界缺陷的。
场景一:数据看板的时间范围筛选(结合 startOf / endOf)
在运营看板与报表统计中,常常需要获取“过去 7 天完整范围”或“本月至今”。为了剔除当前时间戳的时分秒偏差,通常将加减运算与 .startOf()(规整到周期起点)和 .endOf()(规整到周期终点)结合使用:
const now = dayjs();
// 获取“过去 7 天”的起止时间范围
// 起始时间:7 天前的 00:00:00
const startDate = now.subtract(7, 'day').startOf('day');
// 结束时间:昨天的 23:59:59
const endDate = now.subtract(1, 'day').endOf('day');
console.log('统计起始时间:', startDate.format('YYYY-MM-DD HH:mm:ss'));
console.log('统计结束时间:', endDate.format('YYYY-MM-DD HH:mm:ss'));
统计结束时间: 2026-10-31 23:59:59
场景二:月末与闰年边界计算(自动吸附机制)
这是日期计算中最著名的“暗坑”。公历月份天数不固定(大月31天、小月30天、2月平年28天/闰年29天)。考虑一个经典业务:用户在 1 月 31 日开通了 1 个月的月卡,下个月该在何时到期?
按照正常人类认知,2 月份没有 31 日,因此月卡应当在 2 月的最后一天(2月28日或29日)结束。然而原生 JavaScript 会得出荒谬的结果:
// 1. 原生 Date 的错误行为
const nativeDate = new Date('2026-01-31');
nativeDate.setMonth(nativeDate.getMonth() + 1);
// 2026 年 2 月只有 28 天,超出的 3 天溢出到了 3 月!
console.log('原生 Date 计算结果:', nativeDate.toISOString().slice(0, 10));
// 结果输出:2026-03-03(严重违背业务预期)
// 2. Day.js 的智能吸附处理
const dayjsDate = dayjs('2026-01-31');
const nextMonth = dayjsDate.add(1, 'month');
console.log('Day.js 计算结果:', nextMonth.format('YYYY-MM-DD'));
// 结果输出:2026-02-28(自动对齐目标月最后一天)
同样地,对于闰年的 2 月 29 日加 1 年,原生 Date 会溢出到下一年的 3 月 1 日,而 Day.js 会自动将其归整为平年的 2 月 28 日,确保年度周期处于同月份内。
五、进阶补充:时间差计算与扩展插件
在加减计算的基础上,经常需要反向验证两个日期之间的时间间隔,或结合插件处理复杂的时间段对象。
1. 使用 .diff() 计算时间差值
.diff() 方法用于计算两个日期之间的跨度,语法为 date1.diff(date2, unit, float):
const start = dayjs('2026-11-01');
const target = dayjs('2026-12-15');
// 计算相差的天数(整数)
const diffDays = target.diff(start, 'day');
console.log('相隔天数:', diffDays); // 输出:44
// 计算相差的月份(带浮点数,传第三个参数 true)
const diffMonthsExact = target.diff(start, 'month', true);
console.log('相隔月数:', diffMonthsExact.toFixed(1)); // 示例输出:1.5
2. 按需加载 Duration 插件处理时间段
如果业务中包含类似“将现有日期加上一个已定义的工时时长(如 2 天 4 小时 30 分钟)”,可以引入官方轻量插件 duration:
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
// 注册插件
dayjs.extend(duration);
const shiftDuration = dayjs.duration({
days: 2,
hours: 4,
minutes: 30
});
const original = dayjs('2026-11-01 08:00:00');
// 直接将 duration 传递给 add 方法
const completed = original.add(shiftDuration);
console.log(completed.format('YYYY-MM-DD HH:mm:ss'));
六、常见错误与避坑清单
初次从原生 Date 或其他库迁移至 Day.js 时,开发者常遇到以下三大问题,建议在开发中针对性检查:
这是最常见的疏忽。很多习惯了原生
date.setDate(...) 的开发者会写出类似 const d = dayjs(); d.add(1, 'day'); 的代码,然后发现 d 的日期根本没有变化。请务必牢记:Day.js 是不可变对象,.add() 和 .subtract() 永远不会原地修改变量,必须赋值给新变量,例如 const nextDay = d.add(1, 'day');。
在简写模式下,
'M'(大写)代表月份(Month),而 'm'(小写)代表分钟(Minute)。如果不小心将 .add(1, 'm') 误写成了加 1 个月,实际只会增加 1 分钟;反之若把 .add(30, 'M') 误当作 30 分钟,则会直接加 30 个月(2年半),引发灾难性的计算偏差。
避坑三:跨时区服务器上的自然日加减漂移
如果前端浏览器与后端服务器处于不同时区(例如中国 UTC+8 与伦敦 UTC+0),直接使用本地时间加减并转换为 ISO 字符串(toISOString())时,可能会因时差跨过零点而导致日期出现“提前一天或延后一天”的错觉。在涉及纯日期计算(如仅需年月日)时,推荐固定在本地时间下格式化为 YYYY-MM-DD 交付后端,或者引入 dayjs/plugin/utc 插件强制使用 UTC 模式执行加减。
总结
通过使用 Day.js,JavaScript 中的日期加减运算从以往容易踩坑的原地篡改和边界越界,转变为清晰、确定且安全的纯函数链式调用。在实际编码中,只要掌握 .add() 与 .subtract() 的基本调用格式,分清 'M' 与 'm' 的简写区分,并始终用变量接收运算产出的新实例,就能轻松应对各类复杂的时间运算场景,大幅提升代码的可读性与工程健壮度。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。