ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript时间函数全攻略:从Date对象到时区与格式化实战

JavaScript时间函数全攻略:从Date对象到时区与格式化实战 时间函数这个题目看起来是前端基本功但真正能一次写对的人不多。问题主要集中在几个地方Date对象的底层设计、字符串解析的不一致性、时区偏移到底由谁负责、以及格式化函数为什么总在边界条件“翻车”。这次我们围绕 JavaScript 时间函数做一次系统梳理。不聊空洞概念直接落到可运行代码、可复制的工具函数、业务场景中的真实用法把常见坑位一次性踩清楚。如果你最近正好在写倒计时、做报表统计、处理接口时间戳或者被“日期差 8 小时”这类问题困扰这篇文章建议收藏备用。1. 时间函数核心能力速览JavaScript 的时间处理并不是“一套 API 走天下”而是由三部分组成能力层代表工具解决什么问题原生基础Date对象、Date.now()、Date.parse()获取时间、时间戳转换、基础比较原生格式化toISOString()、toLocaleString()、Intl.DateTimeFormat转字符串、本地化显示第三方封装dayjs、date-fns、moment.js链式操作、复杂格式化、相对时间自定义工具函数formatDate、parseDate、dateAdd项目内统一格式避免重复踩坑什么场景该用哪一层这是重点一个时间戳存库、两个时间戳比大小直接原生Date不需要引库。做YYYY-MM-DD HH:mm:ss这种固定格式展示写一个工具函数或者用 dayjs。国际化、多语言、跨时区展示优先Intl.DateTimeFormat。老项目里到处都有moment不一定要迁移但新项目更推荐 dayjs 或 date-fns。定时、轮询、倒计时setInterval、setTimeout注意清理和误差累积。2. 适用场景与使用边界时间函数适合这些场景前端页面活动倒计时、列表时间显示、日历组件、消息“x分钟前”。后端 Node.js日志时间、定时任务、接口返回时间戳、UTC 标准时间存储。数据分析按天/按周/按月分组统计时间区间筛选。数据迁移不同格式的日期字符串统一解析为标准时间。边界条件也要说清楚不要期望原生Date自动处理所有格式new Date(2023-10-05 14:30:00)在旧版浏览器和不同 Node.js 版本上表现不完全一致。Date对象在 JavaScript 内部只存一个数字自 1970-01-01 00:00:00 UTC 以来的毫秒数。年和月从 0 开始计数getMonth()返回值 0-11这是新手最容易错的地方。时间字符串解析在不同引擎上可能有差异业务上如果传入的是外部数据建议先人工解析再构造时间。夏令时地区的时间运算不能假设每天都固定 24 小时。涉及用户隐私的时间数据例如访问记录、聊天记录存储和展示时要遵守产品合规要求避免在日志里暴露多余的个人信息。3. 环境准备与前置条件时间函数是基础能力运行环境要求不高浏览器F12 打开控制台就能直接跑。Node.js建议 18 以上版本Intl支持更完整。包管理器npm、pnpm、yarn 任选。日常调试可以在.js文件里写也可以直接node test.js执行。如果想测试第三方库先初始化一个 npm 项目。# 初始化项目 npm init -y # 安装常见时间处理库按需选择 npm install dayjs date-fns不需要额外依赖原生写法足够跑通这套验证流程。下面给出的代码都可以直接保存为.js文件运行。4. 基础时间函数创建、获取、转换这一节只讲原生能力目标是建立可靠的时间函数基线创建时间、读取时间、转换时间戳、比较早晚。4.1 创建时间对象推荐四种写法分别对应不同输入// 写法1当前时间 const now new Date(); console.log(now); // 写法2时间戳单位毫秒 const fromTs new Date(1696416000000); console.log(fromTs.toISOString()); // 写法3日期字符串 const fromStr new Date(2023-10-05T14:30:00Z); console.log(fromStr.toISOString()); // 写法4数值参数注意 month 从 0 开始 const fromParts new Date(2023, 9, 5, 14, 30, 0); // 2023-10-05 14:30:00 console.log(fromParts.toString());关键点写法 4 的月份是9代表 10 月。如果你传10得到的是 11 月。4.2 获取时间字段const d new Date(2023-10-05T14:30:00); console.log(d.getFullYear()); // 2023 console.log(d.getMonth()); // 9注意从0开始 console.log(d.getDate()); // 5几号 console.log(d.getDay()); // 4星期几0表示周日 console.log(d.getHours()); // 14本地时区 console.log(d.getMinutes()); // 30 console.log(d.getSeconds()); // 0 console.log(d.getMilliseconds());// 04.3 时间戳转换// 当前时间戳 const ts Date.now(); // Date 对象转时间戳 const d new Date(2023-10-05T14:30:00); const tsFromDate d.getTime(); // 时间戳转 Date const dateFromTs new Date(tsFromDate); // 秒级时间戳处理 const secondsTs Math.floor(ts / 1000); const dateFromSeconds new Date(secondsTs * 1000);接口开发里经常遇到秒级时间戳记得先乘 1000 再传给new Date()。4.4 时间比较与差值const start new Date(2023-10-05T00:00:00); const end new Date(2023-10-06T12:00:00); // 直接用 getTime 比较 console.log(start.getTime() end.getTime()); // 计算差值单位毫秒 const diffMs end.getTime() - start.getTime(); console.log(diffMs); // 129600000 console.log(diffMs / 1000); // 秒 console.log(diffMs / (1000 * 60)); // 分钟 console.log(diffMs / (1000 * 60 * 60)); // 小时 console.log(diffMs / (1000 * 60 * 60 * 24)); // 天直接用start end也能比较JavaScript 会自动调用valueOf()但显式用getTime()更清楚。5. 日期格式化与解析自己写还是用库原生Date没有一个好用的format方法这是时间函数在实际开发中最常遇到的痛点。5.1 手写一个 formatDate 函数自己封装的好处是无依赖、体积小、完全可控function formatDate(date, fmt YYYY-MM-DD HH:mm:ss) { const pad (num) String(num).padStart(2, 0); 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 fmt.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) String(map[match])); } // 使用示例 const d new Date(2023-10-05T14:30:00); console.log(formatDate(d)); // 2023-10-05 14:30:00 console.log(formatDate(d, YYYY年MM月DD日)); // 2023年10月05日5.2 解析字符串为 Date外部传入的字符串不一定规范建议先拆解再构造function parseDateString(str) { // 支持 2023-10-05 14:30:00 这类格式 const parts str.split(/[-: ]/).map(Number); return new Date(parts[0], parts[1] - 1, parts[2], parts[3] || 0, parts[4] || 0, parts[5] || 0); } console.log(parseDateString(2023-10-05 14:30:00).toString());这里把“字符串解析”和“内部时间函数”完全分开不依赖Date.parse()对格式的模糊处理。5.3 使用 dayjs 简化如果项目里已经用了 dayjs格式化只写一行import dayjs from dayjs; console.log(dayjs().format(YYYY-MM-DD HH:mm:ss)); console.log(dayjs(2023-10-05 14:30:00).format(YYYY-MM-DD));5.4 相对时间几分钟前、几天前不用额外库也可以实现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)}小时前; return ${Math.floor(diff / day)}天前; } console.log(timeAgo(new Date(Date.now() - 5 * 60 * 1000))); // 5分钟前 console.log(timeAgo(new Date(Date.now() - 2 * 60 * 60 * 1000))); // 2小时前6. 时区与国际化8 小时差从哪里来“日期差 8 小时”是时间函数中出现频率最高的问题本质是“时间字符串没有时区标记”时不同环境解析结果不同。6.1 分清 UTC 与本地时区const now new Date(2023-10-05T14:30:00); // 获取 UTC 时区的字段 console.log(now.getUTCFullYear()); console.log(now.getUTCHours()); // 获取本地时区的字段 console.log(now.getHours()); // 当前环境相对于 UTC 的偏移分钟数 console.log(now.getTimezoneOffset()); // 中国东八区为 -4806.2 toISOString 一定返回 UTCconst d new Date(2023-10-05T14:30:0008:00); console.log(d.toISOString()); // 2023-10-05T06:30:00.000Z console.log(d.toLocaleString()); // 本地时区展示Z表示 UTC 时间08:00表示东八区偏移。如果你存储的是toISOString()读取后想要显示为北京时间需要做本地格式化。6.3 推荐的时间存储规则后端存储统一用 UTC例如2023-10-05T06:30:00.000Z。接口传输统一用时间戳1696382400000避免字符串解析歧义。前端展示才转换本地时区用toLocaleString()或Intl.DateTimeFormat。不要在前端和后端之间直接传2023-10-05这种无时区字符串除非双方约定它代表本地日期且不涉及跨时区用户。6.4 使用 Intl.DateTimeFormat 做本地化const d new Date(2023-10-05T14:30:00); const formatter new Intl.DateTimeFormat(zh-CN, { year: numeric, month: 2-digit, day: 2-digit, hour: 2-digit, minute: 2-digit, second: 2-digit, hour12: false, timeZone: Asia/Shanghai, }); console.log(formatter.format(d));timeZone字段解决了“按特定时区展示”的问题这对多时区产品非常关键。7. 日期时间运算加天、加月、加小时时间运算在业务里非常常见比如订单 30 分钟未支付自动关闭、活动到月底结束、订阅到期日推送。7.1 原生方式set 和 get 配合function addDays(date, days) { const result new Date(date); result.setDate(result.getDate() days); return result; } function addHours(date, hours) { const result new Date(date); result.setTime(result.getTime() hours * 60 * 60 * 1000); return result; } function addMonths(date, months) { const result new Date(date); const d result.getDate(); result.setDate(1); result.setMonth(result.getMonth() months); result.setDate(Math.min(d, result.daysInMonth ? 31 : 31)); return result; }注意addMonths有边界问题1 月 31 日加 1 个月不应变成 3 月 3 日。上面示例先用setDate(1)跳过目标月份的天数检查再用setDate(Math.min(原日, 31))做兜底。实际生产环境建议直接用 dayjsimport dayjs from dayjs; const d1 dayjs(2023-01-31).add(1, month); console.log(d1.format(YYYY-MM-DD)); // 2023-02-287.2 计算某月的第一天和最后一天function getMonthRange(year, month) { const firstDay new Date(year, month - 1, 1); const lastDay new Date(year, month, 0); // 下个月的第0天就是本月最后一天 return { firstDay: formatDate(firstDay, YYYY-MM-DD), lastDay: formatDate(lastDay, YYYY-MM-DD), }; } console.log(getMonthRange(2023, 10));7.3 计算本周一和本周日function getWeekRange(date) { const day date.getDay() || 7; // 周日改为7周一为1 const monday new Date(date); monday.setDate(date.getDate() - day 1); monday.setHours(0, 0, 0, 0); const sunday new Date(monday); sunday.setDate(monday.getDate() 6); sunday.setHours(23, 59, 59, 999); return { monday, sunday }; } const range getWeekRange(new Date()); console.log(range.monday.toString()); console.log(range.sunday.toString());8. 定时任务与倒计时setInterval 的误差问题“时间函数”不只是 Date API还包括浏览器和 Node.js 提供的定时器函数。8.1 常见倒计时实现function startCountdown(targetTime, onTick, onFinish) { const timer setInterval(() { const remain targetTime.getTime() - Date.now(); if (remain 0) { clearInterval(timer); onFinish onFinish(); return; } const days Math.floor(remain / (1000 * 60 * 60 * 24)); const hours Math.floor((remain % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes Math.floor((remain % (1000 * 60 * 60)) / (1000 * 60)); const seconds Math.floor((remain % (1000 * 60)) / 1000); onTick onTick({ days, hours, minutes, seconds }); }, 1000); return timer; } const timer startCountdown( new Date(Date.now() 1000 * 60 * 60 * 25), (val) console.log(val) );8.2 为什么定时器会漂移浏览器和 Node.js 的setInterval受事件循环影响当主线程被长任务阻塞时回调可能延迟执行定时器回调本身执行也需要时间所以标称 1000ms 的间隔并不精确。更稳重的做法不要依赖setInterval每秒累加而是每次都用targetTime.getTime() - Date.now()重新计算剩余时间。上面倒计时示例已经采用这种思路能避免累积误差。8.3 关于清理定时器页面组件销毁、Node.js 进程退出、任务取消时必须调用clearInterval(timer)。不带清理的定时器会让回调一直执行造成内存泄漏和无效请求。9. 接口时间处理与批量任务设计后端接口和批量任务里时间函数通常不只服务于单个日期而是贯穿于数据分组、调度、重试策略等环节。9.1 接口入参统一使用时间戳// 请求示例传入时间戳范围 const payload { startTime: new Date(2023-10-01T00:00:0008:00).getTime(), endTime: new Date(2023-10-31T23:59:5908:00).getTime(), }; // 前端解析返回数据 const start new Date(payload.startTime).toISOString();9.2 按天分组统计function groupByDay(list) { const map new Map(); list.forEach((item) { const day formatDate(new Date(item.createdAt), YYYY-MM-DD); map.set(day, (map.get(day) || 0) 1); }); return Object.fromEntries(map); } const result groupByDay([ { createdAt: new Date().getTime() }, { createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000).getTime() }, ]); console.log(result);9.3 批量重试要注意时间窗口批量任务中常见的错误是“失败后立刻重试”导致时间集中打崩目标服务。通用的做法是增加退避间隔function delay(ms) { return new Promise((resolve) setTimeout(resolve, ms)); } async function batchWithRetry(tasks, maxRetry 3) { for (let i 0; i tasks.length; i) { let success false; for (let retry 0; retry maxRetry; retry) { try { await tasks[i](); success true; break; } catch (err) { await delay(1000 * Math.pow(2, retry)); // 1s, 2s, 4s } } if (!success) { console.error(任务 ${i} 失败); } } }这套思路把时间函数和工程容错结合起来了比单纯会写new Date()更贴近生产。10. 常见问题与排查方法问题现象可能原因排查方式解决方案日期显示为NaN字符串格式不规范new Date()无法解析用console.log(new Date(str).toString())单独验证先拆分字符串再构造 Date时间差 8 小时服务端返回 UTC 时间前端未做本地时区转换检查字符串结尾是否有Z或08:00统一传时间戳展示用toLocaleString()或Intl.DateTimeFormat月份少了 1 个月getMonth()返回 0-11检查代码里是否直接输出getMonth()输出时getMonth() 1星期几不对getDay()返回 0-60 代表周日打印getDay()排查按业务定义映射数组定时器重复执行未清理setInterval检查组件卸载或任务完成逻辑clearInterval统一出口toISOString()后时间变了这是正常行为它总是 UTC 时间确认业务是否要 UTC 标准存储用 ISO 字符串展示时再转本地化日期运算出现 2 月 31 日直接在 31 日上setMonth用 dayjs 或先setDate(1)使用add(1, month)或回退日期函数11. 最佳实践与使用建议写时间函数比“能跑”更重要的是“可维护”。这些建议来自大量业务项目中的通用经验内部存储统一使用时间戳或 UTC ISO 字符串展示时再格式化。封装自己的formatDate和parseDateString工具函数放在公共模块不要到处重复getFullYear()拼接字符串。项目引入时间库时确认体积moment.js 官方已宣布进入维护模式新项目更推荐 dayjs 或 date-fns。所有时间格式化必须通过padStart补零避免2023-1-5 9:30这种不规整结果。涉及“按周统计”“按月统计”的代码一定要把周起始日、月起始日写成明确的函数留给别人看懂。倒计时和定时任务要关注主线程阻塞不要在setInterval回调里执行重计算任务。接口返回值中的时间统一以时间戳或明确时区的 ISO 字符为准禁止传无时区的2023-10-05。测试时重点覆盖跨月、跨年、2 月 29 日、夏令时地区、时间戳秒和毫秒混乱。12. 总结与下一步时间函数的核心不是背 API而是建立一套可靠的处理链路创建Date时明确输入格式运算时用时间戳或 set 方法存储时保留时区信息展示时格式化和本地化。把这四步理清楚8 小时差、月份偏移、定时器漂移这些经典问题都容易定位。下一步建议这样验证在 Node.js 环境跑一遍第 4 节到第 7 节的示例代码把formatDate单独抽到一个date-utils.js文件准备几个测试用例覆盖跨年和月份补零在项目里搜索所有直接拼接日期字符串的地方换成统一工具函数如果项目使用 dayjs顺手把dayjs().add(1, month)的边界行为在本地测试一遍。时间函数的坑大多来自“隐式假设”假设字符串被正确解析、假设本地时区一样、假设月份 1 就是 1。把这些假设全部显式化你就能少写很多 bug。
返回列表