
时间函数在 JavaScript 里是入门级话题但真正能一次写对的人并不多。日常开发中日期格式化、时间戳换算、倒计时、定时器、时区处理几乎每个项目都会碰到一旦出了问题很多人第一反应是“语法写错了”实际真正错的多半是概念UTC 与本地时间差在哪、月份为什么从 0 开始、时间戳单位到底是秒还是毫秒、两个日期相减得到的是什么东西。这篇文章把时间函数的常用场景拆开讲覆盖 Date 对象、格式化、时间计算、定时器和排查思路适合前端开发、Node 后端开发也适合所有被日期计算折磨过的人。1. 先分清时间函数要解决的 4 类问题学时间函数最忌讳的是“遇到一个方法背一个方法”。Date 对象上有几十个方法名称接近、行为各异硬背很容易混。我建议先把时间函数处理的问题分成四类再按类别去记 API。1.1 获取时间当前时刻、时间戳、UTC 与本地时间第一类是“获取时间”。这里要区分三个概念当前时刻用new Date()拿到一个 Date 对象绝对时间通常用时间戳表示是自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数表达时间指的是同一个时刻在不同时区、不同地区显示出来的样子。很多人分不清“当前时刻”和“本地时间”。实际上 Date 对象内部保存的是一个绝对时刻与运行环境所在时区无关只有调用toString、getHours这类方法时才会按本地时区去换算。理解这一点很多显示异常的问题就能想通。1.2 展示与格式化同一个时刻不同的人看到不同样子第二类是“展示”。同样是 2025 年 1 月 15 日 10:30可以显示成2025/01/15、2025-01-15、January 15, 2025、10:30:00等形式。展示不改变时刻本身只是换个格式。这类操作看似简单实际上最容易遇到补零、时区、国际格式差异等问题。后面我会单独用一节来说格式化。1.3 时间计算与比较差值、进位、边界第三类是“计算与比较”。典型场景包括两个时间点相隔多久某个时间点之后 7 天是哪一天判断某天是否在同一天、同一月、同一年计算月末最后一天、季度末、年龄等。时间计算最怕边界条件2 月 29 日、跨月、跨年、夏令时任何一个不注意都会算错。后面的章节会逐个拆。1.4 定时执行setTimeout 与 setInterval第四类是“定时执行”。严格说setTimeout和setInterval不是 Date 的方法而是浏览器和 Node 提供的定时器 API。但它们和时间强相关项目里做倒计时、轮询、延迟任务时都会用到所以必须一起讲清楚。这四类问题对应的使用思路完全不同。你可以先把当前项目里的时间需求归类再看该用哪一类 API比死记方法列表有效得多。2. Date 对象创建和读取先把最容易踩的 3 个坑记住Date 是 JavaScript 里最常用的时间对象。它本身不难难在几个非常反直觉的规则。这些规则只要错一次后面所有基于它的计算都会跟着错。2.1 创建 Date 的几种方式// 当前时刻 const now new Date(); // 传时间戳毫秒 const d1 new Date(1736908200000); // 传日期字符串 const d2 new Date(2025-01-15T10:30:00); // 传年月日时分秒注意月份从 0 开始 const d3 new Date(2025, 0, 15, 10, 30, 0);第四种写法最容易踩坑下面单独说。2.2 月份从 0 开始新手第一坑JavaScript 的 Date 构造函数中月份从 0 开始计数0 表示一月11 表示十二月new Date(2025, 0, 15); // 2025-01-15 new Date(2025, 11, 15); // 2025-12-15很多人在传 1 表示一月结果日期整体往后推了一个月而且不同月份天数不同很难一眼看出来。getMonth()也是同理返回值是 0 到 11需要加 1 才是习惯上的月份。const d new Date(2025, 0, 15); d.getMonth(); // 0即一月 d.getMonth() 1; // 1这个规则不是 bug是历史设计但你必须记住。写工具函数时建议把“月份 1 / -1”的逻辑封装在同一个函数里避免业务代码里到处手动处理。2.3 时间戳单位是毫秒不是秒另一个高频坑是时间戳单位。JavaScript 的Date.now()、getTime()返回的都是毫秒而很多后端接口、数据库、其他语言里的时间戳用的是秒。Date.now(); // 1736908200000毫秒 Math.floor(Date.now() / 1000); // 转换成秒如果拿秒当毫秒传给new Date()时间会直接回到 1970 年附近反过来拿毫秒当秒又会得到几十年后的时间。和接口对接时我一般会先确认字段注释里写的是秒还是毫秒再决定要不要除以 1000。2.4 读取年月日时分秒的方法除了创建还要会读const d new Date(); d.getFullYear(); // 四位年份 d.getMonth(); // 0-11需要 1 d.getDate(); // 1-31 d.getDay(); // 0-60 是星期日 d.getHours(); // 0-23 d.getMinutes(); // 0-59 d.getSeconds(); // 0-59 d.getMilliseconds(); // 0-999注意getDay()返回的是星期几不是“当月第几天”拿它当日期用属于常见误用。对应还有setFullYear、setMonth、setDate等 set 方法名称规律一致。记住口诀getMonth()要加 1getDay()是星期getDate()才是日期。这三个最容易混。3. 日期格式化别急着引入库先搞懂原生方法格式化是时间函数里最常写、也最常出 bug 的部分。很多人一遇到格式化就想起 dayjs、moment但在引入库之前原生能力其实已经覆盖了大半需求。3.1 toISOString、toLocaleString、toString 的区别先看三个容易混淆的方法const d new Date(2025-01-15T10:30:00); d.toString(); // Wed Jan 15 2025 10:30:00 GMT0800 (中国标准时间) d.toISOString(); // 2025-01-15T02:30:00.000Z d.toLocaleString(); // 2025/1/15 10:30:00toString()返回的是当前运行时本地时区的完整字符串toISOString()返回的是 UTC 时间结尾带 Z按 ISO 8601 格式toLocaleString()返回的是本地化字符串具体格式由运行环境和语言区域决定。这里最容易迷惑的是toISOString()它显示的是 UTC 时间不是你的本地时间。我见过有人直接用toISOString()展示日期结果在中国时区下看到的时间比本地时间慢了 8 小时。3.2 手写一个简单的格式化函数如果项目只用到几种固定格式手写格式化函数完全够用function pad(n) { return n 10 ? 0 n : n; } function formatDate(date, pattern YYYY-MM-DD HH:mm:ss) { const map { YYYY: date.getFullYear(), MM: pad(date.getMonth() 1), DD: pad(date.getDate()), HH: pad(date.getHours()), mm: pad(date.getMinutes()), ss: pad(date.getSeconds()) }; return pattern.replace(/YYYY|MM|DD|HH|mm|ss/g, (key) map[key]); } formatDate(new Date()); // 例如 2025-01-15 10:30:00这个函数把年月日时分秒先取出来再按模板替换。优点是逻辑完全可控不受原生方法在浏览器间的差异影响缺点是只处理了固定时区如果需要跨时区显示就要手动换算或者用更成熟的库。3.3 补零问题格式化里最不起眼、最容易出错的是补零。月份、日期、小时、分钟、秒只有个位数时要补成两位数。上面的pad函数就是干这个的。新手常犯的错误是// 错误示范 2025- (d.getMonth() 1) - d.getDate(); // 结果是 2025-1-5和预期 2025-01-05 不一致这种格式看起来没大问题但如果后端接口对格式有严格要求或者要按字典序排序日期字符串不补零会直接导致排序错误。比如2025-02-01和2025-1-15按字符串比较时后者会排在前面因为字符1比2小。3.4 时区对格式化结果的影响格式化时必须想清楚一个问题这个时间是给谁看的给本机用户看用本地格式例如getHours()、toLocaleString()给服务端存数据、给跨时区用户看优先用 UTC例如toISOString()或手动拼接 UTC 字符串给日志记录建议统一用 ISO 8601 并带时区偏移方便排查。很多线上事故出在“前端按照本地时间格式化成字符串传给后端后端再按别的时区解析”结果时间偏差了几个小时。比较稳妥的做法是接口传输尽量传时间戳或 ISO 字符串展示时再转成本地格式。传输数据时优先传时间戳或 ISO 字符串展示时再转本地格式。这条约定能省掉大量时区 bug。4. 时间计算与比较差值、进位和边界条件有了创建和读取的基础接下来是真正考验逻辑的部分时间计算。这里建议所有计算都统一思路——先把日期转成时间戳做数值运算再转回 Date 对象。4.1 两个时间戳相减计算两个时间点间隔最稳定的方式是时间戳相减const start new Date(2025-01-15T10:00:00); const end new Date(2025-01-15T12:30:00); const diff end.getTime() - start.getTime(); // diff 是毫秒数2 小时 30 分 9000000 毫秒转成习惯的单位const seconds Math.floor(diff / 1000); const minutes Math.floor(seconds / 60); const hours Math.floor(minutes / 60); const days Math.floor(hours / 24);这里注意diff可能是负数。如果开始时间晚于结束时间先取绝对值再做转换避免负数的天数和小时混在一起。4.2 setMonth 和 setDate 的自动进位直接用 Date 对象的 set 方法做加减有一个很实用的特性超出范围会自动进位。例如const d new Date(2025, 0, 31); d.setMonth(d.getMonth() 1); // 2 月没有 31 日结果自动变成 2025-03-03这个行为既是便利也是坑。如果你要的是“每月最后一天”用setMonth就不安全因为 1 月 31 日加一个月会跳到 3 月 3 日而不是 2 月 28 日或 29 日。处理这类问题我一般会先设定目标月份再用new Date(year, month 1, 0)的方式取当月最后一天这个技巧下一节会讲。4.3 判断两个日期是否同一天直接比较两个 Date 对象用是不行的因为比较的是对象引用两个不同对象永远不相等。判断同一天正确思路是分别取年、月、日再比较function isSameDay(a, b) { return a.getFullYear() b.getFullYear() a.getMonth() b.getMonth() a.getDate() b.getDate(); }如果要判断同一小时、同一分钟逐级加条件就行。这种写法比转字符串再比较更稳因为不受格式化和补零影响。4.4 本月第一天、最后一天、某月有多少天一个常用技巧是利用 Date 构造函数的自动进位特性// 本月第一天 const firstDay new Date(2025, 0, 1); // 本月最后一天下个月第 0 天就是本月最后一天 const lastDay new Date(2025, 1, 0); // 2025-01-31 // 判断某月有多少天 function daysInMonth(year, month) { // month 传习惯上的 1-12 return new Date(year, month, 0).getDate(); } daysInMonth(2025, 2); // 282025 年 2 月 daysInMonth(2024, 2); // 292024 年是闰年new Date(year, month, 0)的 day 参数传 0意思是上一个月的最后一天因此能很方便地拿到某月天数也能顺便判断闰年。这个技巧在写日历组件、统计报表时非常常用。注意new Date(year, month, 0)里的 month 仍然是“下一个月”的索引结果取的是上一个月的最后一天。这个技巧写之前可以先在控制台验证一次避免方向搞反。5. 定时器setTimeout 和 setInterval 的真实行为时间函数不只是读取和计算日期还涉及定时执行。虽然setTimeout、setInterval不是 Date 的方法但很多人会把它们和时间函数混在一起学。这里把关键行为说清楚。5.1 基本用法// 延迟 1 秒执行一次 setTimeout(() { console.log(执行); }, 1000); // 每隔 1 秒执行一次 setInterval(() { console.log(每隔 1 秒执行); }, 1000);单位同样是毫秒。新手写代码时经常把 1 秒写成 1结果是 1 毫秒执行一次运行时 CPU 占用直接飙升。先明确单位再写数字。5.2 清除定时器定时器要保存返回值才能清除const timer setInterval(() { // 业务逻辑 }, 1000); // 条件满足时清除 clearInterval(timer);setTimeout对应clearTimeout。组件销毁、页面离开、任务结束时要记得清理定时器否则会一直运行造成资源占用和多余请求。在 React 里通常在useEffect的清理函数里清除。5.3 为什么 setInterval 不适合做倒计时setInterval的间隔时间不是精确的。它只保证“每隔大约 N 毫秒把回调加入队列”如果回调执行时间超过 N 毫秒或者浏览器标签页被切到后台计时就会堆积或暂停导致倒计时不准确。一个典型场景用setInterval每秒减 1 实现倒计时用户切到别的标签页 10 分钟再回来发现倒计时还停留在刚才的位置或者突然跳变。更稳的做法是每次执行时用Date.now()计算真实剩余时间而不是简单地计数减一。这样即使定时器被暂停恢复后也能根据当前时刻重新计算。5.4 用 Date.now() 校准定时器const deadline Date.now() 10 * 1000; const timer setInterval(() { const remain deadline - Date.now(); if (remain 0) { clearInterval(timer); console.log(倒计时结束); return; } console.log(剩余, Math.ceil(remain / 1000), 秒); }, 200);这里每隔 200 毫秒检查一次真实剩余时间而不是依赖“每执行一次就减 1”。即便浏览器定时器被延迟剩余时间也是通过当前时刻计算出来的最终结束时间不会偏移。定时器本身不准没关系用绝对时间校准即可。6. 实战倒计时、相对时间和日期工具函数前面讲的是知识点这一节把常见需求写成可以直接参考的工具函数。这些函数都比较小可以直接放进项目的 utils 文件里。6.1 一个真正能用的倒计时结合第 5 节思路封装一个微小的倒计时函数function countdown(endTime, onTick, onEnd) { const timer setInterval(() { const remain endTime.getTime() - Date.now(); if (remain 0) { clearInterval(timer); if (onEnd) onEnd(); return; } const d Math.floor(remain / 86400000); const h Math.floor(remain / 3600000) % 24; const m Math.floor(remain / 60000) % 60; const s Math.floor(remain / 1000) % 60; if (onTick) onTick({ d, h, m, s }); }, 200); return timer; }使用时传入结束时间、每次更新的回调和结束回调即可。注意这里d、h、m、s都是从总剩余毫秒数推导出来的不会出现累计误差。6.2 相对时间刚刚、几分钟前很多内容平台展示“3 分钟前”实现思路是当前时间减去目标时间再按阈值分段function timeAgo(date) { const diff Date.now() - date.getTime(); const minute 60 * 1000; const hour 60 * minute; const day 24 * hour; if (diff minute) return 刚刚; if (diff hour) return Math.floor(diff / minute) 分钟前; if (diff day) return Math.floor(diff / hour) 小时前; if (diff 30 * day) return Math.floor(diff / day) 天前; return date.toLocaleDateString(); }这个函数的核心仍然是时间戳相减。阈值可以根据产品需求调整比如“1 小时内显示分钟24 小时内显示小时7 天内显示天更早显示完整日期”。6.3 计算年龄计算年龄要处理生日和当前日期的大小关系不能只减年份function getAge(birthday) { const now new Date(); let age now.getFullYear() - birthday.getFullYear(); const isBeforeBirthday now.getMonth() birthday.getMonth() || (now.getMonth() birthday.getMonth() now.getDate() birthday.getDate()); if (isBeforeBirthday) { age--; } return age; }先按年份差算一个初始值再判断今年生日是否已经过了没过则减 1。这样能正确处理边界情况。2 月 29 日出生的人在非闰年怎么算需要按业务约定通常取 2 月 28 日或 3 月 1 日作为判断基准。6.4 跨时区的处理思路跨时区场景下最容易犯的错是“直接把本地时间字符串传给接口”。更稳妥的方案接口统一传时间戳例如Date.now()单位毫秒要与后端约定好展示时用new Date(timestamp)转成本地时间需要日期字符串时优先用toISOString()保证是 UTC或明确带上时区偏移。如果项目需要复杂的时区转换例如“给纽约用户展示纽约时间”原生 Date 不够用建议引入专门的时间库不要在业务代码里手动维护时区表。7. 时间相关 bug 的排查链路最后是我自己的排查经验。时间相关的 bug 很特殊它往往不是“崩溃”型报错而是“结果不对”型问题所以很容易被误判成业务逻辑错误。遇到这类问题我按下面顺序排查。7.1 先看现象是显示错、算错还是定时不准如果日期显示成 1970 年附近优先怀疑时间戳单位多半是秒被当成了毫秒。如果月份比预期大 1优先怀疑getMonth()没有 1。如果倒计时忽快忽慢、切回页面后不准优先检查定时器是否依赖计数累减而不是绝对时间。如果某个日期字符串解析后和预期不同下一步检查输入格式。先定位现象类别能少走一半弯路。7.2 再看时区和输入格式时区问题会在“看起来没错但对不上”时暴露。比如后端返回2025-01-15T02:30:00.000Z前端直接toLocaleString()显示和数据库里记录的时间差 8 小时这是正常的因为 Z 结尾表示 UTC。如果需要与本地时间一致必须显式转换而不是怀疑数据错了。字符串解析也要注意new Date(2025-01-15)和new Date(2025/01/15)在不同浏览器下表现可能不同。固定格式时尽量用 ISO 8601 字符串或者干脆传年月日参数给构造函数。7.3 再检查运行环境差异同一段代码Windows 和 macOS 下的toLocaleString()输出可能不一样Safari 对某些日期字符串的解析也比 Chrome 严格。如果只在某个浏览器或某台机器上出问题优先怀疑运行环境差异。7.4 最后看工具边界最后才是确认是不是工具本身不支持。比如某些浏览器对new Date(2025-01-15 10:30:00)这种带空格的格式支持不统一就属于浏览器对非 ISO 格式的解析差异。遇到这种情况统一输入格式或者手动拆分字符串再构造 Date会稳定很多。时间函数的坑很少是“原理不懂”造成的更多是“边界没想清楚”。把单位、时区、补零、进位这四个点记牢能避开绝大多数问题。我建议你在自己项目里把这些工具函数单独抽成一个文件统一封装格式化、差值、相对时间等逻辑长期看比到处散落new Date()好维护得多。