JavaScript时间函数全攻略:从Date对象到时区与格式化实战
2026/8/31 4:08:52 网站建设 项目流程

时间函数这个题目,看起来是前端基本功,但真正能一次写对的人不多。问题主要集中在几个地方:Date对象的底层设计、字符串解析的不一致性、时区偏移到底由谁负责、以及格式化函数为什么总在边界条件“翻车”。

这次我们围绕 JavaScript 时间函数做一次系统梳理。不聊空洞概念,直接落到可运行代码、可复制的工具函数、业务场景中的真实用法,把常见坑位一次性踩清楚。

如果你最近正好在写倒计时、做报表统计、处理接口时间戳,或者被“日期差 8 小时”这类问题困扰,这篇文章建议收藏备用。

1. 时间函数核心能力速览

JavaScript 的时间处理并不是“一套 API 走天下”,而是由三部分组成:

能力层代表工具解决什么问题
原生基础Date对象、Date.now()Date.parse()获取时间、时间戳转换、基础比较
原生格式化toISOString()toLocaleString()Intl.DateTimeFormat转字符串、本地化显示
第三方封装dayjs、date-fns、moment.js链式操作、复杂格式化、相对时间
自定义工具函数formatDateparseDatedateAdd项目内统一格式,避免重复踩坑

什么场景该用哪一层,这是重点:

  • 一个时间戳存库、两个时间戳比大小:直接原生Date,不需要引库。
  • YYYY-MM-DD HH:mm:ss这种固定格式展示:写一个工具函数,或者用 dayjs。
  • 国际化、多语言、跨时区展示:优先Intl.DateTimeFormat
  • 老项目里到处都有moment,不一定要迁移,但新项目更推荐 dayjs 或 date-fns。
  • 定时、轮询、倒计时:setIntervalsetTimeout,注意清理和误差累积。

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());// 0

4.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()); // 中国东八区为 -480

6.2 toISOString 一定返回 UTC

const d = new Date("2023-10-05T14:30:00+08: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))做兜底。实际生产环境建议直接用 dayjs:

import dayjs from "dayjs"; const d1 = dayjs("2023-01-31").add(1, "month"); console.log(d1.format("YYYY-MM-DD")); // 2023-02-28

7.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:00+08:00").getTime(), endTime: new Date("2023-10-31T23:59:59+08: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-6,0 代表周日打印getDay()排查按业务定义映射数组
定时器重复执行未清理setInterval检查组件卸载或任务完成逻辑clearInterval统一出口
toISOString()后时间变了这是正常行为,它总是 UTC 时间确认业务是否要 UTC 标准存储用 ISO 字符串,展示时再转本地化
日期运算出现 2 月 31 日直接在 31 日上setMonth用 dayjs 或先setDate(1)使用add(1, "month")或回退日期函数

11. 最佳实践与使用建议

写时间函数,比“能跑”更重要的是“可维护”。这些建议来自大量业务项目中的通用经验:

  1. 内部存储统一使用时间戳或 UTC ISO 字符串,展示时再格式化。
  2. 封装自己的formatDateparseDateString工具函数放在公共模块,不要到处重复getFullYear()拼接字符串。
  3. 项目引入时间库时确认体积:moment.js 官方已宣布进入维护模式,新项目更推荐 dayjs 或 date-fns。
  4. 所有时间格式化必须通过padStart补零,避免2023-1-5 9:30这种不规整结果。
  5. 涉及“按周统计”“按月统计”的代码,一定要把周起始日、月起始日写成明确的函数,留给别人看懂。
  6. 倒计时和定时任务要关注主线程阻塞,不要在setInterval回调里执行重计算任务。
  7. 接口返回值中的时间,统一以时间戳或明确时区的 ISO 字符为准,禁止传无时区的"2023-10-05"
  8. 测试时重点覆盖:跨月、跨年、2 月 29 日、夏令时地区、时间戳秒和毫秒混乱。

12. 总结与下一步

时间函数的核心不是背 API,而是建立一套可靠的处理链路:创建Date时明确输入格式,运算时用时间戳或 set 方法,存储时保留时区信息,展示时格式化和本地化。把这四步理清楚,8 小时差、月份偏移、定时器漂移这些经典问题都容易定位。

下一步建议这样验证:

  • 在 Node.js 环境跑一遍第 4 节到第 7 节的示例代码;
  • formatDate单独抽到一个date-utils.js文件,准备几个测试用例覆盖跨年和月份补零;
  • 在项目里搜索所有直接拼接日期字符串的地方,换成统一工具函数;
  • 如果项目使用 dayjs,顺手把dayjs().add(1, "month")的边界行为在本地测试一遍。

时间函数的坑大多来自“隐式假设”:假设字符串被正确解析、假设本地时区一样、假设月份 1 就是 1。把这些假设全部显式化,你就能少写很多 bug。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询