时间戳与时区处理完全指南:从Unix时间到全球化应用
几年前我接手过一个跨境电商的订单系统,上线第二天客服就炸了:用户明明晚上 8 点下的单,后台记录的却是中午 12 点。差了整整 8 小时。我盯着代码看了一下午才反应过来——后端用 new Date() 直接存了服务器本地时间,而服务器跑在 UTC 时区,前端又按北京时间显示,两头各自为政,谁也不知道那串数字到底代表哪个时刻。
那次之后我对"时间"这东西彻底没了信任感。它看着简单,一个数字而已,但 UTC、本地时间、夏令时、Date 解析的浏览器差异……每一个都能让你加班到半夜。这篇文章就是我这些年踩坑攒下来的笔记,从 Unix 时间戳的底层讲到时区转换的实战,希望能帮你少熬几个夜。
先说一张我现在贴在工位上的对照表,这是后面所有内容的纲领:
| 表示方式 | 例子 | 适合干什么 | 踩坑点 |
|---|---|---|---|
| 秒级时间戳 | 1704412800 | Unix 标准、数据库、跨语言传输 | 别和毫秒混用,差 1000 倍 |
| 毫秒级时间戳 | 1704412800000 | JS 里 Date.now() 的原生单位 | 给后端时记得确认对方要秒还是毫秒 |
| UTC 时间 | 2024-01-05T12:00:00Z | 存储、API 返回的黄金标准 | 末尾那个 Z 千万别漏,漏了就成了本地时间 |
| ISO 8601 带偏移 | 2024-01-05T20:00:00+08:00 | 需要保留"用户当时在哪个时区"时 | 不同库解析行为不完全一致 |
| 本地时间字符串 | 2024/1/5 20:00:00 | 只给人看的展示层 | 永远别拿它做存储或计算 |
目录
时间戳基础
什么是Unix时间戳
Unix时间戳(Unix Timestamp)是从1970年1月1日00:00:00 UTC开始计算的秒数(或毫秒数)。
// 当前时间的Unix时间戳(毫秒)
const now = Date.now();
console.log(now); // 1704412800000
// 转换为秒(Unix标准)
const seconds = Math.floor(now / 1000);
console.log(seconds); // 1704412800
为什么选择1970年?
1970年1月1日被称为Unix纪元(Unix Epoch),选择这个日期的原因:
- 早期Unix系统:1970年是Unix系统开发的年代
- 技术限制:32位系统使用signed int存储秒数
- 合理范围:可以表示1901-2038年的时间
2038年问题
32位系统的时间戳上限:
// 最大值:2^31 - 1 = 2147483647 秒
const maxTimestamp = 2147483647;
const maxDate = new Date(maxTimestamp * 1000);
console.log(maxDate.toISOString());
// 2038-01-19T03:14:07.000Z
// 超过这个时间会溢出(32位系统)
解决方案:
- 使用64位系统
- 使用毫秒时间戳
- 现代语言都已支持更大范围
时间戳的优势
我为什么逢人就安利用时间戳存时间?因为它把"时区"这个最容易出岔子的维度直接消掉了——一个数字,全球解析结果完全一样,不存在歧义。
// 无歧义:这串数字在东京、纽约、上海解析出的都是同一个时刻
const timestamp = 1704412800000;
// 比较大小就是比数字,不用解析字符串
const time1 = 1704412800000;
const time2 = 1704499200000;
console.log(time2 > time1); // true
// 加减也直接做整数运算
const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
const tomorrow = timestamp + oneDay;
存储上它也省,一列整数就够了,比存一长串字符串高效得多。代价是它对人类不友好——1704412800000 这种东西你瞄一眼根本不知道是哪天,所以展示给用户前必须转换。这个"存数字、展示时才转"的取舍,是后面会反复出现的主线。
JavaScript日期处理
Date对象基础
// 创建Date对象的方式
// 1. 当前时间
const now = new Date();
// 2. 从时间戳创建
const date1 = new Date(1704412800000);
// 3. 从日期字符串创建
const date2 = new Date('2024-01-05');
const date3 = new Date('2024-01-05T12:00:00Z');
// 4. 从年月日创建
const date4 = new Date(2024, 0, 5); // 注意:月份从0开始
const date5 = new Date(2024, 0, 5, 12, 30, 0);
获取日期组件
const date = new Date('2024-01-05T12:30:45.123Z');
// 年月日
console.log(date.getFullYear()); // 2024
console.log(date.getMonth()); // 0 (1月)
console.log(date.getDate()); // 5
// 时分秒
console.log(date.getHours()); // 取决于本地时区
console.log(date.getMinutes()); // 30
console.log(date.getSeconds()); // 45
console.log(date.getMilliseconds()); // 123
// 星期
console.log(date.getDay()); // 0-6 (0是周日)
// UTC版本(不受时区影响)
console.log(date.getUTCHours()); // 12
console.log(date.getUTCDate()); // 5
设置日期组件
const date = new Date('2024-01-05');
// 设置年月日
date.setFullYear(2025);
date.setMonth(11); // 12月
date.setDate(25); // 25号
// 设置时分秒
date.setHours(14);
date.setMinutes(30);
date.setSeconds(0);
// UTC版本
date.setUTCHours(12);
console.log(date); // 2025-12-25T...
日期格式化
const date = new Date('2024-01-05T12:30:00Z');
// ISO 8601格式
console.log(date.toISOString());
// "2024-01-05T12:30:00.000Z"
// 本地化字符串
console.log(date.toLocaleString('zh-CN'));
// "2024/1/5 20:30:00" (假设在UTC+8时区)
console.log(date.toLocaleString('en-US'));
// "1/5/2024, 8:30:00 PM"
// 只显示日期
console.log(date.toLocaleDateString('zh-CN'));
// "2024/1/5"
// 只显示时间
console.log(date.toLocaleTimeString('zh-CN'));
// "20:30:00"
// 自定义格式
console.log(
date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
);
// "2024/01/05 20:30:00"
日期计算
// 日期加减
const today = new Date('2024-01-05');
// 加7天
const nextWeek = new Date(today);
nextWeek.setDate(today.getDate() + 7);
// 加1个月
const nextMonth = new Date(today);
nextMonth.setMonth(today.getMonth() + 1);
// 减1年
const lastYear = new Date(today);
lastYear.setFullYear(today.getFullYear() - 1);
// 计算时间差
const start = new Date('2024-01-01');
const end = new Date('2024-01-05');
const diff = end - start; // 毫秒差
const days = diff / (1000 * 60 * 60 * 24);
console.log(days); // 4
实用日期函数
// 获取月份天数
function getDaysInMonth(year, month) {
// month: 0-11
return new Date(year, month + 1, 0).getDate();
}
console.log(getDaysInMonth(2024, 1)); // 29 (2024是闰年)
console.log(getDaysInMonth(2023, 1)); // 28
// 判断是否闰年
function isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
// 获取某月第一天是星期几
function getFirstDayOfMonth(year, month) {
return new Date(year, month, 1).getDay();
}
// 格式化日期为 YYYY-MM-DD
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
console.log(formatDate(new Date())); // "2024-01-05"
时区详解
时区基础概念
UTC(协调世界时):
- 全球标准时间
- 不受夏令时影响
- 原子钟精确计时
GMT(格林威治标准时间):
- 基于地球自转
- 与UTC相差不到1秒
- 日常使用中可以等同
时区偏移:
// 获取本地时区偏移(分钟)
const offset = new Date().getTimezoneOffset();
console.log(offset); // -480 (UTC+8,北京时间)
// 负数表示东时区,正数表示西时区
常见时区
// UTC+0: 伦敦
// UTC+1: 巴黎、柏林
// UTC+8: 北京、上海、香港、新加坡
// UTC+9: 东京、首尔
// UTC-5: 纽约(标准时间)
// UTC-8: 洛杉矶(标准时间)
// 使用Intl API获取时区信息
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log(timeZone); // "Asia/Shanghai"
时区转换
// 方法1:使用toLocaleString
const date = new Date('2024-01-05T12:00:00Z');
// 转换为北京时间
console.log(
date.toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
})
);
// "2024/1/5 20:00:00"
// 转换为纽约时间
console.log(
date.toLocaleString('en-US', {
timeZone: 'America/New_York',
})
);
// "1/5/2024, 7:00:00 AM"
// 转换为东京时间
console.log(
date.toLocaleString('ja-JP', {
timeZone: 'Asia/Tokyo',
})
);
// "2024/1/5 21:00:00"
// 方法2:手动计算偏移
function convertToTimezone(date, offsetHours) {
const utc = date.getTime() + date.getTimezoneOffset() * 60000;
return new Date(utc + 3600000 * offsetHours);
}
const utcDate = new Date('2024-01-05T12:00:00Z');
const shanghaiTime = convertToTimezone(utcDate, 8);
console.log(shanghaiTime);
夏令时处理
// 夏令时会自动调整时区偏移
// 美国东部时间
const winter = new Date('2024-01-05T12:00:00');
const summer = new Date('2024-07-05T12:00:00');
console.log(
winter.toLocaleString('en-US', {
timeZone: 'America/New_York',
timeZoneName: 'short',
})
);
// "1/5/2024, 7:00:00 AM EST" (UTC-5)
console.log(
summer.toLocaleString('en-US', {
timeZone: 'America/New_York',
timeZoneName: 'short',
})
);
// "7/5/2024, 8:00:00 AM EDT" (UTC-4, 夏令时)
常见陷阱与解决方案
陷阱1:月份从0开始
// ❌ 错误:1月写成1
const wrong = new Date(2024, 1, 5);
console.log(wrong); // 2024-02-05 (2月5日)
// ✅ 正确:1月写成0
const correct = new Date(2024, 0, 5);
console.log(correct); // 2024-01-05
陷阱2:日期字符串解析
这个坑我亲自踩过,而且是在生产环境踩的。同一句 new Date('2024-01-05'),老版本 Safari 当本地时间解析,Chrome 却当 UTC——一个 bug 在我自己机器上死活复现不出来,最后是同事用 Mac 上的 Safari 才抓到。结论很简单:永远不要把不带时区信息的字符串丢给 new Date()。
// ❌ 不同浏览器可能有不同解析结果
const ambiguous = new Date('2024-01-05');
// 可能解析为本地时间或UTC时间
// ✅ 明确指定UTC
const explicit = new Date('2024-01-05T00:00:00Z');
// ✅ 或使用Date.UTC
const utc = new Date(Date.UTC(2024, 0, 5));
陷阱3:时区混淆
// ❌ 混淆本地时间和UTC
const date = new Date('2024-01-05T12:00:00');
console.log(date.getHours()); // 取决于本地时区!
// ✅ 明确使用UTC方法
const utcDate = new Date('2024-01-05T12:00:00Z');
console.log(utcDate.getUTCHours()); // 始终是12
陷阱4:浮点数精度
// ❌ 浮点数运算可能不精确
const days = 86400000; // 一天的毫秒数
const result = Date.now() + days * 0.5;
// ✅ 使用整数运算
const halfDay = Math.floor(days / 2);
const result2 = Date.now() + halfDay;
陷阱5:月末日期计算
// ❌ 错误:可能溢出到下个月
const date = new Date(2024, 0, 31); // 1月31日
date.setMonth(date.getMonth() + 1);
console.log(date); // 2024-03-02 (2月只有29天,溢出到3月)
// ✅ 正确:先设置为1号
function addMonths(date, months) {
const d = new Date(date);
const targetMonth = d.getMonth() + months;
d.setMonth(targetMonth);
// 如果溢出,设置为目标月的最后一天
if (d.getMonth() !== targetMonth % 12) {
d.setDate(0); // 设置为上个月的最后一天
}
return d;
}
陷阱6:跨越DST边界
夏令时是我心里永远的痛。有个定时任务设在凌晨 2:30 跑,平时好好的,结果某年三月那个周日它根本没触发——因为那天 2 点直接跳到了 3 点,2:30 这个时刻在物理上压根不存在。秋天反过来更阴间,1 点到 2 点会走两遍,任务有概率跑两次。所以涉及定时调度,要么全程用 UTC 排期,要么老老实实用 IANA 时区数据库,别自己拿偏移量硬算。
// 夏令时切换时,某些时间不存在或重复
// 2024年美国夏令时开始:3月10日凌晨2点跳到3点
const dst = new Date('2024-03-10T02:30:00');
// 这个时间实际上不存在
// 夏令时结束:11月3日凌晨2点退回到1点
const std = new Date('2024-11-03T01:30:00');
// 这个时间重复出现两次
实战案例
案例1:倒计时组件
class Countdown {
constructor(targetDate, callback) {
this.targetDate = new Date(targetDate).getTime();
this.callback = callback;
this.timer = null;
}
start() {
this.update();
this.timer = setInterval(() => this.update(), 1000);
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
update() {
const now = Date.now();
const diff = this.targetDate - now;
if (diff <= 0) {
this.stop();
this.callback({ days: 0, hours: 0, minutes: 0, seconds: 0 });
return;
}
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
this.callback({ days, hours, minutes, seconds });
}
}
// 使用
const countdown = new Countdown('2024-12-31T23:59:59Z', (time) => {
console.log(`${time.days}天 ${time.hours}时 ${time.minutes}分 ${time.seconds}秒`);
});
countdown.start();
案例2:日期范围选择器
class DateRangePicker {
constructor() {
this.startDate = null;
this.endDate = null;
}
setStart(date) {
this.startDate = new Date(date);
this.startDate.setHours(0, 0, 0, 0);
}
setEnd(date) {
this.endDate = new Date(date);
this.endDate.setHours(23, 59, 59, 999);
}
getDays() {
if (!this.startDate || !this.endDate) return 0;
const diff = this.endDate - this.startDate;
return Math.ceil(diff / (1000 * 60 * 60 * 24));
}
includes(date) {
const checkDate = new Date(date);
return checkDate >= this.startDate && checkDate <= this.endDate;
}
toISO() {
return {
start: this.startDate?.toISOString(),
end: this.endDate?.toISOString(),
};
}
}
// 使用
const picker = new DateRangePicker();
picker.setStart('2024-01-01');
picker.setEnd('2024-01-31');
console.log(picker.getDays()); // 31
console.log(picker.includes('2024-01-15')); // true
案例3:相对时间显示
function getRelativeTime(date) {
const now = Date.now();
const timestamp = new Date(date).getTime();
const diff = now - timestamp;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
if (seconds < 60) {
return '刚刚';
} else if (minutes < 60) {
return `${minutes}分钟前`;
} else if (hours < 24) {
return `${hours}小时前`;
} else if (days < 7) {
return `${days}天前`;
} else if (days < 30) {
const weeks = Math.floor(days / 7);
return `${weeks}周前`;
} else if (months < 12) {
return `${months}个月前`;
} else {
return `${years}年前`;
}
}
// 使用Intl.RelativeTimeFormat(现代浏览器)
function getRelativeTimeIntl(date, locale = 'zh-CN') {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const now = Date.now();
const timestamp = new Date(date).getTime();
const diff = (timestamp - now) / 1000;
const units = [
{ unit: 'year', seconds: 31536000 },
{ unit: 'month', seconds: 2592000 },
{ unit: 'week', seconds: 604800 },
{ unit: 'day', seconds: 86400 },
{ unit: 'hour', seconds: 3600 },
{ unit: 'minute', seconds: 60 },
{ unit: 'second', seconds: 1 },
];
for (const { unit, seconds } of units) {
if (Math.abs(diff) >= seconds) {
const value = Math.floor(diff / seconds);
return rtf.format(value, unit);
}
}
return rtf.format(0, 'second');
}
console.log(getRelativeTime(Date.now() - 60000)); // "1分钟前"
console.log(getRelativeTimeIntl(Date.now() + 86400000)); // "明天"
案例4:工作日计算
function getWorkdays(startDate, endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
let count = 0;
const current = new Date(start);
while (current <= end) {
const day = current.getDay();
// 0是周日,6是周六
if (day !== 0 && day !== 6) {
count++;
}
current.setDate(current.getDate() + 1);
}
return count;
}
function addWorkdays(date, days) {
const result = new Date(date);
let added = 0;
while (added < days) {
result.setDate(result.getDate() + 1);
const day = result.getDay();
if (day !== 0 && day !== 6) {
added++;
}
}
return result;
}
console.log(getWorkdays('2024-01-01', '2024-01-31')); // 23个工作日
console.log(addWorkdays('2024-01-01', 5)); // 5个工作日后
最佳实践
1. 始终使用UTC存储
// ✅ 数据库存储UTC时间戳
const timestamp = Date.now();
await db.insert({ created_at: timestamp });
// ✅ API返回ISO 8601格式(UTC)
const data = {
created_at: new Date().toISOString(),
};
// 前端显示时转换为本地时间
const localTime = new Date(data.created_at).toLocaleString();
2. 使用库处理复杂时区
// 推荐库:
// - date-fns: 轻量、函数式
// - moment.js: 功能丰富(较大)
// - dayjs: moment.js的轻量替代
// - Luxon: moment.js作者的新作品
// date-fns示例
import { format, addDays, isBefore } from 'date-fns';
const date = new Date();
const formatted = format(date, 'yyyy-MM-dd HH:mm:ss');
const tomorrow = addDays(date, 1);
3. 验证用户输入
function isValidDate(dateString) {
const date = new Date(dateString);
return !isNaN(date.getTime());
}
function parseDate(input) {
const date = new Date(input);
if (isNaN(date.getTime())) {
throw new Error('Invalid date');
}
return date;
}
4. 处理边界情况
// 考虑闰年、月末、夏令时等边界情况
function isSameDay(date1, date2) {
return date1.toDateString() === date2.toDateString();
}
function startOfDay(date) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
return d;
}
function endOfDay(date) {
const d = new Date(date);
d.setHours(23, 59, 59, 999);
return d;
}
5. 国际化考虑
// 使用Intl API进行本地化
function formatDateLocalized(date, locale, options = {}) {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
...options,
}).format(date);
}
console.log(formatDateLocalized(new Date(), 'zh-CN'));
// "2024年1月5日"
console.log(formatDateLocalized(new Date(), 'en-US'));
// "January 5, 2024"
写在最后
如果这篇文章你只记住一句话,那就记这条:存时间一律存 UTC 时间戳,只有在展示给用户的那一刻才转成本地时区。 这是我被那个差 8 小时的 bug 折磨出来的铁律,这些年再没因为时区背过锅。中间层(数据库、API、消息队列)但凡碰时间,全用时间戳或带 Z 的 ISO 字符串,谁也别擅自加时区,等到了最外层的 UI 再统一转换。
剩下的几条经验,按我踩坑的惨烈程度排:日期字符串永远带时区信息,别赌浏览器怎么解析;定时任务遇到夏令时要么走 UTC 要么用 IANA 时区库;复杂的时区运算别手撸,直接上 date-fns 或 Luxon,自己算偏移量迟早出事。
顺带提一句,文末那个时间戳转换工具是我们自己常用的——它纯粹在你浏览器里跑,时间戳不会发到任何服务器,调试线上数据时不用担心敏感信息泄露。需要看不同时区或做日期加减的话,下面两个也一并放这了。
时间这东西,看懂原理只是第一步,真正的功力是养成"碰到时间先问一句它是哪个时区"的肌肉记忆。祝你少加班。
常见问题速查
| 问题 | 解决方案 |
|---|---|
| 月份从0开始 | 使用常量或注释提醒 |
| 时区混淆 | 明确使用UTC方法 |
| 日期格式不一致 | 统一使用ISO 8601 |
| 夏令时问题 | 使用库或IANA时区 |
| 性能问题 | 缓存计算结果 |
相关工具
关键词: 时间戳, 时区, JavaScript Date, UTC, 国际化, 日期处理
更新时间: 2026-01-05
我们是一支开发者小团队,负责构建和维护 ToolsForge —— 150+ 个完全在浏览器本地运行、隐私优先的工具。这些文章来自我们日常的工程实践,而不是纸上谈兵。
了解我们相关阅读
前端性能优化实战:从加载到渲染的完整指南
深入解析前端性能优化技巧,从资源加载、代码分割到渲染优化,包含实战案例和性能监控方案,助力构建高性能Web应用
浏览器API完全指南:FileReader、Canvas、Web Workers深度解析
深入探索现代浏览器API,涵盖文件处理、Canvas绘图、Web Workers多线程、IndexedDB存储等核心技术,助力构建强大的Web应用
哈希算法与数据安全实践:MD5、SHA家族完全指南
深入解析哈希算法原理和应用场景,涵盖MD5、SHA-1、SHA-256等算法特点,密码哈希最佳实践,以及文件完整性校验实战