5

这篇文章的目的主要是讲解JavaScript中的Date对象,同时解答下面几个困惑我很久的问题:

  • 可以解析的表示时间的字符串格式有哪些?
  • 时区TimeZone
  • 进位问题

名词约定

表示时间的词很多,比如时间日期时刻等,为了便于下面的讲解以及概念的统一,对于下面这种格式

YYYY-MM-DDTHH:mm:ss.sssZ

现在统一约定:

  • YYYY-MM-DD: 也就是年月日部分使用日期这个词;
  • HH:mm:ss.sss: 也就是时分秒部分使用时刻这个词;
  • YYYY-MM-DDTHH:mm:ss.sssZ: 也就是说并不特指日期时刻时,我们用时间这个词;

其它名词约定:

  • 时间对象:通过Date构造函数创建的对象;
  • 内部插槽:指官网文档中的internal slot
  • 计算机时区:指计算机设置的时区,而不是计算机所在地区的时区;

虽然JavaScript用了Date这个单词,这个单词翻译过来是日期,但是从上面的约定来看,翻译成时间这个词或许更为合适。

至于为什么采用上面这种字符串格式,而不是YYYY/MM/DD HH:mm:ss或者其它格式进行说明的原因下面会提及。

另外,下面所有代码的运行环境为Chrome浏览器,并且注释部分不一定代表返回值。

内容结构

文章将会从以下几个方面进行讲解:

  • Date概述
  • Date作为构造器的用法;
  • Date作为函数的用法;
  • Date上的静态方法;
  • getter相关函数;
  • setter相关函数;

Date概述

通过把Date作为构造函数使用,可以创建一个时间对象,这个时间对象表示时间长河中的一个时间点。时间对象内部有一个名为[[DateValue]]的内部插槽,这个内部插槽里面存储的是一个时间戳。这个时间戳表示的是以世界协调时的1970年1月1日0时0分0秒0毫秒作为起始的毫秒数。

问:不知道是否有人有过疑问,时间戳和时区相关吗,同一时间不同时区获取的时间戳一样吗?
答:根据上面时间戳的规定,时间戳就是从世界协调时开始算的,所以可以说时间戳和时区有关,这个时区就是世界协调时。但也是因为这个时区是定死的,所以不同的时区在同一时间获取到的时间戳都是一样的。

Date作为构造器的用法

JavaScript中,我们经常会通过参数的类型和个数让函数作出不同的处理,如:

function justForExample () { // 这个例子只是起说明作用,并无实际意义
    let length = arguments.length
    if (length === 0) return 'zero parameter'
    if (length === 1) return arguments[0]
    if (length === 2) return arguments[0] + arguments[1] // 两个参数做加法
    if (length === 3) return arguments[0] * arguments[1] * arguments[2] // 三个参数做乘法
    // ...
}

Date也是通过判断参数的类型和个数来进行不同处理的。Date可以处理的参数的个数是07个。

当参数的个数是0的时候,返回一个时间对象,[[DateValue]]内部插槽的值代表以世界协调时为基准的当前时间的时间戳。

当参数的个数是1的时候,还需根据参数的类型做进一步判断:

  1. 如果参数的类型是数字,返回一个时间对象,[[DateValue]]内部插槽的值就是这个数字,当然如果这个数据不合法或者超出边界的话,这个插槽的值就不一定是这个数字了。未免本文过于啰嗦,本文不讨论数字不合法或者超出边界等其它情况,有兴趣的可以参阅规范文档。
  2. 如果参数的类型是字符串,就会调用Date上的静态方法parse,然后把返回值作为[[DateValue]]的值。具体详见下面的Date.parse

当参数的个数大于2的时候,就会使用下面这种形式:

// 从左到右参数分别代表年,月,日,时,分,秒,毫秒
Date (year, month [,date [, hours [, minutes [, seconds [, ms ]]]]])

上面这种形式接受27个参数。日的默认值是1,时,分,秒,毫秒的默认值是0

需要注意的是:

  1. 月份是从0开始算的,也就是0代表1月1代表2月...11代表12月
  2. 我在文章的开始说过一个进位的问题。我们知道当两个十进制的数相加时,如果低位超出该位的最大值,那么就会向高位进位,以使该位的值在合法范围之内。同样,在Date构造器以上述形式调用的时候,就会发生自动进位,也就是如果该位的值已经超出了该位的最大值,并不会报错,而是自动执行进位操作:

    // 进位
    new Date(2018, 8, 34) // Thu Oct 04 2018 00:00:00 GMT+0800 (中国标准时间)
    // 退位,类似于数学中的减法,也是可以的
    new Date(2018, 8, -1) // Thu Aug 30 2018 00:00:00 GMT+0800 (中国标准时间)

    可以看到,第三个参数34表示月份中的天数,但是9月只有30天,所以34并不是9月合法的日期,所以就产生了进位,月份进入10月,然后日期是4日,也就是34-30。同理,当第三个参数是-1的时候,会退到8月份。

  3. 时区是计算机时区。也就是年,月,日,时,分,秒,毫秒代表的是计算机时区的时间。因此,不同的计算机返回的时间可能是不一样的:

    let date1 = new Date(2018, 8, 8, 8, 8, 8) // Sat Sep 08 2018 08:08:08 GMT+0800 (中国标准时间)
    date1.getTime() // 1536365288000
    
    // 然后,改了下计算机时区
    let date2 = new Date(2018, 8, 8, 8, 8, 8) // Sat Sep 08 2018 08:08:08 GMT+0300 (莫斯科标准时间)
    date2.getTime() // 1536383288000
    
    (date1 - date2) / 3600 / 1000 // 5,因为date1是东八区,date2是东三区,正好相差5个小时

Date作为函数的用法

Date作为普通函数调用时,并不会对参数进行处理,直接返回代表当前时间的字符串。

Date() // "Sat Aug 18 2018 17:26:16 GMT+0800 (中国标准时间)"
Date(2018, 9, 9) // "Sat Aug 18 2018 17:26:21 GMT+0800 (中国标准时间)"

Date上的静态方法

Date上的静态方法有三个:

  • Date.now()
  • Date.parse(string)
  • Date.UTC(year, month [, date [, hours [, minutes [, seconds [, ms ]]]]])

Date.now()

返回函数调用时,以世界协调时为基准的时间戳。

Date.parse(string)

Date.parse处理第一个参数,返回一个以世界协调时为基准的时间戳。
在上面介绍Date作为构造函数使用的时候,当参数的个数是1,并且类型是字符串时,会在内部调用Date.parse方法。
我们平时见过很多表示时间的格式:

"2018-08-08 08:08:08"
"2018/08/08 08:08:08"
"2018/8/8 8:8:8"
...

那么,JavaScript支持的格式有哪些?JavaScript是否支持上述全部格式呢?

根据文档,规范只定义了一种格式:YYYY-MM-DDTHH:mm:ss.sssZ,其中T代表时间的开始,Z代表时区,也就是世界协调时。时区还可以用或者-拼上HH:mm来表示。这也是我为什么在开头使用这种格式的原因。当字符串的格式不符合上述格式的时候,就交给具体的实现自己看着办了。

需要注意的是:

  1. 尽量使用规范规定的字符串格式,否则可能会出现不同的浏览器运行结果不一致的问题;
  2. 另外上述格式不是所有部分都有才算合法,可以省略某些部分,日期部分允许的格式如下:

    YYYY
    YYYY-MM
    YYYY-MM-DD

    时刻部分允许的格式如下:

    THH:mm
    THH:mm:ss
    THH:mm:ss.sss

    可以只使用上面的日期格式,也可以使用上面的任意一种日期格式+上面的任意一种时刻格式。月、日的默认值是"01",时、分、秒的默认值是"00",毫秒的默认值是"000"。时区缺省的时候,日期+时刻的格式代表的是计算机时区。

    // 只有日期格式
    new Date('2018-08-08') // Wed Aug 08 2018 08:00:00 GMT+0800 (中国标准时间)
    // 日期+时刻格式,时区默认是计算机时区
    new Date('2018-08-08 08:08:08') // Wed Aug 08 2018 08:08:08 GMT+0800 (中国标准时间)
    // 时区为东三区,我的电脑是在东八区,所以输出的时间是08+05,也就是13点
    new Date('2018-08-08 08:08:08+03:00') // Wed Aug 08 2018 13:08:08 GMT+0800 (中国标准时间)

    上面第一个例子,可以发现当只有日期格式的时候,字符串是按照世界协调时解析的,也就是世界协调时的2018年8月8日,所以东八区就变成了8点了。文档只规定了日期+时刻格式默认时区是计算机时区,只有日期的时候并没有规定用什么时区,所以尽量不要用日期格式。
    所以,就个人而言,我觉得应该尽量避免使用字符串格式来实例化一个时间对象。当想创建一个本地时区的时间对象时,可以使用上面Date作为构造函数接受27个参数的那种形式去实例化一个时间对象。

  3. 没有进位问题,相应部分超出合法值之外就会报错:

    new Date('2018-08-34') // Invalid Date

Date.UTC(year, month [, date [, hours [, minutes [, seconds [, ms ]]]]])

类似于上面Date作为构造函数时,使用27个参数的形式。不同之处在于:

  1. 依据的时区不同,该方法参考世界协调时,而不是计算机时区;
  2. 返回值不同,该方法返回以世界协调时为基准的时间戳,而不是一个时间对象。

所以,可以通过下述方式创建参数以计算机时区和世界协调时为基准的时间对象:

// 参数是以计算机时区为基准
new Date(2018, 8, 8) // Sat Sep 08 2018 00:00:00 GMT+0800 (中国标准时间)
// 参数是以世界协调时为基准
new Date(Date.UTC(2018, 8, 8)) // Sat Sep 08 2018 08:00:00 GMT+0800 (中国标准时间)

getter相关函数

获取一个时间对象的年、月、日、时、分、秒、毫秒等都有对应的方法,这里不再赘述,只简述几个注意点:

  1. 上述方法针对计算机时区和世界协调时都有对应的一系列方法,如获取小时:

    // Date.prototype.getHours() 计算机时区
    // Date.prototype.getUTCHours() 世界协调时
    let date = new Date(2018, 8, 8, 8) //Sat Sep 08 2018 08:00:00 GMT+0800 (中国标准时间)
    date.getHours() // 8
    date.getUTCHours() // 0
  2. Date.prototype.getTime()返回时间对象的内部插槽[[DateValue]]的值,也就是以世界协调时为基准的时间戳。

setter相关函数

getter相关函数一样,设置一个时间对象的年、月、日、时、分、秒、毫秒等都有对应的方法,这里同样不再赘述。只简述几个注意点:

  1. getter相关函数一样,上述方法针对计算机时区和世界协调时都有对应的一系列方法,如设置日期:

    // Date.prototype.setHours() 计算机时区
    // Date.prototype.setUTCHours() 世界协调时
    let date = new Date(2018, 8, 8, 8) //Sat Sep 08 2018 08:00:00 GMT+0800 (中国标准时间)
    date.setHours(9) // Sat Sep 08 2018 09:00:00 GMT+0800 (中国标准时间)
    date.setUTCHours(9) // Sat Sep 08 2018 17:00:00 GMT+0800 (中国标准时间)
  2. 设置月、日、时、分、秒、毫秒的时候,同样会有进位:

    let date = new Date(2018, 8, 8) // Sat Sep 08 2018 00:00:00 GMT+0800 (中国标准时间)
    
    date.setDate(34) // Thu Oct 04 2018 00:00:00 GMT+0800 (中国标准时间)

总结

希望上述内容对大家有所帮助。如果发现本文有什么错误,您可以在评论区留言。


luckness
6.2k 声望5.1k 粉丝