- 教程
【免费下载链接】jstips
This is about useful JS tips!
Unix 时间戳在日志记录、接口签名、缓存过期、埋点上报等场景中无处不在。本文以 JS Tips 仓库第 49 期中文文档(_posts/zh_CN/javascript/2016-02-26-extract-unix-timestamp-easily.md)为核心骨架,系统讲解获取当前时刻与指定日期 Unix 时间戳的多种写法、底层运行原理、时区与精度陷阱,以及实战选型建议。读完本文,你将掌握一套可读、可维护、可复用的时间戳获取方案,并理解Date.now()、getTime()、一元+操作符之间的等价关系与细微差别。
一、先明确概念:Unix 时间戳与 JavaScript Date 的单位
Unix 时间戳(Unix timestamp / Epoch time)定义为自 1970 年 1 月 1 日 00:00:00 UTC 以来经过的秒数,是一个整数(或含小数的实数)。
而 JavaScript 的Date对象在底层以毫秒为单位存储时间——Date内部保存的是自同一纪元以来的毫秒数。因此,从Date相关 API 拿到的原始数值是毫秒,要得到 Unix 时间戳(秒),必须除以 1000。这一单位差异是整篇文章所有代码里Math.floor(dateTime / 1000)一行的由来。
二、获取当前时刻的 Unix 时间戳:三种写法
原文档给出了一组等价的写法,核心套路都是:先拿到毫秒值,再除以 1000 取整。
1.Date.now()—— 最简单直接
const dateTime = Date.now(); const timestamp = Math.floor(dateTime / 1000);Date.now()是 ES5 引入的静态方法,直接返回当前时间的毫秒值,无需先构造Date实例。从实现层面可以推断:相比需要 new 对象的写法,它省去了对象分配的开销,因此原文档称之为"最简单最快的方式"(fastest way)是有依据的——它同时做到了写法最简与路径最短。
2.new Date().getTime()—— 传统等价写法
const dateTime = new Date().getTime(); const timestamp = Math.floor(dateTime / 1000);getTime()是Date.prototype上的实例方法,返回与Date.now()完全相同的毫秒值。二者的返回值等价,区别仅在于new Date()先创建了一个当前时刻的实例。若要追求极致的性能与更少的对象分配,优先选Date.now();在必须持有Date对象(例如后续还要调用其它Date方法)的场景下,再用getTime()。
3.+new Date()—— 一元加号技巧
const dateTime = +new Date(); const timestamp = Math.floor(dateTime / 1000);这是原文档强调的一个简洁技巧:在new Date()前面加一个一元+操作符,直接把Date对象"一元化"为毫秒数值,省去了显式调用.getTime()。
一元+也适用于指定日期:
const dateTime = +new Date('2012-06-08'); const timestamp = Math.floor(dateTime / 1000);4. 等价但更啰嗦的变体(了解即可)
以下写法在语义上与上述等价,但由于需要额外的方法调用或解析,并不推荐日常使用:
// 显式调用 valueOf()(一元 + 内部最终也是走到 valueOf) const timestamp = Math.floor(new Date().valueOf() / 1000); // Date.parse() 只解析字符串,不构造对象 const timestamp = Math.floor(Date.parse(new Date().toISOString()) / 1000);其中Date.parse()更适合解析已存在的日期字符串,而不是"取当前时间"——拿当前时间再序列化再解析,绕了一圈且存在精度损耗,属于反模式。
5. 统一换算:为什么用Math.floor
dateTime是毫秒,除以 1000 后可能产生小数(例如当前毫秒数不是整秒时)。Math.floor用于向零方向取整,得到秒级整数:
Math.floor(Date.now() / 1000); // 秒级整数 Date.now(); // 毫秒级整数(精度更高)若你还需要亚秒精度,可直接保留毫秒值或使用Date.now() % 1000获取毫秒余数,无需经过除法取整。
三、获取指定日期的 Unix 时间戳
1. 用字符串解析:YYYY-MM-DD与YYYY-MM-DDT00:00:00Z
原文档指出,将yyyy-mm-dd或YYYY-MM-DDT00:00:00Z形式的字符串传给Date构造函数即可解析指定日期:
const dateTime = new Date('2012-06-08').getTime(); const timestamp = Math.floor(dateTime / 1000);两种字符串形态的差异在于时区信息:
YYYY-MM-DD:纯日期形式。按 ECMAScript 规范的日期-时间字符串格式(ISO 8601 子集),date-only 形式按UTC解释,即视为 UTC 当天 00:00:00。YYYY-MM-DDT00:00:00Z:带T分隔符并显式声明Z(UTC 时区标记),含义完全确定,不存在解释歧义。
由于各版本规范对"无时区信息的字符串"处理存在差异,且历史上部分运行时实现并不完全一致,最稳妥的做法是显式携带时区信息(Z或+08:00),让解析结果与运行环境无关。
2. 时区陷阱:同样写"2012-06-08",结果可能不同
new Date('2012-06-08')与new Date('2012-06-08T00:00:00')(带时间但无时区标记)的解析基准可能不同:前者按 UTC,后者按本地时区解释。在非 UTC 时区(如东八区)下运行,二者会相差 8 小时,换算成秒就是 28800 秒的偏差。如果你的业务要求"某天在服务器时区下的零点",请务必明确时区语义,否则会出现时间戳偏移的隐性 bug。
3. 更可控的写法:数字参数构造函数与Date.UTC()
字符串解析受规范版本与运行时实现影响,若要完全确定,可用数字参数构造:
// 本地时区:2012-06-08 00:00:00(本地) const local = new Date(2012, 5, 8); // 注意:月份从 0 开始,6 月是 5 const timestampLocal = Math.floor(local.getTime() / 1000); // UTC:2012-06-08 00:00:00 UTC,与 '2012-06-08T00:00:00Z' 等价 const timestampUTC = Math.floor(Date.UTC(2012, 5, 8) / 1000);Date.UTC()是静态方法,接受与Date构造函数相同的数字参数(年、月从 0 计、日、时、分、秒),但始终按 UTC 计算并直接返回毫秒值,无需先构造实例——这是获取"某具体时刻 UTC 时间戳"最无歧义的方式。
四、底层原理:一元+、valueOf()与toNumber()
原文档专门解释了+new Date()的底层机制:运行时调用Date对象的valueOf()方法,然后一元+操作符对该返回值执行toNumber()(即规范中的 ToNumber 抽象操作)。
拆解如下:
new Date()产生一个Date实例;- 一元
+操作符要求把操作数转为数值,触发ToNumber抽象操作; - 对对象类型,
ToNumber会先执行ToPrimitive(hint 为number),此时Date对象优先调用valueOf(); Date.prototype.valueOf返回该日期自纪元以来的毫秒数;- 数值转换完成,得到毫秒值。
因此+new Date()、new Date().valueOf()、new Date().getTime()三者最终殊途同归,返回的都是同一个毫秒数。原文档引用了 ES5 规范中的Date.prototype.valueOf、一元+操作符与toNumber()相关章节,读者如有兴趣可进一步查阅 ECMAScript 规范对应条目(这些外部规范链接在原文档 _posts/zh_CN/javascript/2016-02-26-extract-unix-timestamp-easily.md 中有列出)。
五、精度与边界:毫秒换算、取整方式与 Y2038
1.Math.floor与一元+的组合是安全的
Math.floor(dateTime / 1000)对任意毫秒值都成立,因为毫秒值恒为正数,Math.floor等价于"去掉小数"。这一组合不存在负数方向的问题。
2. 警惕~~位运算取整:Y2038 隐患
部分开发者为了"更快"会写成~~(+new Date() / 1000),用双按位非替代Math.floor。仓库内另一篇 tip(_posts/zh_CN/javascript/2016-01-18-rounding-the-fast-way.md)专门警告过这条路的风险:~操作符会先把操作数截断为32 位有符号整数,因此~~对超出 ±2^31 范围的数值会溢出回绕。
Unix 时间戳恰好会踩中这个雷区:当时间超过2038 年 1 月 19 日 03:14:07 UTC(即著名的 Y2038 limit,32 位有符号秒数上限)后,~~会算出完全错误的负数。该文档给出了一个 2040 年的例子:
epoch = +new Date('2040-01-01') / 1000 + 0.123 // -> 2208988800.123 epoch_int = ~~epoch // -> -2085978496(回绕成负数!) console.log(new Date(epoch_int * 1000)) // -> Wed Nov 25 1903 17:31:44 UTC // 用 Math.floor 才是正确结果 epoch_flr = Math.floor(epoch) // -> 2208988800 console.log(new Date(epoch_flr * 1000)) // -> Sun Jan 01 2040 00:00:00 UTC结论很明确:获取 Unix 时间戳请坚持用Math.floor(dateTime / 1000),不要用~~。这既是原文档的写法,也规避了 32 位截断与 Y2038 风险。
3. 关于性能的理性态度
Date.now()不构造对象、不触发额外抽象操作,理论上开销最小;原文档也将其描述为最简单最快的途径。但"快"的程度取决于具体运行时与调用频次,若在极端性能敏感的循环中大量取时间戳,应以实测(如performance.now()对照基准)为准,而不是盲目迷信某一种写法。
六、实战场景与选型建议
- 日志与埋点打点:用
Date.now(),配合new Date().toISOString()输出人类可读时间。 - 缓存过期 / 会话有效期:秒级时间戳足够,用
Math.floor(Date.now() / 1000),并注意与后端约定的单位保持一致(秒 vs 毫秒是常见事故源)。 - 接口签名 / 防重放:需要"当前时刻"与"指定时刻"两种能力,分别用
Date.now()与new Date('2024-01-01T00:00:00Z').getTime()。 - 展示层格式化:拿到毫秒值后不要急着转秒,
new Date(ms)可以继续用于getFullYear()、getMonth()等日期分解。 - 跨时区确定性要求高:优先
Date.UTC(...)或带Z的 ISO 字符串,避免隐式本地时区解析。
七、小结
获取 Unix 时间戳在 JavaScript 中有多条等价路径,本文围绕 JS Tips 第 49 期文档(英文原版见 _posts/en/javascript/2016-02-26-extract-unix-timestamp-easily.md,另有 西班牙语版 与 繁体中文版)总结出三条准则:
- 当前时刻:首选
Math.floor(Date.now() / 1000),其次Math.floor(new Date().getTime() / 1000); - 指定日期:字符串用
YYYY-MM-DD或显式带Z的YYYY-MM-DDT00:00:00Z,追求绝对确定用Date.UTC(); - 底层一致:
+new Date()的魔法来自valueOf()与一元+的ToNumber转换,理解原理即可放心使用,但取整务必用Math.floor而非~~,以远离 Y2038 位溢出陷阱。
- 教程
【免费下载链接】jstips
This is about useful JS tips!
相关推荐
Android 1.6中的Date.now支持:Mars中的时间戳获取实现
Android 1.6中的Date.now支持:Mars中的时间戳获取实现 你是否在开发移动Web应用时遇到过Android 1.6设备上时间戳获取失败的问题?
前端移动开发Vite-Express 高级技巧:如何实现无 Vite 模式与性能优化
Vite Express 高级技巧:如何实现无 Vite 模式与性能优化 Vite Express 是一个将 Vite 与 Express 框架无缝集成的工具,
ReClip视频下载器监控告警:接入Uptime Kuma,服务掉线秒级通知
ReClip视频下载器监控告警:接入Uptime Kuma,服务掉线秒级通知 ReClip 是一款轻量级、可自托管的视频下载工具,支持把 YouTube、Tik
音视频后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考