JavaScript Date 的那些事

一、时间的"长相":你看到的时间有哪些形式?

在代码中,时间通常以两种形式存在:时间戳字符串

1.1 时间戳 (Timestamp)

时间戳是一个数字,表示从 1970 年 1 月 1 日 00:00:00 UTC(称为 Unix 纪元)到某个时刻经过的时间。

秒级 vs 毫秒级

  • 秒级时间戳 :Unix/Linux 系统常用,如 1734345000
  • 毫秒级时间戳 :JavaScript 使用的是这种,如 1734345000000

💡 提示:两者差 1000 倍,位数相差 3 位。秒级 10 位数,毫秒级 13 位数。

获取时间戳的方式

javascript 复制代码
// 获取当前时间的毫秒级时间戳
Date.now() // ✅ 推荐,简洁高效
new Date().getTime() // 等价,但多创建了一个 Date 对象
+new Date() // 隐式转换,不推荐(可读性差)

// 示例
console.log(Date.now()) // 1734345000000

1.2 字符串形式

时间字符串有多种格式标准,了解它们能帮你避免很多解析问题。

ISO 8601 标准(推荐)

国际标准化组织制定的格式,跨平台、跨语言通用

makefile 复制代码
2025-12-16T10:30:00.000Z
│    │  │ │ │  │  │   └── Z 表示 UTC 时区(也可以是 +08:00)
│    │  │ │ │  │  └────── 毫秒
│    │  │ │ │  └───────── 秒
│    │  │ │ └──────────── 分
│    │  │ └────────────── 时
│    │  └──────────────── 日
│    └─────────────────── 月
└──────────────────────── 年
javascript 复制代码
// ISO 字符串示例
'2025-12-16' // 只有日期部分
'2025-12-16T10:30:00' // 不带时区(会被当作本地时间)
'2025-12-16T10:30:00Z' // UTC 时间
'2025-12-16T10:30:00+08:00' // 带时区偏移(东八区)

RFC 2822 标准

常见于邮件头、HTTP 响应头。

javascript 复制代码
'Mon, 16 Dec 2025 10:30:00 GMT'
'Tue, 16 Dec 2025 18:30:00 +0800'

本地化字符串

因地区、语言而异,不建议用于数据传输,仅用于展示。

javascript 复制代码
'2025年12月16日' // 中文
'12/16/2025' // 美式(月/日/年)
'16/12/2025' // 欧式(日/月/年)
'December 16, 2025' // 英文

各格式适用场景

格式 适用场景 备注
时间戳 存储、计算、接口传输 最通用,无歧义
ISO 8601 接口传输、日志、数据库 标准格式,强烈推荐
RFC 2822 邮件、HTTP 头 特定协议使用
本地化字符串 仅用于 UI 展示 展示友好,但不能用于传输

二、创建 Date 对象的几种方式

2.1 无参构造:获取当前时间

javascript 复制代码
const now = new Date()
console.log(now) // Mon Dec 16 2024 18:30:00 GMT+0800 (中国标准时间)

2.2 时间戳构造

javascript 复制代码
// 毫秒级时间戳
const date1 = new Date(1734345000000)

// ⚠️ 如果后端返回秒级时间戳,记得乘 1000
const backendTimestamp = 1734345000 // 秒级
const date2 = new Date(backendTimestamp * 1000)

💡 常见错误:忘记转换秒级时间戳,导致日期显示为 1970 年。

2.3 字符串构造(重点:可靠性问题)

✅ ISO 格式最可靠

javascript 复制代码
// 这些在所有现代浏览器中表现一致
new Date('2025-12-16T10:30:00.000Z') // UTC 时间
new Date('2025-12-16T10:30:00+08:00') // 带时区
new Date('2025-12-16T10:30:00') // 本地时间

⚠️ 不可靠的字符串格式

javascript 复制代码
// 1. 纯日期字符串 - 时区行为不一致!
new Date('2025-12-16')
// Chrome/Firefox: 当作 UTC 00:00:00,转本地时间是 08:00:00
// Safari/iOS Safari: 当作本地时间 00:00:00
// 结果: 同一个字符串,不同浏览器可能相差 8 小时!

// 2. 斜杠分隔 - 部分浏览器不支持
new Date('2025/12/16') // 大部分浏览器 OK,但不是标准
new Date('12/16/2025') // 美式格式,依赖浏览器实现

// 3. 其他格式 - 结果不可预测
new Date('16-12-2025') // ❌ 可能返回 Invalid Date
new Date('12-16-2025') // ❌ 不同浏览器解析不同
new Date('December 16, 2025') // ⚠️ 能用,但依赖英文环境

// 4. 带中文 - 完全不支持
new Date('2025年12月16日') // ❌ Invalid Date

💡 最佳实践

javascript 复制代码
// 如果拿到非标准格式,先转成 ISO 或时间戳
const dateStr = '16/12/2025' // 欧式格式
const [day, month, year] = dateStr.split('/')
const safeDate = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`)

// 或者使用日期库(如 Day.js)处理

2.4 时间分量构造(月份坑点预告)

javascript 复制代码
// new Date(year, monthIndex, day, hours, minutes, seconds, ms)
const date = new Date(2025, 11, 16, 10, 30, 0)
//                         ↑
//                    注意: 11 表示 12 月!

这就引出了下一章的重点------月份从 0 开始的问题。


三、月份从 0 开始:到底是哪里的坑?

这是 JavaScript Date 最臭名昭著的设计之一。但很多人对它有误解,让我们来澄清。

3.1 澄清误区:字符串构造不受影响

javascript 复制代码
// ✅ 字符串中的 12 就是 12 月,没有任何问题
new Date('2025-12-16') // 12 月 16 日
new Date('2025-12-16T10:30') // 12 月 16 日 10:30

// ✅ ISO 字符串中的月份是正常的 1-12
new Date('2025-01-01') // 1 月 1 日
new Date('2025-12-31') // 12 月 31 日

3.2 真正的坑点:时间分量构造和 getter/setter

坑点 1:时间分量构造

javascript 复制代码
// ❌ 常见错误: 第二个参数以为是月份(1-12)
new Date(2025, 12, 16) // 错误! 这是 2026 年 1 月 16 日
new Date(2025, 1, 1)   // 错误! 这是 2 月 1 日,不是 1 月

// ✅ 正确写法: 第二个参数是 monthIndex(0-11)
new Date(2025, 11, 16) // 2025 年 12 月 16 日
new Date(2025, 0, 1)   // 2025 年 1 月 1 日

坑点 2:getMonth() 返回 0-11

javascript 复制代码
const date = new Date('2025-12-16')
console.log(date.getMonth()) // 11,不是 12!

// ✅ 想要得到正常月份,需要 +1
const month = date.getMonth() + 1 // 12

// ❌ 常见错误: 忘记 +1
const wrongMonth = date.getMonth() // 11 (错误!)
console.log(`当前月份是 ${wrongMonth} 月`) // "当前月份是 11 月"(实际是 12 月)

坑点 3:setMonth() 同样是 0-11

javascript 复制代码
const date = new Date('2025-06-16')

// ❌ 错误: 以为是设置为 12 月
date.setMonth(12) // 实际设置为下一年 1 月!

// ✅ 正确: 设置为 12 月
date.setMonth(11)

3.3 为什么设计成这样?

这是历史遗留问题。JavaScript 的 Date 对象设计借鉴了 Java 的 java.util.Date(Java 后来也废弃了这个类)。

可能的原因:

  1. 数组索引思维:月份可以直接作为月份名称数组的索引
javascript 复制代码
const months = ['January', 'February', 'March', 'April', 'May', 'June',
                'July', 'August', 'September', 'October', 'November', 'December']
const date = new Date()
console.log(months[date.getMonth()]) // 直接取月份名,无需 -1
  1. 早期设计仓促:JavaScript 只用了 10 天设计出来,很多决策没有深思熟虑

3.4 记忆口诀

字符串月份正常写,分量构造和 getter 要减一(或从 0 开始)。


四、Date 的其他常见坑

4.1 时区问题:本地时间 vs UTC 时间

javascript 复制代码
const date = new Date('2025-12-16T00:00:00Z') // UTC 时间午夜

// 本地时间方法(受时区影响)
date.getHours() // 8 (北京时间 +8 小时)
date.getDate() // 16
date.getDay() // 2 (周二)

// UTC 时间方法(不受时区影响)
date.getUTCHours() // 0
date.getUTCDate() // 16
date.getUTCDay() // 2

// toString 也不同
date.toString() 
// "Tue Dec 16 2025 08:00:00 GMT+0800 (中国标准时间)"

date.toISOString() 
// "2025-12-16T00:00:00.000Z"

date.toUTCString() 
// "Tue, 16 Dec 2025 00:00:00 GMT"

💡 提示:涉及跨时区场景时,统一使用 UTC 时间,避免混乱。

4.2 月末溢出:自动进位

javascript 复制代码
// Date 会自动处理溢出,这有时是 feature,有时是 bug
new Date(2025, 0, 32) // 1 月 32 日 → 2 月 1 日
new Date(2025, 1, 30) // 2 月 30 日 → 3 月 2 日(2025 非闰年)
new Date(2025, 11, 32) // 12 月 32 日 → 2026 年 1 月 1 日

// ✅ 利用这个特性获取某月最后一天
function getLastDayOfMonth(year, month) {
  // month 是 1-12,所以 month 作为 monthIndex 就是下个月
  // day 传 0 表示上个月最后一天
  return new Date(year, month, 0).getDate()
}

getLastDayOfMonth(2025, 2) // 28 (2 月最后一天)
getLastDayOfMonth(2024, 2) // 29 (2024 是闰年)
getLastDayOfMonth(2025, 12) // 31 (12 月最后一天)

更多特殊参数用法

javascript 复制代码
// day 传 0: 上个月最后一天
new Date(2025, 2, 0) // 2025-02-28 (2 月最后一天)

// day 传负数: 往前推
new Date(2025, 2, -1) // 2025-02-27 (2 月倒数第二天)
new Date(2025, 0, 0) // 2024-12-31 (去年最后一天)

// 获取上个月同一天
const today = new Date(2025, 2, 15) // 3 月 15 日
const lastMonth = new Date(2025, 1, 15) // 2 月 15 日

// 获取下个月第一天
const nextMonthFirst = new Date(2025, 3, 1) // 4 月 1 日

// ✅ 判断是否为闰年
function isLeapYear(year) {
  // 2 月 29 日如果存在,就是闰年
  return new Date(year, 1, 29).getDate() === 29
}

isLeapYear(2024) // true
isLeapYear(2025) // false

4.3 Invalid Date:如何判断日期是否有效

javascript 复制代码
const valid = new Date('2025-12-16')
const invalid = new Date('not a date')

// ✅ 方法 1: 检查 getTime() 是否为 NaN (推荐)
isNaN(valid.getTime()) // false
isNaN(invalid.getTime()) // true

// 方法 2: 转字符串检查
invalid.toString() // "Invalid Date"

// 方法 3: 使用 valueOf()
isNaN(invalid.valueOf()) // true

// ✅ 封装成函数
function isValidDate(date) {
  return date instanceof Date && !isNaN(date.getTime())
}

// 测试
isValidDate(new Date()) // true
isValidDate(new Date('invalid')) // false
isValidDate('2025-12-16') // false (不是 Date 对象)

4.4 Date 对象比较的陷阱

javascript 复制代码
const date1 = new Date('2025-12-16')
const date2 = new Date('2025-12-16')

// ❌ 错误: 直接比较会比较引用,而非值
date1 == date2 // false
date1 === date2 // false

// ✅ 正确: 转成时间戳比较
date1.getTime() === date2.getTime() // true
+date1 === +date2 // true (隐式转换)

// ✅ 比较大小(可以直接比较,会自动转时间戳)
date1 > date2 // false
date1 < date2 // false
date1 >= date2 // true

// 实际应用示例
const deadline = new Date('2025-12-31')
const today = new Date()
if (today > deadline) {
  console.log('已过期')
}

4.5 时间戳精度问题

javascript 复制代码
// JavaScript 的 Number 类型是 64 位浮点数
// 安全整数范围: -(2^53 - 1) 到 (2^53 - 1)
Number.MAX_SAFE_INTEGER // 9007199254740991

// 对于时间戳来说:
// 毫秒级时间戳在 2287 年之前都是安全的
const year2287 = 9999999999999
new Date(year2287) // Sat Nov 20 2286 17:46:39 GMT+0800

// ⚠️ 但如果后端返回微秒级或纳秒级时间戳,可能超出安全范围
const microTimestamp = 1734345000000000 // 微秒级(16位,超出安全范围)
// 这种情况需要用 BigInt 或字符串处理

// 解决方案示例
const microStr = '1734345000000000'
const millisTimestamp = Math.floor(Number(microStr) / 1000)
new Date(millisTimestamp)

4.6 格式化困难:没有内置 format 方法

javascript 复制代码
const date = new Date('2025-12-16T10:30:00')

// 想要 "2025-12-16" 格式? 只能手动拼接
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') // 别忘了 +1!
const day = String(date.getDate()).padStart(2, '0')
const formatted = `${year}-${month}-${day}` // "2025-12-16"

// 想要 "2025-12-16 10:30:00"? 继续拼...
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
const fullFormatted = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`

// 😫 每次都要写这么多代码

// ✅ 封装成工具函数
function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  const hours = String(date.getHours()).padStart(2, '0')
  const minutes = String(date.getMinutes()).padStart(2, '0')
  const seconds = String(date.getSeconds()).padStart(2, '0')
  
  return format
    .replace('YYYY', year)
    .replace('MM', month)
    .replace('DD', day)
    .replace('HH', hours)
    .replace('mm', minutes)
    .replace('ss', seconds)
}

// 使用
formatDate(new Date()) // "2025-12-16 10:30:00"
formatDate(new Date(), 'YYYY/MM/DD') // "2025/12/16"

4.7 Date 对象是可变的

javascript 复制代码
// ⚠️ Date 对象的 setter 方法会修改原对象
const date = new Date('2025-12-16')
console.log(date.toString()) // "Mon Dec 16 2025..."

date.setMonth(0) // 修改为 1 月
console.log(date.toString()) // "Thu Jan 16 2025..." (原对象被改变!)

// 这在函数传参时容易产生副作用
function addOneDay(date) {
  date.setDate(date.getDate() + 1)
  return date // ⚠️ 返回的是修改后的原对象
}

const original = new Date('2025-12-16')
const next = addOneDay(original)
console.log(original.toString()) // 原对象也变了!

// ✅ 正确做法: 先复制再修改
function addOneDaySafe(date) {
  const newDate = new Date(date.getTime()) // 复制
  newDate.setDate(newDate.getDate() + 1)
  return newDate
}

五、Day.js 解决了哪些痛点? (对比原生 Date)

Day.js 是一个轻量级的日期处理库(仅 2KB gzip),API 设计借鉴了 Moment.js,但更加现代和轻便。

5.1 痛点对比表

痛点 原生 Date Day.js
月份从 0 开始 getMonth() 返回 0-11,需要手动 +1 month() 也是 0-11,但 format('M') 自动输出 1-12
格式化日期 无内置方法,需手动拼接十几行代码 format('YYYY-MM-DD HH:mm:ss') 一行搞定
字符串解析不一致 不同浏览器结果不同,非 ISO 格式不可靠 统一解析,customParseFormat 插件支持任意格式
日期加减 需手动计算毫秒或用 setDate() 等方法 add(7, 'day')subtract(1, 'month') 语义清晰
日期比较 需转时间戳比较,代码冗长 isBefore()isAfter()isSame() 直观易读
不可变性 setMonth() 等方法会修改原对象,易产生 bug 所有操作返回新对象,原对象不变,避免副作用
时区处理 只有本地和 UTC,切换麻烦 timezone 插件轻松处理任意时区
相对时间 无内置支持 fromNow() 直接输出"3 天前"、"2 小时后"
体积 内置,0 成本 ~2KB gzip,极轻量

5.2 Day.js 基本使用

javascript 复制代码
import dayjs from 'dayjs'

// 创建日期对象
dayjs() // 当前时间
dayjs('2025-12-16') // 从字符串
dayjs(1734345000000) // 从时间戳
dayjs(new Date()) // 从 Date 对象

// 格式化 (最常用!)
dayjs().format('YYYY-MM-DD') // "2025-12-16"
dayjs().format('YYYY-MM-DD HH:mm:ss') // "2025-12-16 10:30:00"
dayjs().format('YYYY年MM月DD日') // "2025年12月16日"

// 日期加减
dayjs().add(7, 'day') // 7 天后
dayjs().subtract(1, 'month') // 1 个月前
dayjs().add(1, 'year') // 1 年后

// 日期比较
dayjs('2025-12-16').isBefore('2025-12-17') // true
dayjs('2025-12-16').isAfter('2025-12-15') // true
dayjs('2025-12-16').isSame('2025-12-16') // true

// 相对时间 (需要 relativeTime 插件)
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
dayjs.extend(relativeTime)
dayjs.locale('zh-cn')

dayjs().fromNow() // "几秒前"
dayjs().add(3, 'day').fromNow() // "3 天后"
相关推荐
犬大犬小7 小时前
从头说下DOM XSS
前端·javascript·xss
我的div丢了肿么办7 小时前
echarts中appendData的详细讲解
前端·javascript·vue.js
JamesGosling6667 小时前
async/defer 执行顺序全解析:从面试坑到 MDN 标准
前端·javascript
l1t7 小时前
Javascript引擎node bun deno比较
开发语言·javascript·算法·ecmascript·bun·精确覆盖·teris
over6977 小时前
掌控 JavaScript 的 this:从迷失到精准控制
前端·javascript·面试
天才熊猫君7 小时前
基于 `component` 的弹窗组件统一管理方案
前端·javascript
巴拉巴拉~~8 小时前
Flutter 通用按钮组件 CommonButtonWidget:多样式 + 多状态 + 交互优化
javascript·flutter·交互
豆苗学前端8 小时前
Vue 2 vs Vue 3 响应式原理深度对比(源码理解层面,吊打面试官)
前端·javascript·面试
TimelessHaze8 小时前
算法复杂度分析与优化:从理论到实战
前端·javascript·算法