Skip to content

JavaScript中的Date #16

@xwcoder

Description

@xwcoder

这篇内容中的代码运行环境如下:

  • 时区:东八区,北京时间
  • OS:macOS 10.14.3
  • Chrome: 71.0.3578.98(正式版本) (64 位)
  • Safari: 12.0.3 (14606.4.5)

JavaScript中的Date一直被认为设计的非常糟糕。1995年Brendan只有十天的时间设计JavaScript,同时还被要求像Java,所以其拷贝了java.Util.Date的实现。而随后在1997年发布的Java1.1中,java.Util.Date中的所有方法都被废弃和取代了。但是这些糟糕的设计在JavaScript中被保留了下来,比如getYear()

先看几个例子(忽略输出格式,只关注时间):

new Date('2019/01/31')
// Chrome: Thu Jan 31 2019 00:00:00 GMT+0800 (中国标准时间)
// Safari: Thu Jan 31 2019 00:00:00 GMT+0800 (CST)

new Date('2019-01/31')
// Chrome: Thu Jan 31 2019 00:00:00 GMT+0800 (中国标准时间)
// Safari: Invalid Date
new Date('2019-01-31')
// Chrome: Thu Jan 31 2019 08:00:00 GMT+0800 (中国标准时间)
// Safari: Thu Jan 31 2019 08:00:00 GMT+0800 (CST)

new Date('2019-01-31T00:00')
// Chrome: Thu Jan 31 2019 00:00:00 GMT+0800 (中国标准时间)
// Safari: Thu Jan 31 2019 08:00:00 GMT+0800 (CST)

首先简要复习下时区、标准时间、日期时间格式等相关内容。

时区、GMT、UTC

这部分内容主要来自维基百科

时区

时区是地球上的区域使用同一个时间定义。以前,人们通过观察太阳的位置来决定时间,地球上处于不同经度的地区看到日出、日落的时间是有偏差的,这些偏差就是所谓的时差。

在农业社会,世界各地一般都采用各自的本地时间,由于那时人们的活动范围有限,这并不会带来太大问题。随着工业社会的发展,尤其是铁路的发展,人们的活动范围越来越广,不同地区的人交流越来越频繁,各地再采用本地时间就会对交流造成很大困扰。这时需要统一的时间定义和划分。

1870年代加拿大铁路工程师弗莱明首次提出全世界按统一标准划分时区。1883年11月18日,美国铁路部门正式实施五个时区。1884年华盛顿子午线国际会议正式通过采纳这种时区划分,称为世界标准时制度。

现行时区

本初子午线

本初子午线(Prime meridian),即0度经线,是经过英国格林尼治天文台的一条经线。

理论上任何一条经线均可定为本初子午线。1851年御用天文学家艾里(Sir George Airy)在格林尼治天文台设置中星仪,并以此确定格林尼治子午线。因为当时超过三分之二的船只已使用该线为参考子午线,所以1884年华盛顿子午线国际会议上正式确定其为0度经线。

GMT

理论上来说,格林尼治标准时间的正午是指当太阳横穿本初子午线时的时间。

格林尼治标准时间也被称为格林尼治平时(Greenwich Mean Time,GMT),是指0度经线当地的平太阳时。格林尼治平时的正午是指当平太阳横穿本初子午线时(也就是在格林尼治上空最高点时)的时间。

人们最初确定时间的方式是直接观测太阳在当地天空中的位置,这样测量出来的时间被称为地方真太阳时。后来,人们为了解决地球公转轨道不是正圆和黄道与赤道之间存在夹角而造成的测出的时间的流逝不均匀的问题,以假想天体平太阳(mean Sun)为基准测量时间,而不再以真太阳为基准,这样测量出来的时间被称为地方平太阳时

格林尼治天文台所在地的地方平太阳时被定义为全世界的时间标准,被称为格林尼治平时(Greenwich Mean Time)。

由于1925年以前人们在天文观测中,常常把每天的起始(0时)定为正午,而不是通常民用的午夜,给格林尼治平时的意义造成含糊,人们使用世界时(Universal Time, UT)一词来明确表示每天从午夜开始的格林尼治平时。目前使用的世界时测算标准又称UT1

由于地球每天的自转是有些不规则的,而且速度正在减缓,因此基于天文观测的格林尼治平时本身是有缺陷的,所以其已被UTC所取代。

UTC

协调世界时(Coordinated Universal Time,UTC)是最主要的世界时间标准,其以国际原子时秒长为基础,在时刻上尽量接近于格林尼治标准时间。

UTC与0度经线的平太阳时相差不超过1秒,是最接近格林尼治标准时间(GMT)的几个替代时间系统之一。对于大多数用途来说,UTC时间被认为能与GMT时间互换,但GMT时间已不再被科学界所确定

1967年第13届国际计量大会上通过一项决议,定义一秒为铯-133原子基态两个超精细能级间跃迁辐射9,192,631,770周所持续的时间。其起点为世界时1958年的开始。国际原子时作为一种国际参照时标,使用以上秒的定义。

UTC基于国际原子时,并通过不规则的加入闰秒来抵消地球自转变慢的影响。闰秒在必要的时候会被插入到UTC中,以保证协调世界时(UTC)与世界时(UT1)相差不超过0.9秒。

UTC中,每天包含24小时,每小时包含60分钟。一分钟通常有60秒,但加入了随机的闰秒后,一分钟可能是61秒或59秒。

几乎所有的UTC天都包含 86,400 秒,即每分钟正好有60秒。然而,由于一个平太阳日比86,400秒稍微长一些,偶尔会有一个UTC天的最后一分钟被调整为61秒。一个UTC天的最后一分钟也可以是59秒,以此来适应地球自转得更快的情况,但是这样的可能性很小,至今还没有出现过。

UTC时间使用大写字母“Z”来表示。“Z”是UTC中0时区的标志。

UTC时间也被叫做祖鲁时间,“Z”在无线电中应以北约音标字母读作“Zulu”。

日期时间表示方法

定义日期时间表示方法的ISO标准是 ISO 8601。中文全称是《数据存储和交换形式·信息交换·日期和时间的表示方法》。目前一共发布了三个版本,分别是1988年发布的第一版ISO 8601:1988,2000年发布的第二版ISO 8601:2000,以及2004年发布的第三版ISO 8601:2004

日期表示法

ISO 8601规定用4位数字[YYYY]表示年,因此表示范围是0000年到9999年。公元前1年是0000年,公元1年是0001年。

1583年之前的年份并不被标准自动允许(not automatically allowed)。0000到1582之间的值需要交换信息的双方自行协定。

日期有多种表示方法,日历日期表示法顺序日期表示法星期日历表示法等。

最常用到的是日历日期表示法

  • 年,4位数字[YYYY],取值范围从0000到9999 。
  • 月,2位数字[MM],取值范围从01到12 。
  • 日,2位数字[DD],取值范围从01到31 。

只使用数字的为基本格式[YYYY][MM][DD],使用短横线"-"分隔的为扩展格式[YYYY]-[MM]-[DD]

例如,2019年1月31日可表示为20190131(基本格式),或者2019-01-31(扩展格式)。

时间表示方法

ISO 8601使用24小时制。

  • 时,2位数字[hh],取值范围从00到24(24表示一天的结束)。
  • 分,2位数字[mm],取值范围从00到59 。
  • 秒,2位数字[ss],取值范围从00到60(60表示闰秒)。

只使用数字的为基本格式[hh][mm][ss],使用“:”分隔的为扩展格式[hh]:[mm]:[ss]

午夜可以表示为00:00,或者24:0000:00表示一天的开始,24:00表示一天的结束。2007-04-05T24:002007-04-06T00:00代表的是同一时刻。

时区标识

ISO 8601中的时区指本地时间,UTC(0度经线位置的时间),或UTC偏移量(offset from UTC)。

  • 本地时间:ISO 8601中,如果没有特别标明即指本地时间。
  • UTC:需要在时间之后加上大写字母“Z”,中间没有空格。
  • UTC偏移量:在时间之后加上±[hh]:[mm], ±[hh][mm], 或者±[hh] 。如东八区22:30:05+08:0022:30:05+0800, 或者22:30:05+08

“Z”可以认为是UTC偏移量为0 。

日期时间表示方法

即组合使用日期表示法时间表示法,以大写字母T做分隔。如2019-01-31T18:3020190131T1830。也可以加上时区标识,如2019-01-31T18:30+08:00

JavaScript中的Date

这部分内容主要来自ECMA-262

一个Date对象代表一个时刻。它包含一个数字(Number),这个数字代表相对于1970-01-01T00:00Z的毫秒数。这个数字被称为time value,它的值可能为NaNtime value忽略闰秒,它假定一天正好有86400000毫秒。

time value为0的时刻是UTC时间的1970年1月1日0点0分0秒,东八区的1970年1月1日8点0分0秒。

const date = new Date('1970-01-01T00:00Z')
date.getTime() // 0
date.toString() // Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间)
date.toUTCString() // Thu, 01 Jan 1970 00:00:00 GMT

Date使用的日期格式

ES5开始使用简化的ISO 8601扩展格式,格式为YYYY-MM-DDTHH:mm:ss.sssZ

  • YYYY:表示年(公历)的4位数字,取值0000到9999 。
  • MM:表示月的2位数字,取值01到12 。
  • DD:表示日的2位数字,取值01到31 。
  • HH:表示小时的2位数字,取值00到24 。
  • mm:表示分钟的2位数字,取值00到59 。
  • ss:表示秒的2位数字,取值00到59(忽略闰秒)。
  • sss:表示毫秒的3位数字。
  • Z:时区标识,Z表示UTC;±[hh]:[mm]表示UTC偏移量。

ECMA-262又将其划分出了两类格式,

  1. date-only format:
    • YYYY
    • YYYY-MM
    • YYYY-MM-DD
  2. date-time format,即date-only加上以下时间格式,再加上可选的时区标识。
    • THH:mm
    • THH:mm:ss
    • THH:mm:ss.sss

Date的日期格式解析

Date对日期格式的解析是一个其长久以来被诟病的点

  • 所有数字都是10进制的。
  • MM和DD的默认值是01
  • HH, mm, ss的默认值是00
  • sss的默认值是000

问题出在了时区的默认值。ISO 8601中定义时区缺省时表示本地时间

If no UTC relation information is given with a time representation, the time is assumed to be in local time.

ES5中规定时区的缺省值是Z,即UTC时间。

The value of an absent time zone offset is “Z”.

这和ISO 8601的规定正好相反,随后TC39发现了这个错误,并在ES2015中做了修正。

If the time zone offset is absent, the date-time is interpreted as a local time.

由于已发布的浏览器实现了不同的标准,并且web上已经有大量的js代码,经过权衡,TC39在ES2016中修改为:当时区缺省时,data-only格式按UTC时间解析,date-time格式按本地时间解析。

When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.

再看开头的例子:

new Date('2019-01-31')
// Chrome: Thu Jan 31 2019 08:00:00 GMT+0800 (中国标准时间)
// Safari: Thu Jan 31 2019 08:00:00 GMT+0800 (CST)

new Date('2019-01-31T00:00')
// Chrome: Thu Jan 31 2019 00:00:00 GMT+0800 (中国标准时间)
// Safari: Thu Jan 31 2019 08:00:00 GMT+0800 (CST)

Chrome实现的最新标准,Safari实现的ES5“错误”标准。

当解析以上时间格式时要格外小心, 比如处理<input type="datetime-local">时,其value是缺省时区的date-time格式。

除了上述日期时间格式,Date还支持以下格式:
如果x是合法的Date实例,Date支持以下方法返回的字符串格式,

  • x.toString()
  • x.toUTCString()
  • x.toISOString()

除此之外的其他格式解析依赖具体实现。这也就解释了开头的例子:

new Date('2019/01/31')
// Chrome: Thu Jan 31 2019 00:00:00 GMT+0800 (中国标准时间)
// Safari: Thu Jan 31 2019 00:00:00 GMT+0800 (CST)

new Date('2019-01/31')
// Chrome: Thu Jan 31 2019 00:00:00 GMT+0800 (中国标准时间)
// Safari: Invalid Date

toXXXString()

Date的toXXXString()方法也是长久以来被诟病的点,以至于开发中很少被使用。直到ES2018对其返回格式做了明确定义情况才有所改善。

Date的星期和月是从0开始计数的。
表一,weekday:

Number Name
0 "Sun"
1 "Mon"
2 "Tue"
3 "Wed"
4 "Thu"
5 "Fri"
6 "Sat"

表二,month:

Number Name
0 "Jan"
1 "Feb"
2 "Mar"
3 "Apr"
4 "May"
5 "Jun"
6 "Jul"
7 "Aug"
8 "Sep"
9 "Oct"
10 "Nov"
11 "Dec"

以下假设date是合法的Date实例。

ES2018明确定义了TimeString, DateString, TimeZoneString的格式。

TimeString

TimeString是以下字符串的拼接: hour, ":", minute, ":", second, the code unit 0x0020 (SPACE), "GMT"。例如04:08:49 GMT

DateString

DateString是以下字符串的拼接: weekday, the code unit 0x0020 (SPACE), month, the code unit 0x0020 (SPACE), day, the code unit 0x0020 (SPACE), year。例如Wed Feb 13 2019

TimeZoneString

TimeZoneString是以下字符串的拼接: offsetSign, offsetHour, offsetMin, tzName

  • offsetSign: +或者-
  • tzName: 依赖具体实现,或者是空字符串,或者是如下字符拼接,the code unit 0x0020 (SPACE), the code unit 0x0028 (LEFT PARENTHESIS), an implementation-dependent timezone name, and the code unit 0x0029 (RIGHT PARENTHESIS)

例如:
+0800 (中国标准时间) // Chrome
+0800 (CST) // Safari

date.toDateString()

ES2018中定义返回DateString。之前版本依赖具体实现(implementation-dependent)。

date.toTimeString()

ES2018中定义返回TimeStringTimeZoneString的拼接字符串,如10:42:00 GMT+0800 (中国标准时间)。之前版本依赖具体实现

date.toISOString()

返回date-time格式的字符串,时区是UTC。

const date = new Date() // Wed Feb 13 2019 12:08:49 GMT+0800 (中国标准时间)
date.toISOString() // 2019-02-13T04:08:49.522Z

date.toUTCString()

ES2018中定义返回如下字符串拼接:weekday, ",", the code unit 0x0020 (SPACE), day, the code unit 0x0020 (SPACE), month, the code unit 0x0020 (SPACE), year, the code unit 0x0020 (SPACE), TimeString.
例如: Sun, 17 Feb 2019 02:42:00 GMT

之前版本依赖具体实现。

date.toString()

ES2018中定义返回如下字符串的拼接:DateString, the code unit 0x0020 (SPACE), TimeString, TimeZoneString。例如:Sun Feb 17 2019 10:42:00 GMT+0800 (中国标准时间)

之前版本依赖具体实现。

toLocaleXXXString

toLocaleString(), toLocaleDateString(), toLocaleTimeString, 或者实现ECMA-402 Internationalization API,或者依赖具体实现。

废弃的方法

  • getYear()
  • getYear()
  • toGMTString()

目前JavaScript环境中Date的实现依然比较混乱,遇到需要解析Date时要格外小心,尤其需要处理时区和国际化时。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions