为何未清零时间时JavaScript计算日期会减整月而非单日?
问题场景
我在计算今日至3天前的日期及日期字符串时发现:若将今日的UTC时间清零,计算完全正常;若不清零(且当前时间接近UTC午夜),计算出的“昨天”会变成上月日期,其余天数计算则符合预期。测试时间为美国东部时间下午5:30(UTC时间晚11:30),且我是基于原today对象的getDate()计算昨天日期,而非基于新创建的yesterday对象,想明确该现象的原因。
对比代码
清零时间的正常代码
let today = new Date(); today.setUTCHours(0,0,0); // remove time, otherwise yesterday if based off of today could be a month ago. let todays_date_string = today.toISOString().slice(0, 10); let yesterday = new Date(todays_date_string); yesterday.setDate(today.getDate() - 1); let yesterdays_date_string = yesterday.toISOString().slice(0, 10); let two_days_ago = new Date(yesterdays_date_string); two_days_ago.setDate(two_days_ago.getDate() - 1); let two_days_ago_date_string = two_days_ago.toISOString().slice(0, 10); let three_days_ago = new Date(two_days_ago_date_string); three_days_ago.setDate(three_days_ago.getDate() - 1); let three_days_ago_date_string = three_days_ago.toISOString().slice(0, 10); console.log({today, today_s: today.toISOString(), todays_date_string, yesterdays_date_string, two_days_ago_date_string, three_days_ago_date_string});
未清零时间的异常代码
let today = new Date(); //today.setUTCHours(0,0,0); // remove time, otherwise yesterday if based off of today could be a month ago. let todays_date_string = today.toISOString().slice(0, 10); let yesterday = new Date(todays_date_string); yesterday.setDate(today.getDate() - 1); let yesterdays_date_string = yesterday.toISOString().slice(0, 10); let two_days_ago = new Date(yesterdays_date_string); two_days_ago.setDate(two_days_ago.getDate() - 1); let two_days_ago_date_string = two_days_ago.toISOString().slice(0, 10); let three_days_ago = new Date(two_days_ago_date_string); three_days_ago.setDate(three_days_ago.getDate() - 1); let three_days_ago_date_string = three_days_ago.toISOString().slice(0, 10); console.log({today, today_s: today.toISOString(), todays_date_string, yesterdays_date_string, two_days_ago_date_string, three_days_ago_date_string});
核心原因解析
1. getDate()的本地时区特性
getDate()方法返回的是当前Date对象在本地时区下的当月日期。测试时你处于美国东部时间,当本地时间是某月1日下午5:30时,UTC时间是该日晚11:30,此时today.getDate()返回1。
2. YYYY-MM-DD字符串的UTC解析规则
用YYYY-MM-DD格式的字符串创建Date对象时,JavaScript会默认将其解析为UTC时区的当日0点。比如测试当天是10月1日,todays_date_string是"2024-10-01",new Date("2024-10-01")对应的UTC时间是10月1日0点,转换为美国东部夏令时就是9月30日下午7点——此时这个yesterday对象的本地时区月份是9月(月份从0开始,对应9月的索引是8)。
3. setDate(0)的特殊行为
你调用yesterday.setDate(today.getDate() - 1),也就是传入参数1-1=0。JavaScript中setDate()的规则是:
- 如果传入0,会将日期设置为当前对象所在月份的上一个月的最后一天;
- 如果传入的数值大于当前月份的最大天数,会自动进位到下个月。
结合前面的场景,yesterday对象的本地月份是9月,调用setDate(0)就会将日期设置为8月的最后一天,这就出现了你看到的“昨天变成上月日期”的异常。
4. 清零UTC时间后的正常逻辑
当你调用today.setUTCHours(0,0,0)后:
today对象的UTC时间被设为当日0点,转换为美国东部夏令时就是前一天的下午7点;today.getDate()返回的是本地日期(比如原本是10月1日,此时本地是9月30日,getDate()返回30);- 调用
yesterday.setDate(30-1=29)时,yesterday对象是基于"2024-09-30"创建的(UTC 9月30日0点,对应本地9月30日0点),设置日期为29就会得到9月29日,计算完全正常。
关键触发点
你基于本地时区的today对象日期值,去修改一个UTC时区解析的Date对象的日期,两者的时区和当前月份存在差异,才触发了setDate(0)的特殊逻辑,导致日期跳至上月。
内容的提问来源于stack exchange,提问作者evan

