一、时间的"长相":你看到的时间有哪些形式?
在代码中,时间通常以两种形式存在:时间戳 和字符串。
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 后来也废弃了这个类)。
可能的原因:
- 数组索引思维:月份可以直接作为月份名称数组的索引
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
- 早期设计仓促: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 天后"