本文作者:文蔺
本文地址:http://www.wemlion.com/2016/date-in-javascript/
本文由 @文蔺 创作,转载请保留此声明。
所有权利保留,请勿用于商业目的。

时间的发现

日常生活中,各种形式的时间字符到处都是。

时间观念的产生,时间单位、计时工具的发明,给人类带来的变化实在一言难尽。

今天就来谈谈日期那些事儿。一起来看看 JavaScript 中的日期对象 Date。

Date 对象

和其他对象如 Math、RegExp 等一样,Date 对象是 JavaScript 语言中的内建(build-in)对象。在工作中,Date 对象有着许多重要的应用。

创建一个 Date 实例很简单,下面简单回顾下常用的方法。

获取当前时间

var now = new Date();

注意,JavaScript 中的当前时间与操作系统相关。因此,在重要的 Web 应用中,应该会避免使用该时间,更可靠的方式是操作前先请求服务器获取时间,或者将工作直接交给服务端。

生成时间

// 2016-08-31 23:27:22
new Date(2016, 7, 31, 23, 27, 22);
new Date('2016-08-31 23:27:22');
new Date('2016/08/31 23:27:22');

上面只是最常见的几种形式。实际应用中会发现,Date 对象非常强大,能够解析多种格式的字符串。本文暂且略过不表。

值得一提的是,上面 new Date('2016-08-31') 这种形式应当尽量避免。如果没有记错,iOS 系统环境中,这种格式会报错,遇到该格式的字符串时,应该一律先进行替换操作:

var string = '2016-08-31';
var date = new Date(string.replace(/-/g, '/'));

获取或修改更多时间细节

Date 对象提供了一系列 set get 方法供我们使用。方法名也很语义化。在此略过。

眼花缭乱的时间字符串

在工作中,常常会接触各种不同格式的时间字符串。除了那些格式整齐,地球人几乎都能读懂的之外,还有一些不那么为普通人所了解的格式。

与此同时,细心的同学可能注意到,在控制台中输入一个 Date 变量的引用,按下 . 的那一刻,会有一大堆属性、方法提示。除去 set get 这一类方法之外,还有一堆 to***String 形式的方法。相信多数同学用得不多。

往前数月,我也不太关注这些东西。但后来某次,后端返回的数据总是随机地有一些数据是 2016-08-31T15:44:30.244Z 这种格式的。当时不明白其含义,只能自作聪明地拿正则表达式来匹配,作为容错方案。

接下来,自己的一个小爬虫工具想要支持 rss 解析。拿到的不少日期数据是 Thu Aug 25 2016 01:31:50 GMT+0800 (CST) 这种格式的。

由此我开始试着去了解这些看上去奇奇怪怪的日期格式。

接下来,主要通过 JavaScript 中的 to***String 系列方法,了解这些时间字符串。

一堆 to***String 方法

首先,让我们写个简单的脚本,看看 Date 对象到底有哪些 to***String 方法。

注意,这些方法是无法通过 for in 循环取到的,也就是说,默认是 enumerable: false

使用 Object.getOwnPropertyDescriptor 方法可以查看这些细节。

toString 为例,一起看下:

Object.getOwnPropertyDescriptor(Date.prototype, 'toString');

结果如下:

{
writable: true,
enumerable: false,
configurable: true,
value: function toString(),
__proto__: Object
}

既然不能通过 for in 遍历,那还有没有其他办法呢?

有的。Object.getOwnPropertyNames 这个方法可以帮我们拿到对象自身属性的 key 值。

(写到这里,虽然看上去很啰嗦,但我总觉得有必要把细节记下来。万一下次又记不清了呢。)

接着,就可以愉快地取到所有的 to***String 方法名了。

let proto = Date.prototype;
let names = Object.getOwnPropertyNames(proto).filter((name) => /^to[a-zA-Z]*String/.test(name));
console.log(names);

一共有 9 个,如下:

[ "toString",
"toDateString",
"toTimeString",
"toISOString",
"toUTCString",
"toGMTString",
"toLocaleString",
"toLocaleDateString",
"toLocaleTimeString"
]

看名称大概也能知道,这 9 个方法可以分为三组。下面按组来细看。

toString 系列

接下来,我们所有的实验,统一使用一个 Date 实例。需要说明的,我所使用的是 Chrome 52,所有实验都是在控制台中进行的。

// 2016-09-02 10:49:22
var date = new Date(2016, 8, 2, 10, 49, 22);

一并展示出所有结果吧,就是这么简单粗暴。

toString

注意到,date + ''date.toString() 的结果是一样的。这不是偶然,它们实际上是等价的。这涉及到 JavaScript 中的隐式类型转换

根据 ECMA-262 标准,toString() toDateString() toTimeString() 执行的结果,是实现相关的(implementation-dependent)。

toDateString() 为例,看看标准中的是如何说的:

该方法返回一个 String 类型的值。该值是实现相关的,但其目的是以一种简便、便于阅读的形式,展示 Date 实例在当前时区内的“日期”部分。
—— ECMA-262 7ᵗʰ Edition

再来看看结果中的 GMT+0800 是什么鬼。所谓 GMT,是英文 “Greenwich Mean Time” 的缩写,完整翻译过来就是“格林尼治平时”,也就是通常所说的“格林尼治时间”,即位于英国伦敦郊区的皇家格林尼治天文台的标准时间。详细信息可以查看维基百科

至于“+0800”,则是时区的概念了。这意味着,当前时间与标准时区相差 8 小时。

比如,此刻是北京时间 2016 年 9 月 2 日 13:07:22,也就是 Fri Sep 02 2016 13:07:22 GMT+0800,那么此时,格林尼治时区的时间就应该是 05:07:22。不过,从世界时钟中此时英国伦敦的时间是 06:07:22。为何会有此差别?因为 9 月 2 号的时候,伦敦使用的是 BST 时区,即 British Summer Time,也就是众所周知的“夏令时”。

时区图

像 BST、CST 这些标志,就像上面表格中的“中国标准时间”一样。这些是用来说明时区的,通常用缩写表示,不过这并不是标准。CST 正好就是中国标准时间(China Standar Time)的缩写,可以参考 timeanddate 这个网站。

不过,上面仅仅是提到格林尼治时间。并不意味着真正用到了它。JavaScript 中实际使用到的,还是 UTC 时间。

toLocaleString 系列

执行结果如下图所示:

toLocaleString 系列

也很好理解。这三个方法,也是和实现相关的。[标准]中有一句很关键,“与宿主环境当前区域设置的约定保持一致”(”corresponds to the conventions of the host environment’s current locale.”)。

标准时间系列

接着看 toISOString() toUTCString() toGMTString() 三个方法。按照惯例,先看结果:

toISOString() 与 toUTCString()、toGMTString()

这些年,想必各种商业广告已经帮我们普及了 ISO 的概念,八九岁的时候就知道,某某品牌的摩托车号称通过 ISO-2001 标准。ISO,全称是国际标准化组织(International Organization for Standardization),负责制定全世界工商业国际标准的国际标准。

JavaScript 标准定义的时间交换格式(interchange format),是基于 ISO 8601 扩展格式的简化版本,格式是 “YYYY-MM-DDTHH:mm:ss.sssZ”。toISOString() 返回的就是这样的字符串。

T 只是一个字面量,标志着接下来的内容是时间(相对于前面的日期而言)。

Z 标志着时差。直接使用 Z,意味着我们使用的是标准时间(UTC)。

在我们的例子中,date.toISOString() 的结果是 2016-09-02T02:49:22.000Z,可以看到,和我们的实际时间 10:49:22 相差了 8 小时。

当然,Z 的位置上,还可以使用+HH:mm -HH:mm 的形式。这样就是直接指定与标准时间的时差了。

例如,2016-09-02T02:49:22.000Z 作为标准时间,相当于北京时间的 10:49:22,换一种形式就是 2016-09-02T10:49:22.000+08:00

UTC 是“世界标准时间”的简称,又作“协调世界时” “世界协调时间”,英文是 Coordinated Universal Time。下面引用下维基百科中的说明:

协调世界时是世界上调节时钟和时间的主要时间标准,它与0度经线的平太阳时相差不超过1秒,并不遵守夏令时。协调世界时是最接近格林尼治标准时间(GMT)的几个替代时间系统之一……
UTC基于国际原子时,并通过不规则的加入闰秒来抵消地球自转变慢的影响。

我们注意到,toUTCString() toGMTString() 两者返回的字符串是一样的。实际上,这还是和具体实现有关。

还是引用维基百科中的一段,来看看 UTC 时间和格林尼治时间的不同。

理论上来说,格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时(也就是在格林尼治上空最高点时)的时间。由于地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能与实际的太阳时有误差,最大误差达16分钟。
由于地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林尼治时间已经不再被作为标准时间使用。现在的标准时间,是由原子钟报时的协调世界时(UTC)。

ES 2016 中提到,toGMTString() 主要是用来满足旧代码兼容性的,新代码中推荐使用 toUTCString()。标准还提到这么一句:

The function object that is the initial value of Date.prototype.toGMTString is the same function object that is the initial value of Date.prototype.toUTCString.

也就是说,在 JavaScript 中,toUTCString() toGMTString() 这俩是一样的。

assertEqual(date.toGMTString, date,toUTCString);

日期提取工具

从字符串中提取时间,已经有很多工具,还相当智能,英文不必说,中文的“前天”“五天前”“上周三”之类的不在话下。

临时应急,也仅仅是为了辨别一些常用的日期字符串,我也写了一个小工具,主要是提取日期。

关键的正则表达式如下:

// 2012 年 2 月 28 日
re_zh = /(\d{4})\s*[^x00-xff]\s*(\d{1,2})\s*[^x00-xff]\s*(\d{1,2})\s*[^x00-xff]/,

// 2012-02-28, 2012.02.28, 2012/02/28
re_ymd = /\d{4}([\/\-\.])\d{1,2}(\1)\d{1,2}/,

// 02/28/2012 etc.
re_mdy = /\d{1,2}([\/\-\.])\d{1,2}(\1)\d{4}/,

re_en = new RegExp([
// toUTCString(): "Tue, 30 Aug 2016 03:01:19 GMT"
/(\w{3}), (\d{2}) (\w{3}) (\d{4}) ((\d{2}):(\d{2}):(\d{2})) GMT/.source,

// toString(): "Tue Aug 30 2016 11:02:45 GMT+0800 (中国标准时间)"
/(\w{3}) (\w{3}) (\d{2}) (\d{4}) ((\d{2}):(\d{2}):(\d{2})) GMT\+\d{4}/.source,

// toISOString(): "2016-08-30T03:01:19.543Z"
/(\d{4})-(\d{2})-(\d{2})T((\d{2}):(\d{2}):(\d{2}))\.(\d{3})Z/.source,

// toDateString(): "Tue Aug 30 2016"
/(\w{3}) (\w{3}) (\d{2}) (\d{4})/.source
].join('|'), 'm');

详细代码,可见 Github。之后有时间,也会考虑加入更智能的识别功能。

参考

本文作者:文蔺
本文地址:http://www.wemlion.com/2016/date-in-javascript/
转载请注明来源