零散专题31 JS中的日期对象

最近做一个重构的业务,使用到了Mongo数据库,在接口中需要返回数据的的更新时间。之前数据库中都使用了时间戳记录时间,返给前端的数据直接使用时间戳进行格式化,并且查询条件也是由时间戳进行比对。

没想到,做数据写入接口的同事,将这个字段由时间戳改为了使用Mongo自带的标准的日期格式:ISODate("2016-01-01T00:00:00Z")

突然发现自己一时间对JS中的日期对象、对时间的各种表示方法并没有一个比较清晰的脉络。所以想用一点时间对这块知识做一个梳理、总结。

1 几种时间标准

首先要明确,时间标准和时刻的关系。

我理解,时刻对于整个地球来说是唯一的。就在我写下这行文字的这一时刻,无论是美国、老挝还是柬埔寨,大家都经历了同一时刻,对于时间轴的刻度,全球是唯一的。

但是在不同的时区的人,代表这一刻的时间是各不相同的,我所在的北京时间是晚上21:26,但是对于美国、老挝、柬埔寨可能时间各不相同。

时区是地球上的区域使用同一个时间定义。以前,人们通过观察太阳的位置(时角)决定时间,这就使得不同经度的地方的时间有所不同(地方时)。1863年,首次使用时区的概念。时区通过设立一个区域的标准时间部分地解决了这个问题。

不同给的时区有对应的时间标准,对于中国的开发者来说,常见的时间标准有GMT/UTC/CST

(1)GMT

GMT,格林尼治标准时间,是指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义为通过那里的经线。但是格林尼治时间本身有一些缺陷,并不准确,已经被原子钟报时的协调世界时(UTC)替代。

(2)UTC

刚才提到了UTC,协调世界时,是目前最主要的是世界时间标准,以原子时为基础。它的精确度比GTM更高,但是对于大多数用途,UTC时间被认为能够与GMT时间互换。

(3)CST

CST,是北京时间,也叫做中国标准时间,比UTC时间快八个小时,与澳门、香港、台北、吉隆坡、新加坡等地的标准时间相同。

(4)小结

也就是说,UTC时间是GMT的时间的升级版,基本可以认为是相同的,有如下的关系:

1
GTM ≈ UTC = CST + 8

2 ISO时间

ISO时间与前面提到的时间标准属于不同的概念,它并不是一种时间标准,二是一种时间的表示方法。

ISO时间的全称是ISO 8601,是国际标准化组织的日期和时间的表示方法。目前是2004年12月1日发行的第三版标ISO8601:2004

标准中规定了日期和时间的组合表表示法,只使用数字为基本格式,使用冒号间隔开小时、分、秒,小时、分和秒都用2位数表述。日期和时间之间要一个大写字母T

如果时间与UTC时间相同,那么(不加空格地)在时间最后加一个大写字母Z,比如下午2点30分5秒表示为14:30:05Z,其他时区用实际时间加上时差表示,比如当地的UTC+8时间表示为22:30:05+08:00

所以:

1
2
3
4
5
2004-05-03T17:30:08+08:00
// 北京时间2004年5月3日下午5点30分8秒

2004-05-03T17:30:08Z
// UTC时间2004年5月3日下午5点30分8秒

前面提到的,Mongo里的时间ISODate("2016-01-01T00:00:00Z")使用的就是ISO表示法的UTC时间。

3 时间戳

时间戳也是常见的表达时间的方式,Unix时间戳指的是格林威治时间1970-01-01 00:00:00(北京时间1970-01-01 08:00:00)其到现在(或某个指定时间)的总秒数。它用来唯一的标识某一刻的时间。

那么,北京时间的时间戳和UTC的时间戳是一样的吗?

当然是一样的,前面提到了,时间戳是用来唯一的标识某一刻的时间,它是独一无二的。虽然北京时间和UTC时间相差了8个小时,但是正如上面提到的,北京时间和UTC时间计算时间戳的起点也相差了8个小时。

所以时间戳是没有时区的区别的。

4 JavaScript中的日期对象

ECMAScript中的Date类型使用UTC时间的1970年1月1日午夜(零时)开始经过的毫秒数(注意是毫秒)来保存日期。

创建一个日期对象:

1
const now = new Date()

在调用Date构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。如果想根据特定的日期和时间创建日期对象,必须传入表示该日期的毫秒时间戳。

构造函数也可以接受一个字符串返回相应的日期对象,这是因为构造函数在后台调用了Date.parse()方法,将字符串转换为了对应日期的毫秒数。

Date可以接受多种日期格式,常用的有以下几种:

1
2
3
4
5
6
7
8
9
10
11
// 时间戳
new Date(1559569974691)

// 月/日/年
new Date('6/13/2004')

// 年-月-日 时:分:秒
new Date('2019-03-18 00:00:00')

// ISO 8601 格式日期
new Date('2019-03-18T00:00:00.000Z')

在使用Date构造函数创建一个日期对象时,日期和时间都是基于本地时区而非GMT来创建的:

1
2
new Date()
// Mon Jun 03 2019 21:56:47 GMT+0800 (中国标准时间)

上面的代码创造的日期对象是基于本地时区,也就是CST时区(为GMT+8)。当然JavaScript也提供了各种方法来进行各种事件标准间的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const now = new Date()
// Mon Jun 03 2019 22:01:55 GMT+0800 (中国标准时间)

now.toString()
// "Mon Jun 03 2019 22:01:55 GMT+0800 (中国标准时间)"

now.getTime()
// 1559570515646

now.toLocaleString()
// "2019/6/3 下午10:01:55"

now.toGMTString()
// "Mon, 03 Jun 2019 14:01:55 GMT"

now.toUTCString()
// "Mon, 03 Jun 2019 14:01:55 GMT"

now.toISOString()
// "2019-06-03T14:01:55.646Z"

Date对象的各种API可以查看MDN的文档,要注意的是日期对象的valueOf方法是取时间戳的值,效果与getTime是相同的。日期对象转换为数字,结果就是其时间戳

1
2
now.valueOf() === now.getTime()
true

日期对象进行比较时,如果是两个对象进行比较,结果永远是不等的,原因和你直接比较两个{}不等是相同的,它们指向的不同的内存地址。但是可以将它们转换为字符串或者数字(时间戳)进行比较,而且在进行大于或者小于的比较时,是不用进行转换可以直接进行比较的(我猜测执行了隐式的转换,先转换为了数字,实际进行比较的是时间戳的值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var startDate1 = new Date("02/10/2012");
var startDate2 = new Date("02/10/2012");
var startDate3 = new Date("01/10/2012");

startDate1 === startDate2
// false

startDate1.toISOString() === startDate2.toISOString()
// true

+startDate1 === +startDate2
// true

startDate1 > startDate3
// true

对于日期对象,比较简单的转换和使用完全可以使用原生API实现,稍微复杂的一些可以借助Moment.jsdate-fns,后者是纯函数,是不变的,而且更容易做到按需引入,所以更加推荐。

参考