1. 从零开始:理解JavaScript的Date对象
大家好,我是你们的老朋友,一个在JavaScript世界里摸爬滚打了十多年的开发者。今天我想和大家聊聊JavaScript中时间处理这个话题,这可能是每个前端开发者都会遇到,但又常常让人头疼的问题。我记得刚入行那会儿,为了计算两个日期相差多少天,我写了一大堆复杂的代码,结果还经常出错。后来才发现,原来JavaScript的Date对象已经帮我们做了很多工作,只是我们没用好它。
JavaScript的Date对象本质上是一个时间戳的封装,它记录的是从1970年1月1日00:00:00 UTC(协调世界时)开始经过的毫秒数。这个起点被称为“Unix纪元”。听起来有点抽象?我打个比方,这就像我们生活中用“公元”来纪年一样,1970年1月1日就是JavaScript时间的“公元元年”。
创建一个Date对象非常简单,有几种常用的方式:
// 1. 获取当前时间
let now = new Date();
console.log(now); // 输出类似:Mon Feb 17 2025 14:30:25 GMT+0800 (中国标准时间)
// 2. 从时间戳创建
let timestamp = 1739773825000; // 2025年2月17日的时间戳
let dateFromTimestamp = new Date(timestamp);
// 3. 从日期字符串创建
let dateFromString = new Date('2025-02-17T14:30:25');
// 4. 指定年月日等参数创建
let specificDate = new Date(2025, 1, 17, 14, 30, 25); // 注意:月份从0开始,1代表二月
这里有个坑我得提醒大家:当你用new Date(2025, 1, 17)这种方式创建日期时,第二个参数(月份)是从0开始计数的!也就是说,0代表一月,1代表二月,以此类推。这个设计确实有点反人类,我当年就因为这个踩过坑,明明想创建3月1日,结果写成了new Date(2025, 3, 1),实际得到的却是4月1日。
Date对象提供了很多方法来获取时间的各个部分。比如getFullYear()获取四位数的年份,getMonth()获取月份(0-11),getDate()获取一个月中的第几天(1-31)。这里又有一个容易混淆的地方:getDate()是获取日期(几号),而getDay()是获取星期几(0代表周日,1代表周一,以此类推)。我建议大家在写代码时,可以加个注释提醒自己,避免搞混。
在实际项目中,我经常看到有人用getYear()这个方法,这里我必须郑重提醒:千万不要用getYear()!这个方法在不同的浏览器中行为不一致,有些返回两位数的年份(比如2025年返回125),这是个历史遗留问题。正确的做法是始终使用getFullYear(),它保证返回四位数的完整年份。
2. 时间加减运算:避开那些常见的坑
时间加减是日常开发中最常见的需求之一。比如电商网站要计算优惠券的过期时间,社交应用要显示“3分钟前”这样的相对时间,或者报表系统要生成上个月的数据统计。JavaScript的Date对象提供了非常方便的自动校准功能,这让时间加减变得简单,但也需要理解它的工作原理。
2.1 基础加减:天、小时、分钟、秒
我们先从最简单的开始。给一个日期加一天,你会怎么写?我见过很多新手会这样写:
// 错误示范:直接修改getDate()的返回值
let date = new Date('2025-02-28');
let day = date.getDate(); // 28
date.setDate(day + 1); // 这样写虽然能工作,但不是最佳实践
其实有更优雅的方式。Date对象的setDate()、setMonth()、setFullYear()等方法有个很智能的特性:自动校准。这意味着你可以设置“超出范围”的值,JavaScript会自动帮你调整。比如:
// 正确做法:利用自动校准
let date = new Date('2025-02-28');
date.setDate(date.getDate() + 1); // 直接加1,JavaScript会自动处理跨月
console.log(date); // 输出:Sat Mar 01 2025 00:00:00 GMT+0800
// 同样的原理适用于其他时间单位
date.setHours(date.getHours() + 3); // 加3小时
date.setMinutes(date.getMinutes() + 30); // 加30分钟
date.setSeconds(date.getSeconds() + 45); // 加45秒
我封装了一个常用的时间加减函数,在实际项目中非常好用:
/**
* 日期加减工具函数
* @param {Date} date - 原始日期
* @param {number} value - 要加减的值
* @param {string} unit - 单位:'days'、'months'、'years'、'hours'、'minutes'、'seconds'
* @returns {Date} 计算后的新日期
*/
function addTime(date, value, unit) {
// 创建副本,避免修改原日期
const result = new Date(date);
switch(unit) {
case 'days':
result.setDate(result.getDate() + value);
break;
case 'months':
result.setMonth(result.getMonth() + value);
break;
case 'years':
result.setFullYear(result.getFullYear() + value);
break;
case 'hours':
result.setHours(result.getHours() + value);
break;
case 'minutes':
result.setMinutes(result.getMinutes() + value);
break;
case 'seconds':
result.setSeconds(result.getSeconds() + value);
break;
default:
throw new Error(`不支持的unit参数:${unit}`);
}
return result;
}
// 使用示例
let today = new Date();
let nextWeek = addTime(today, 7, 'days');
let lastMonth = addTime(today, -1, 'months');
2.2 处理月底和年底的特殊情况
时间加减中最容易出问题的地方就是月底和年底的边界情况。比如给2月28日加1个月,应该得到3月28日,但如果给1月31日加1个月呢?2月没有31日,这时候JavaScript的自动校准就派上用场了。
// 月底加一天:自动跳到次月第一天
let endOfMonth = new Date('2025-01-31');
endOfMonth.setDate(endOfMonth.getDate() + 1);
console.log(endOfMonth.getDate()); // 输出:1(2月1日)
console.log(endOfMonth.getMonth()); // 输出:1(二月)
// 年底加一个月:自动跳到次年一月
let endOfYear = new Date('2025-12-15');
endOfYear.setMonth(endOfYear.getMonth() + 1);
console.log(endOfYear.getFullYear()); // 输出:2026
console.log(endOfYear.getMonth()); // 输出:0(一月)
这里有个重要的细节需要注意:当你使用setMonth()进行月份加减时,如果目标月份的天数少于当前日期,JavaScript会自动调整到目标月份的最后一天。比如:
let date = new Date('2025-01-31');
date.setMonth(date.getMonth() + 1); // 加一个月
console.log(date); // 输出:Fri Feb 28 2025 00:00:00 GMT+0800
// 2月没有31日,所以自动调整到2月28日
在实际项目中,我建议对月底操作进行特殊处理。比如在做财务报表时,如果用户选择“加一个月”,通常希望得到的是下个月的同一天,但如果下个月没有这一天(比如1月31日到2月),业务上可能希望取最后一天。这时候就需要根据具体业务需求来写逻辑:
function addMonthsSafely(date, months) {
const result = new Date(date);
const originalDate = result.getDate();
// 先加上月份
result.setMonth(result.getMonth() + months);
// 检查日期是否发生变化(说明目标月份天数较少)
if (result.getDate() !== originalDate) {
// 如果日期变了,说明自动调整到了目标月份的最后一天
// 这里可以根据业务需求决定是否要调整
// 比如有些业务希望始终返回月份的最后一天
// 有些则希望返回目标月份中与原始日期最接近的一天
}
return result;
}
2.3 计算时间差:毫秒、秒、天、月
计算两个日期之间的差值也是常见需求。JavaScript让这个变得很简单,因为Date对象可以直接相减:
let date1 = new Date('2025-02-17');
let date2 = new Date('2025-02-20');
let diffInMs = date2 - date1; // 直接相减得到毫秒差
console.log(diffInMs); // 输出:259200000(3天的毫秒数)
// 转换为天数
let diffInDays = diffInMs / (1000 * 60 * 60 * 24);
console.log(diffInDays); // 输出:3
但是计算月份差就比较复杂了,因为每个月的天数不同。我写过一个计算月份差的函数,考虑了各种边界情况:
function monthDiff(startDate, endDate) {
// 计算完整的年份差和月份差
let years = endDate.getFullYear() - startDate.getFullYear();
let months = endDate.getMonth() - startDate.getMonth();
let totalMonths = years * 12 + months;
// 调整:如果结束日期的日期小于开始日期的日期,说明不满一个月
if (endDate.getDate() < startDate.getDate()) {
totalMonths--;
}
return totalMonths;
}
// 使用示例
let start = new Date('2025-01-15');
let end = new Date('2025-03-10');
console.log(monthDiff(start, end)); // 输出:1(虽然跨了3个月,但实际只有1个完整月)
对于性能敏感的场景,比如需要频繁计算时间差,我推荐使用时间戳来计算,因为数字运算比Date对象的方法调用要快:
// 高性能的时间差计算
function dateDiffFast(date1, date2) {
const time1 = date1.getTime(); // 转换为时间戳
const time2 = date2.getTime();
return Math.abs(time2 - time1); // 返回毫秒差
}
3. 时间戳与日期字符串的相互转换
时间戳和日期字符串的转换是前后端数据交互中的高频操作。后端通常返回时间戳或ISO格式的字符串,前端需要将其转换为用户友好的格式展示;用户输入的日期又需要转换为时间戳或特定格式的字符串传给后端。
3.1 时间戳转日期字符串:yyyy-MM-dd HH:mm:ss
时间戳转日期字符串是最常见的需求。虽然网上有很多现成的代码,但我发现很多都有点复杂或者考虑不周全。这里我分享一个经过多年实战检验的格式化函数:
/**
* 时间戳转格式化字符串
* @param {number|Date} timestamp - 时间戳(毫秒)或Date对象
* @param {string} format - 格式,默认'yyyy-MM-dd HH:mm:ss'
* @returns {string} 格式化后的日期字符串
*/
function formatDate(timestamp, format = 'yyyy-MM-dd HH:mm:ss') {
// 处理Date对象或时间戳
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
// 如果时间戳是秒级(10位),需要转换为毫秒
if (typeof timestamp === 'number' && timestamp.toString().length === 10) {
date.setTime(timestamp * 1000);
}
const map = {
'yyyy': date.getFullYear(),
'MM': String(date.getMonth() + 1).padStart(2, '0'),
'dd': String(date.getDate()).padStart(2, '0'),
'HH': String(date.getHours()).padStart(2, '0'),
'mm': String(date.getMinutes()).padStart(2, '0'),
'ss': String(date.getSeconds()).padStart(2, '0'),
'M': date.getMonth() + 1, // 不补零的月份
'd': date.getDate(), // 不补零的日期
'H': date.getHours(), // 不补零的小时
'm': date.getMinutes(), // 不补零的分钟
's': date.getSeconds() // 不补零的秒
};
return format.replace(/yyyy|MM|dd|HH|mm|ss|M|d|H|m|s/g, matched => map[matched]);
}
// 使用示例
let timestamp = 1739773825123;
console.log(formatDate(timestamp)); // 输出:2025-02-17 14:30:25
console.log(formatDate(timestamp, 'yyyy年MM月dd日 HH:mm')); // 输出:2025年02月17日 14:30
console.log(formatDate(timestamp, 'yyyy-M-d H:m:s')); // 输出:2025-2-17 14:30:25
这个函数有几个亮点:第一,它同时支持Date对象和时间戳;第二,它能自动识别10位秒级时间戳并正确转换;第三,它支持多种格式,包括补零和不补零的版本;第四,使用正则表达式替换,性能较好。
3.2 日期字符串转时间戳
反过来,把日期字符串转成时间戳也很重要。这里最容易踩的坑是时区问题。我强烈建议在创建Date对象时,使用/作为分隔符,或者使用ISO 8601格式:
// 安全的方式:使用/分隔符
let date1 = new Date('2025/02/17 14:30:25');
console.log(date1.getTime()); // 正确的时间戳
// 安全的方式:ISO 8601格式
let date2 = new Date('2025-02-17T14:30:25');
console.log(date2.getTime()); // 正确的时间戳
// 危险的方式:直接使用yyyy-MM-dd HH:mm:ss格式
let date3 = new Date('2025-02-17 14:30:25');
// 在某些浏览器中,这种格式可能被解析为UTC时间,导致时区问题
如果你从后端接收到的就是yyyy-MM-dd HH:mm:ss格式的字符串,我建议先替换分隔符:
function parseDateString(dateStr) {
// 将yyyy-MM-dd HH:mm:ss格式转换为yyyy/MM/dd HH:mm:ss
const normalizedStr = dateStr.replace(/-/g, '/');
return new Date(normalizedStr);
}
// 或者更健壮的版本
function parseDateStringSafe(dateStr) {
// 处理多种可能的格式
if (dateStr.includes('T')) {
// ISO格式,直接解析
return new Date(dateStr);
} else if (dateStr.includes('-')) {
// yyyy-MM-dd格式,替换分隔符
return new Date(dateStr.replace(/-/g, '/'));
} else {
// 其他格式,尝试直接解析
return new Date(dateStr);
}
}
3.3 处理时区问题
时区问题是日期处理中最棘手的部分之一。我经历过一个项目,用户反馈说他们的预约时间总是差8小时,排查了半天才发现是时区问题。JavaScript的Date对象内部存储的是UTC时间,但显示时会根据本地时区转换。
// 创建日期时指定时区
let utcDate = new Date('2025-02-17T14:30:25Z'); // Z表示UTC时间
let localDate = new Date('2025-02-17T14:30:25+08:00'); // 东八区时间
console.log(utcDate.toLocaleString()); // 本地时间显示
console.log(localDate.toLocaleString()); // 本地时间显示
// 获取UTC时间
console.log(utcDate.getUTCHours()); // 获取UTC小时
console.log(localDate.getUTCHours()); // 获取UTC小时
// 时区偏移(分钟)
let offset = new Date().getTimezoneOffset(); // 中国标准时间返回-480
console.log(`本地时间比UTC快${-offset/60}小时`);
对于需要处理多时区的应用,我建议在存储和传输时都使用UTC时间,只在显示时转换为本地时间:
// 存储和传输:使用UTC时间戳或ISO字符串
function toUTCString(date) {
return date.toISOString(); // 例如:2025-02-17T06:30:25.000Z
}
// 显示:根据用户时区格式化
function formatForUser(date, timezone = 'Asia/Shanghai') {
return date.toLocaleString('zh-CN', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
4. 实战应用:常见场景的完整解决方案
经过前面的学习,我们已经掌握了JavaScript时间处理的核心技能。现在让我们看看如何在实际项目中应用这些知识。我挑选了几个最常见的场景,给出完整的解决方案。
4.1 倒计时功能的实现
倒计时是电商、活动页面中的常见功能。实现一个健壮的倒计时需要考虑性能、准确性和用户体验。
class CountdownTimer {
constructor(endTime, options = {}) {
this.endTime = new Date(endTime).getTime();
this.onTick = options.onTick || (() => {});
this.onEnd = options.onEnd || (() => {});
this.interval = null;
this.isRunning = false;
// 立即计算一次
this.tick();
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.interval = setInterval(() => {
this.tick();
}, 1000); // 每秒更新一次
}
stop() {
this.isRunning = false;
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
tick() {
const now = Date.now();
const remaining = this.endTime - now;
if (remaining <= 0) {
this.stop();
this.onEnd();
return;
}
// 计算天、小时、分钟、秒
const days = Math.floor(remaining / (1000 * 60 * 60 * 24));
const hours = Math.floor((remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((remaining % (1000 * 60)) / 1000);
this.onTick({
days,
hours,
minutes,
seconds,
totalMs: remaining
});
}
// 格式化显示
static formatTime({days, hours, minutes, seconds}) {
if (days > 0) {
return `${days}天${hours}小时${minutes}分${seconds}秒`;
} else if (hours > 0) {
return `${hours}小时${minutes}分${seconds}秒`;
} else if (minutes > 0) {
return `${minutes}分${seconds}秒`;
} else {
return `${seconds}秒`;
}
}
}
// 使用示例
const timer = new CountdownTimer('2025-02-20 23:59:59', {
onTick: (time) => {
console.log(`剩余时间:${CountdownTimer.formatTime(time)}`);
},
onEnd: () => {
console.log('倒计时结束!');
}
});
timer.start();
这个倒计时类有几个优点:第一,它使用类封装,便于复用;第二,它提供了开始、停止的控制方法;第三,它支持回调函数,方便与UI框架集成;第四,它考虑了性能,使用Date.now()而不是new Date()获取当前时间。
4.2 相对时间显示(如"3分钟前")
社交应用、新闻网站经常需要显示相对时间,比如"刚刚"、"3分钟前"、"昨天"等。这种显示方式比完整的日期时间更友好。
function formatRelativeTime(date, baseDate = new Date()) {
const targetTime = date instanceof Date ? date.getTime() : new Date(date).getTime();
const baseTime = baseDate.getTime();
const diffInMs = baseTime - targetTime;
const diffInSeconds = Math.floor(diffInMs / 1000);
const diffInMinutes = Math.floor(diffInSeconds / 60);
const diffInHours = Math.floor(diffInMinutes / 60);
const diffInDays = Math.floor(diffInHours / 24);
// 如果是未来时间
if (diffInMs < 0) {
return '刚刚';
}
// 规则定义
const rules = [
{limit: 60, unit: '秒', text: '刚刚'},
{limit: 3600, unit: '分钟', calc: () => diffInMinutes},
{limit: 86400, unit: '小时', calc: () => diffInHours},
{limit: 2592000, unit: '天', calc: () => diffInDays},
{limit: 31536000, unit: '个月', calc: () => Math.floor(diffInDays / 30)},
{limit: Infinity, unit: '年', calc: () => Math.floor(diffInDays / 365)}
];
const diffInSecondsAbs = Math.abs(diffInSeconds);
for (const rule of rules) {
if (diffInSecondsAbs < rule.limit) {
if (rule.text) return rule.text;
const value = rule.calc();
return `${value}${rule.unit}前`;
}
}
// 超过一年,显示完整日期
return formatDate(date, 'yyyy-MM-dd');
}
// 增强版:支持更多人性化显示
function formatRelativeTimeSmart(date) {
const now = new Date();
const target = new Date(date);
const diffInDays = Math.floor((now - target) / (1000 * 60 * 60 * 24));
// 今天
if (diffInDays === 0) {
const diffInHours = Math.floor((now - target) / (1000 * 60 * 60));
if (diffInHours === 0) {
const diffInMinutes = Math.floor((now - target) / (1000 * 60));
if (diffInMinutes === 0) {
return '刚刚';
}
return `${diffInMinutes}分钟前`;
}
return `${diffInHours}小时前`;
}
// 昨天
if (diffInDays === 1) {
return '昨天 ' + formatDate(target, 'HH:mm');
}
// 本周内
if (diffInDays < 7) {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return weekdays[target.getDay()] + ' ' + formatDate(target, 'HH:mm');
}
// 今年内
if (target.getFullYear() === now.getFullYear()) {
return formatDate(target, 'MM月dd日 HH:mm');
}
// 其他
return formatDate(target, 'yyyy年MM月dd日');
}
// 使用示例
console.log(formatRelativeTimeSmart(new Date())); // 刚刚
console.log(formatRelativeTimeSmart(new Date(Date.now() - 5 * 60 * 1000))); // 5分钟前
console.log(formatRelativeTimeSmart(new Date(Date.now() - 25 * 60 * 60 * 1000))); // 昨天 14:30
4.3 日期范围选择与验证
在表单中,日期范围选择是常见需求。比如选择入住和离店日期、选择生日范围等。这里我分享一个完整的日期范围处理工具:
class DateRangeValidator {
/**
* 验证日期范围是否有效
* @param {Date} startDate - 开始日期
* @param {Date} endDate - 结束日期
* @param {Object} options - 配置选项
* @returns {Object} 验证结果
*/
static validateRange(startDate, endDate, options = {}) {
const {
minDays = 1, // 最小天数
maxDays = 365, // 最大天数
allowSameDay = false, // 是否允许同一天
disabledDates = [] // 禁用日期
} = options;
// 确保是Date对象
const start = new Date(startDate);
const end = new Date(endDate);
// 清除时间部分,只比较日期
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
const result = {
isValid: true,
error: null,
days: 0
};
// 计算天数差
const timeDiff = end.getTime() - start.getTime();
const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24));
result.days = dayDiff;
// 验证开始日期不能晚于结束日期
if (dayDiff < 0) {
result.isValid = false;
result.error = '开始日期不能晚于结束日期';
return result;
}
// 验证是否允许同一天
if (dayDiff === 0 && !allowSameDay) {
result.isValid = false;
result.error = '开始日期和结束日期不能是同一天';
return result;
}
// 验证最小天数
if (dayDiff < minDays - 1) { // -1是因为包含开始日期
result.isValid = false;
result.error = `最少需要选择${minDays}天`;
return result;
}
// 验证最大天数
if (dayDiff > maxDays - 1) {
result.isValid = false;
result.error = `最多只能选择${maxDays}天`;
return result;
}
// 检查禁用日期
for (let i = 0; i <= dayDiff; i++) {
const currentDate = new Date(start);
currentDate.setDate(start.getDate() + i);
// 检查是否在禁用日期中
const isDisabled = disabledDates.some(disabledDate => {
const disabled = new Date(disabledDate);
disabled.setHours(0, 0, 0, 0);
return currentDate.getTime() === disabled.getTime();
});
if (isDisabled) {
result.isValid = false;
result.error = `选择的日期中包含不可用的日期:${formatDate(currentDate, 'yyyy-MM-dd')}`;
return result;
}
}
return result;
}
/**
* 生成日期范围内的所有日期
* @param {Date} startDate - 开始日期
* @param {Date} endDate - 结束日期
* @returns {Date[]} 日期数组
*/
static generateDateRange(startDate, endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
const dates = [];
const current = new Date(start);
while (current <= end) {
dates.push(new Date(current));
current.setDate(current.getDate() + 1);
}
return dates;
}
/**
* 获取两个日期的交集
* @param {Object} range1 - 第一个日期范围 {start, end}
* @param {Object} range2 - 第二个日期范围 {start, end}
* @returns {Object|null} 交集范围,如果没有交集返回null
*/
static getIntersection(range1, range2) {
const start1 = new Date(range1.start);
const end1 = new Date(range1.end);
const start2 = new Date(range2.start);
const end2 = new Date(range2.end);
// 计算交集的开始和结束
const intersectionStart = start1 > start2 ? start1 : start2;
const intersectionEnd = end1 < end2 ? end1 : end2;
// 如果没有交集
if (intersectionStart > intersectionEnd) {
return null;
}
return {
start: intersectionStart,
end: intersectionEnd,
days: Math.floor((intersectionEnd - intersectionStart) / (1000 * 60 * 60 * 24)) + 1
};
}
}
// 使用示例
const bookingRange = {
start: '2025-02-20',
end: '2025-02-25'
};
const validation = DateRangeValidator.validateRange(
bookingRange.start,
bookingRange.end,
{
minDays: 2,
maxDays: 30,
allowSameDay: false,
disabledDates: ['2025-02-22', '2025-02-23'] // 禁用这两天
}
);
if (validation.isValid) {
console.log(`预订成功,共${validation.days}天`);
} else {
console.log(`预订失败:${validation.error}`);
}
// 生成日期范围
const dateRange = DateRangeValidator.generateDateRange('2025-02-20', '2025-02-25');
console.log(dateRange.map(date => formatDate(date, 'yyyy-MM-dd')));
// 输出:['2025-02-20', '2025-02-21', '2025-02-22', '2025-02-23', '2025-02-24', '2025-02-25']
4.4 工作日计算和节假日排除
在企业应用中,经常需要计算工作日,排除周末和节假日。这是一个比较复杂但实用的功能:
class WorkdayCalculator {
constructor(options = {}) {
this.weekends = options.weekends || [0, 6]; // 默认周末是周日(0)和周六(6)
this.holidays = options.holidays || []; // 节假日数组
this.workdays = options.workdays || []; // 调休工作日数组
}
/**
* 判断是否是工作日
* @param {Date} date - 要判断的日期
* @returns {boolean} 是否是工作日
*/
isWorkday(date) {
const day = date.getDay();
// 检查是否是周末
if (this.weekends.includes(day)) {
// 如果是周末,检查是否是调休工作日
return this.workdays.some(workday =>
this.isSameDay(date, new Date(workday))
);
}
// 检查是否是节假日
const isHoliday = this.holidays.some(holiday =>
this.isSameDay(date, new Date(holiday))
);
return !isHoliday;
}
/**
* 计算两个日期之间的工作日天数
* @param {Date} startDate - 开始日期
* @param {Date} endDate - 结束日期
* @param {boolean} inclusive - 是否包含开始和结束日期
* @returns {number} 工作日天数
*/
getWorkdaysBetween(startDate, endDate, inclusive = true) {
const start = new Date(startDate);
const end = new Date(endDate);
// 确保开始日期不晚于结束日期
if (start > end) {
[start, end] = [end, start];
}
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
let workdayCount = 0;
const current = new Date(start);
// 调整包含性
if (!inclusive) {
current.setDate(current.getDate() + 1);
}
const endDateForLoop = new Date(end);
if (!inclusive) {
endDateForLoop.setDate(endDateForLoop.getDate() - 1);
}
// 遍历每一天
while (current <= endDateForLoop) {
if (this.isWorkday(current)) {
workdayCount++;
}
current.setDate(current.getDate() + 1);
}
return workdayCount;
}
/**
* 添加n个工作日后的日期
* @param {Date} startDate - 开始日期
* @param {number} workdays - 要添加的工作日数
* @returns {Date} 计算后的日期
*/
addWorkdays(startDate, workdays) {
if (workdays === 0) return new Date(startDate);
const result = new Date(startDate);
const direction = workdays > 0 ? 1 : -1;
let remaining = Math.abs(workdays);
while (remaining > 0) {
result.setDate(result.getDate() + direction);
if (this.isWorkday(result)) {
remaining--;
}
}
return result;
}
/**
* 判断两个日期是否是同一天
* @private
*/
isSameDay(date1, date2) {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
}
}
// 使用示例:配置中国的节假日和调休
const chinaHolidays2025 = [
'2025-01-01', // 元旦
'2025-01-28', '2025-01-29', '2025-01-30', // 春节
'2025-04-04', '2025-04-05', '2025-04-06', // 清明节
'2025-05-01', '2025-05-02', '2025-05-03', // 劳动节
'2025-06-10', // 端午节
'2025-09-17', '2025-09-18', // 中秋节
'2025-10-01', '2025-10-02', '2025-10-03', '2025-10-04', '2025-10-05', '2025-10-06', '2025-10-07' // 国庆节
];
const chinaWorkdays2025 = [
'2025-01-26', // 春节调休
'2025-02-08', // 春节调休
'2025-04-27', // 劳动节调休
'2025-05-10', // 劳动节调休
'2025-09-14', // 中秋节调休
'2025-09-29', // 国庆节调休
'2025-10-12' // 国庆节调休
];
const calculator = new WorkdayCalculator({
weekends: [0, 6], // 周六周日休息
holidays: chinaHolidays2025,
workdays: chinaWorkdays2025
});
// 计算2025年2月的工作日
const start = new Date('2025-02-01');
const end = new Date('2025-02-28');
const workdaysInFeb = calculator.getWorkdaysBetween(start, end);
console.log(`2025年2月有${workdaysInFeb}个工作日`);
// 计算10个工作日后的日期
const today = new Date();
const deadline = calculator.addWorkdays(today, 10);
console.log(`今天起10个工作日后的日期是:${formatDate(deadline, 'yyyy-MM-dd')}`);
// 检查特定日期是否是工作日
const testDate = new Date('2025-02-08'); // 春节调休,应该是工作日
console.log(`${formatDate(testDate, 'yyyy-MM-dd')}是工作日吗?${calculator.isWorkday(testDate)}`);
通过这些实战案例,我们可以看到,JavaScript的日期处理虽然有一些坑,但只要掌握了正确的方法,就能写出健壮、高效的代码。关键是要理解Date对象的工作原理,特别是时区和自动校准的特性,然后根据具体业务需求封装合适的工具函数。在实际项目中,我建议将常用的日期处理函数封装成工具类,这样既能提高开发效率,也能保证代码的一致性。
383



被折叠的 条评论
为什么被折叠?



