new Date()?没那么简单!!

2,278 阅读5分钟

相信不少人都觉得Date对象很简单,不就是new一下,然后调用下原型方法嘛,so easy啦。恰巧我也是其中一员,平时上手即用,没怎么关注过这一块,直到最近一个小需求点把自己坑了,才醒悟过来,new Date()没那么简单。哎,血泪史,任重而道远。

问题的表现

请先思考一下,下面代码的输出分别是什么?

new Date('2020-06-05')

new Date('2020/06/05')

new Date('2020-6-5')

我之前一直认为,输出的都是Fri Jun 05 2020 00:00:00 GMT+0800 (中国标准时间),然而实际并不如我想。

这正是我踩坑的地方,直接使用Date构造函数解析日期字符串,然后与指定日期比大小,像下面的方式:

// 比如当前时间为2020年6月5日凌晨0点到早上8点之前
new Date('2020-06-05').getTime() < new Date().getTime() // 期望返回true,实际返回false

定位到问题后,遂查阅了相关资料,才发现Date()真没我想的这么简单。

问题的本质

ECMA262-RFC中的Date Objects详细规定了如何利用Date构造函数创建Date对象实例,因为本文使用的new Date(value),所以重点关注这一方式,其他方式在拓展部分会稍微讲一讲。

new Date(value)

  1. 生成一个新对象,对象的原型继承Date构造函数的原型对象;
  2. 新对象内部的[[class]]属性设置为Date
  3. 新对象内部的[[Extensible]]属性设置为true
  4. 新对象内部的[[PrimitiveValue]]属性按照下面方式设置:
    • 调用ToPrimitivevalue转成基本数据;
    • 如果基本数据是String类型,在行为上和Date.parse方法相同(下面讲);
    • 如果是其他类型则转换成Number,然后调用TimeClip方法,该方法是数字转换成合法的 Unix 时间戳,否则返回NaN

Unix 时间戳(Unix Time Stamp),是一个整数值,表示自1970年1月1日00:00:00 UTC 以来的毫秒数,忽略了闰秒。

在看一看Date.parse方法。

Date.parse(dateString)

这个方法作用是解析一个表示日期的字符串,并返回从 1970-01-01 00:00:00 UTC 所经过的毫秒数。但在实现过程中,每个浏览器不太一样。因为RFC中有这样一段比较模糊的描述:

The function first attempts to parse the format of the String according to the rules called out in Date Time String Format (15.9.1.15). If the String does not conform to that format the function may fall back to any implementation-specific heuristics or implementation-specific date formats.

意思是如果日期字符串不符合ISO 8601标准,可以使用具体实现(比如浏览器)提供的非标准日期格式。但实际上各个实现提供的非标准日期格式不尽相同,从而导致不兼容。

针对浏览器上的实现,MDN也特别给出提示:

由于浏览器之间的差异与不一致性,强烈不推荐使用Date构造函数来解析日期字符串 (或使用与其等价的Date.parse)。对 RFC 2822 格式的日期仅有约定俗称的支持。 对 ISO 8601 格式的支持中,形如"1970-01-01"仅有日期的字符串的会被处理为 UTC 而不是本地时间。

Date.parse()的例子中也额外指出了这一问题。

所以锅在于我使用的日期字符串格式为"2020-06-05"

解决办法

'-'替换成'/'

const getDate = (dateString) => {
  // '-'分隔符全部转换成'/'
  dateString = dateString.replace(/-/g, '/')
  return new Date(dateString)
}

拓展

  1. new Date()

创建一个实例化时刻的日期和时间的Date对象。

  1. 生成一个新对象,对象的原型继承Date构造函数的原型对象;
  2. 新对象内部的[[class]]属性设置为Date
  3. 新对象内部的[[Extensible]]属性设置为true
  4. 新对象内部的[[PrimitiveValue]]属性设置为当前时间对应的UTC(世界协调时)。
  1. new Date(year, month [, date [, hours [, minutes [,seconds [, ms ]]]]])

前三步与new Date()一样,第四步不一样,主要是转换参数为Number类型或设置默认值,最终调用TimeClip方法。这些参数的含义如下:

描述 是否必选
year 表示年份的整数值。 0到99会被映射至1900年至1999年,其它值代表实际年份。
month 表示月份的整数值,从 0(表示1月份)到 11(表示12月份)。
date 表示一个月中的第几天的整数值,从1开始。默认值为1。
hours 表示一天中的小时数的整数值 (24小时制)。默认值为0(凌晨)。
minutes 表示一个完整时间(如 01:10:00)中的分钟部分的整数值。默认值为0。
seconds 表示一个完整时间(如 01:10:00)中的秒部分的整数值。默认值为0。
ms 表示一个完整时间的毫秒部分的整数值。默认值为0。

值得注意的有两点:

  • month是从0开始计算,0表示1月份,11表示12月份
  • 当日00:00:00其UNIX时间戳等于上一日的24:00:00,例如2020年6月5日24:00:00 等同于 2020年6月6日00:00:00
  1. Date.UTC(year, month [, date [, hours [, minutes [, seconds [, ms ]]]]])

接受和构造函数最长形式的参数相同的参数,并返回从 1970-01-01 00:00:00 UTC 开始所经过的毫秒数。

虽然Date.UTC看上去和New Date的第三种方式相似,但实际上有两点区别:

  • 前者返回的是一个Unix时间戳,而后者返回的是一个Date实例对象;
  • 前者以UTC来解析参数,而后者以本地时区解析参数;