![daaae0ad3019567abccd222b538d0991.png](https://i-blog.csdnimg.cn/blog_migrate/1cb43b7fdca180e9b32b8df26760a2e5.jpeg)
本文不涉及任何中高级的技术知识,仅为面向业务的科普。
前些天在 cnode 上看到网友的抱怨 一个javascript日期的坑 ,内容如下:
![2fa7544380f6ee0d65a848bb7c0ccd53.png](https://i-blog.csdnimg.cn/blog_migrate/4921f3fda473c212a091d36cf5417a39.jpeg)
而回复也多表示,这个“坑”是来自 Java 的。那么问题来了:
- 这个“坑”真的来自 Java 吗?如果不是,又是来自何处?
- 这个“坑”我们在业务中真要直接面对吗?
- 关于时间、日期等,还有没有其它类似的“坑”?
Unix epoch 与 struct tm
大家都知道,Date.now() 或是 Date 对象的 getTime() 方法可以得到一个数字,俗称“时间戳”,或是“Unix 时间戳”。不少朋友还知道,常见的编程语言都能处理这个“时间戳”,也有不少在线工具可以帮我们将“时间戳”转为可阅读的时间日期,如 https://tool.lu/timestamp/ 及 https://tool.chinaz.com/tools/unixtime.aspx 等等。这个“时间戳”,英文名为 Unix epoch、POSIX time,本质就是给定时间在 UTC 1970年1月1日零点之后的秒数。注意,在 TS、JS 等语言里使用的是毫秒数。
在 C 语言中,不晚于 C89 / C90 标准,<time.h> 就已将 Unix epoch 对应的32位有符号整数定义为 time_t 。自然,C 语言也能将 Unix epoch 和具体日期的年月日时分秒等相互转换。struct tm 就是同样定义于 <time.h> 的结构体,mktime 和 localtime / gmtime 就是 time_t 和 struct tm 的转换函数。我们来看看 struct tm 的成员对象:
![6dd5d01e0d83a68e241badc6c5623c24.png](https://i-blog.csdnimg.cn/blog_migrate/db2f6ffd183bc503cb7c7c60f07ba265.jpeg)
原来早在80年代的编程语言中,月份就以 [0, 11] 来表示了,Java 不是这个“坑”的源头。
日期时间的格式化
显然,这个“坑”多出在日期时间的格式化上。如果我们需要的是可阅读的本地化时间、日期字符串,完全可以使用 Date 自带的方法。
![63179d9ae9f77672c8cf42162e2c27f3.png](https://i-blog.csdnimg.cn/blog_migrate/1362636281f49852cedd9edbcee652c0.jpeg)
而对于类似 YYYYMMDD 等自定义格式,moment.js 这个库可以让我们方便地在其与 Date 对象间双向转换。
![6a9f5f6dc01345591973f90933630570.png](https://i-blog.csdnimg.cn/blog_migrate/c6860016a3ea2a564b34a701e0cc1a0a.jpeg)
日期时间的加减计算
之前提到,Date 对象的 getTime() 方法可以取出毫秒单位的 Unix Epoch ,那么我们是不是可以基于此来取得给定时间之前或之后指定长的 Date 对象呢?当然可以。 同样,moment.js 也为我们提供了方便的计算方法。
![65ce51b5ecf4ed01f9347917598ebcce.png](https://i-blog.csdnimg.cn/blog_migrate/17cbc22a542b609033ed14cea315e650.jpeg)
虽然二者可以达到一致的效果,我们在日常工作中还是应尽量使用后者,代码中有意无意出现的 magic number 是随时可能爆炸的炸弹。你不能确定你的 86400 是不是多写或者少些了一个零,或是忘了乘以 1000 。
相对时间与日历时间
moment.js 还提供了带本地化的相对时间与日历时间输出,这使得日期时间在不追求极端精确显示的情况下具有更好的可阅读性。
![cb9d1ae706aee71f80b3b76966e4f1ed.png](https://i-blog.csdnimg.cn/blog_migrate/d1772387cff5047ffd3cb436fa91e7e7.jpeg)
如仅想使用本地化的相对时间功能,timeago.js 也是个不错的选择,本文不作详述。注意在简体中文环境下,moment.js 与 timeago.js 对于将来时刻的输出是有所不同的,moment.js 是“n 天内”,而 timeago.js 是"n 天后"。
如果说日期时间的加减计算不使用任何第三方库的问题不大,那么相对时间与日历时间就完全不建议这么做了。举个例子,时间单位的单复数就是大麻烦。更何况 moment.js 还自带几十种语言的本地化。
时长
看惯了便于阅读的相对时间显示,再看到惯用的 hh:mm:ss 的时长显示,是不是瞬间觉得可阅读行太差了呢?特别是当时长较长且策划 / 产品经理死也不同意使用相对时间来显示时,策划 / 产品经理往往会给出一些毫无常识、宛如智障的显示格式,比如... 01:02:03:04:05,这叫“一个月零两天三小时四分五秒”。这种显示,不是非蠢即坏,就是单纯的又蠢又坏。遗憾的是,包括 moment.js 在内的大部分日期时间库都没有时长相关的处理。这里我们使用 Humanize Duration 来解决这个问题。
![31f7df083bafb3a321519815665a27e3.png](https://i-blog.csdnimg.cn/blog_migrate/344b183397d8be65a5f8ff4610a5b626.jpeg)
Humanize Duration 同样支持多种语言的本地化。largest 参数决定着输出结果的最大单位数量,越多则越精确但也更啰嗦,一般我们使用 1 或 2。
其它
大家大概都遇到过时区的问题,不过我们了解了 Unix Epoch 之后就应该明白,在存储、传输时不要使用自己格式化产生的字符串或数字,而只使用 Unix epoch、 Date 对象、ISO格式的字符串(可由 Date 的 toISOString() 方法得到),在计算与显示时使用 Date 内置的方法和 moment.js 等第三方库,几乎所有的坑都可绕过。
另一些较为罕见的问题就是夏令时和闰秒,上述原则仍然有效。值得注意的是闰秒的显示秒数是 60,所以我们回头查看 strunct tm 的定义,tm_sec 的范围是 [0, 60] 。
总结
本文简单带过了 TS / JS 中的日期时间常见的问题及解决办法。回到文章开头的三个问题,精简回答如下:
1. 这个“坑”真的来自 Java 吗?如果不是,又是来自何处?
不,80年代的编程语言就是这么处理的。
2. 这个“坑”我们在业务中真要直接面对吗?
不需要,尽量使用 Date 自带的方法和第三方库。
3. 关于时间、日期等,还有没有其它类似的“坑”?
相对时间、日期时间与时长的本地化显示,时区、夏令时、闰秒。
遵循存储与传输是使用 Unix epoch、Date 对象等 raw data,计算与显示时使用 Date 内置的方法和第三方库的原则,能绕过几乎所有的坑。