GMT、UTC、 时区、JavaScript Date总结

5,356 阅读21分钟

一、GMT

GMT(Greenwich Mean Time)全名是格林威治平时(也称格林威治标准时间)。这个时间系统的概念是在1884年确立。

它规定太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午12点。

1972年之前,格林威治时间(GMT)一直是世界时间的标准。1972年之后,GMT不再是一个时间标准了。

GMT因为是根据地球的转动来计算时间的,而地球的自转受潮汐、地震、风速等因素的影响自转速度是不均衡的,而且自转速度也会越来越慢,所以GMT不够精确,就不再使用了。

按照常理说GMT系统已经不是世界时间标准了就需要退役了,但实际上由于GMT这个名字已经被使用了几十年,所以GMT这个名字被保留了下來。如下面JS日期的打印信息:

new Date() // Thu Feb 10 2022 16:41:47 GMT+0800 (中国标准时间)

我们常说的时间戳,timestamp就是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。JS中出输出的timestamp是到毫秒级的。

new Date('1970-01-01').getTime() // 0

new Date().getTime() // 1644482867088

二、UTC

UTC(Coodinated Universal Time),协调世界时,又称世界统一时间、世界标准时间、国际协调时间。

UTC是现在全球通用的时间标准,全球各地都同意将各自的时间进行同步协调。UTC时间是经过平均太阳时(以格林威治时间GMT为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成,计算过程相当严谨精密。

一般来说,当我们提到UTC时间而不带任何别的修饰时,常指UTC 0点(UTC+0)

协调世界时(UTC)不与任何地区位置相关,也不代表此刻某地的时间,所以在说明某地时间时要加上时区。比如说UTC + 8 = 北京时间

也就是说GMT并不等于UTC,而是等于UTC+0,只是格林威治刚好在0时区上,所以GMT = UTC+0

三、GMT VS UTC

GMT是前世界标准时,UTC是现世界标准时。

UTCGMT更精准,以原子时计时,适应现代社会的精确计时。

但在不需要精确到秒的情况下,二者可以视为等同。(UTC有闰秒,而GMT没有)

四、时区

随着火车铁路与其他交通和通讯工具的发展,以及全球化贸易的推动,各地使用各自的当地太阳时间带来了时间不统一的问题,在19世纪催生了统一时间标准的需求,时区由此诞生。

从格林威治本初子午线起,经度每向东或者向西间隔15°,就划分一个时区,在这个区域内,大家使用同样的标准时间。

实际上,为了照顾到行政上的方便,常将1个国家或1个省份划在一起。所以时区并不严格按南北直线来划分,而是按自然条件来划分。由于目前国际上并没有一个批准各国更改时区的机构。一些国家会由于特定原因改变自己的时区。

全球共分为24个标准时区,相邻时区的时间相差一个小时。(东、西各12个时区)。规定英国格林威治天文台为中时区(零时区)、东1-12区西1-12区。每个时区横跨经度15度,时间正好是1小时。

image.png

如果在JS中获取一个日期的时区,可以使用Date.prototype.getTimezoneOffset()getTimezoneOffset返回返回本地时间与UTC时间相差的分钟数。

let now = new Date()
console.log(now.getTimezoneOffset()) // -480

上述代码可以理解为( 0时区 - 本地时区(+8) ) * 60min = -480

五、本地时间

在日常生活中所使用的时间我们通常称之为本地时间。这个时间等于我们所在(或者所使用)时区内的当地时间,它由与世界标准时间(UTC)之间的偏移量来定义。这个偏移量可以表示为UTC-UTC+,后面接上偏移的小时和分钟数。

因为时区的问题北京时间和UTC时间有这样的关系 UTC + 8 = 北京时间 ,这个公式有助于我们后面理解Date 类型为什么在不同方法下的转换结果不同。

六、ISO 8601

ISO 8601一种国际通用的无歧义的日期和时间格式。这个ISO标准能够帮助消除因不同的日期转换、文化差异、时区等的影响导致对日期时间格式理解上的偏差,他给出了一种无论对人还是机器都清晰定义的日期和时间表示形式。

标准制定的很细致,这里只简单介绍一些:

1. 年(Year): YYYY (1997)
2. 年和月(Year and month): YYYY-MM (1997-07)
3. 完整的日期(Complete date): YYYY-MM-DD (1997-07-16)
4. 完整的日期加上小时和分钟(Complete date plus hours and minutes): YYYY-MM-DDThh:mmTZD (1997-07-16T19:20+01:00)
5.  完整的日期加上小时、分钟和秒(Complete date plus hours, minutes and seconds): YYYY-MM-DDThh:mm:ssTZD (1997-07-16T19:20:30+01:00)
6. 完整的日期加上小时、分钟、秒和小数点后一秒(Complete date plus hours, minutes, seconds and a decimal fraction of a second):YYYY-MM-DDThh:mm:ss.sTZD (1997-07-16T19:20:30.45+01:00)
  • YYYY表示四位数的年份。

  • MM表示两位数的月份。

  • DD表示两位数的天(day of the month),从0131

  • T是用来指示时间元素的开始字符,日期和时间合并表示时,要在时间前面加大写字母T

  • hh表示两位数的小时,从0023,不包括AM/PM

  • mm表示两位数的分钟,从0059

  • ss表示两位数的秒,从0059

  • s表示一或多位数,表示秒的小数部分。

  • mmm表示三位数的毫秒数,从000999

  • TZD表示时区指示符:Z+hh:mm-hh:mm+-表示时区距离UTC(世界标准时间)时区多远。

    • Z表示UTC时间。
    • +hh:mm表示比UTC时间快的本地时区。
    • -hh:mm表示比UTC时间慢的本地时区。

七、星期和月份

下面周一到周日的简写和全称:

星期全称简写
周一MondayMon
周二TuesdayTue
周三WednesdayWed
周四ThursdayThur
周五FridayFri
周六SaturdaySat
周日SundaySun

下面是一月到十二月的简写和全称:

月份全称简写
一月JanuaryJan
二月FebruaryFeb
三月MarchMar
四月AprilApr
五月MayMay
六月JuneJun
七月JulyJul
八月AugusAug
九月SeptemberSept
十月OctoberOct
十一月NovemberNov
十二月DecemberDec

八、JavaScript中的Date

在学习JSDate时,需要特别注意一点:不管如何实例化一个Date对象,JS在本地存储时,都会将它转换成本地时区。

Date对象是JavaScript原生的时间库。它以1970年1月1日00:00:00作为时间的零点,可以精确表示1970 年1月1日之前及之后285616年的日期。

Date实例有一个独特的地方。其他对象求值的时候,都是默认调用.valueOf()方法,但是Date实例求值的时候,默认调用的是toString()方法。这导致对Date实例求值,返回的是一个字符串,得到本地时间,在不同时区打印new Date(),输出的结果会不一样:

new Date() // Wed Feb 09 2022 17:17:50 GMT+0800 (中国标准时间)
new Date().toString() // Wed Feb 09 2022 17:17:50 GMT+0800 (中国标准时间)

在不给Date构造函数传参数的情况下,创建的对象将保存当前日期和时间。

Date还可以传入一个整数值,表示自1970年1月1日00:00:00以来的毫秒数。

new Date(1644398818532) // Wed Feb 09 2022 17:26:58 GMT+0800 (中国标准时间)

得到本地时间距1970年1月1日0点至今所经历的的毫秒数:

new Date().getTime() // 1644398818532

如果想要根据指定的日期和时间创建对象,必须传入该日期的毫秒数(即从UTC时间1970年1月1日0点至指定时间经过的毫秒数)。难道我们还要自己计算好毫秒数才能创建相应的时间对象吗?这样岂不是太麻烦了?

针对上面的问题ECMAScript提供了两个方法Date.parse()Date.UTC(),以此来简化这一计算过程。它们会根据我们传入的参数来自动计算出毫秒数的大小。下面我们来分别介绍一下这两个方法。

8.1、Date.parse()

Date.parse()方法是基于本地时区建立的。Date.parse()方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫秒数。ECMA-262第5版定义了Date.parse()应该支持的日期格式,填充了第3版遗留的空白。所有实现都必须支持下列日期格式:

  • "月/日/年",如 "5/23/2019"

  • "月名 日,年" 如 "May 23,2019"

  • "周几 月名 日 年 时:分:秒 时区",如"Tue May 23 2019 00:00:00 GMT-0700";

  • ISO 8601扩展格式“YYYY-MM-DDTHH:mm:ss.sssZ”,如 2019-05-23T00:00:00。

比如,要创建一个表示"2019年5月23日"的日期对象,可以使用以下代码:

new Date(Date.parse('May 23, 2019')) // Thu May 23 2019 00:00:00 GMT+0800 (中国标准时间)

如果传给Date.parse()的字符串并不表示日期,则该方法会返回NaN。如果直接把表示日期的字符串传给Date构造函数,那么Date会在后台调用Date.parse()。换句话说,下面这行代码跟前面那行代码是等价的:

new Date('May 23, 2019') // Thu May 23 2019 00:00:00 GMT+0800 (中国标准时间)

不同的浏览器对Date类型的实现有很多问题。如果我们输入的日期值超过了正常的范围,在不同的浏览器中的会有不同的处理方式。例如在解析"January 32,2007"时,有的浏览器会将其解析为February 1,2007"。而Opera浏览器则倾向于插入当前月份的当前日期值,返回"January 当前日期值,2007"

8.2、Date.UTC()

Date.UTC()方法是基于无时区偏差建立的。Date.UTC()方法也返回日期的毫秒表示,但它需要的参数不是字符串,它的参数分别是年份,基于0的月份(0到11),日(1到31),小时(0到23),分钟,秒和毫秒。这些参数中,只有前两个(年和月)是必需的。如果不提供日,那么默认为1日。其他参数的默认值都是0。

下面是使用Date.UTC()的两个例子:

/* js在本地存储时,都会将它转换成本地时区,需要调用toUTCString()方法来正确显示无时区偏差。 */

// 内部先得到UTC时间,再被转为本地时区,就多了8个小时。
new Date(Date.UTC(2000, 0)) // Sat Jan 01 2000 08:00:00 GMT+0800 (中国标准时间)

// 内部先得到UTC时间,再被转为本地时区,就多了8个小时。
new Date(Date.UTC(2022, 1, 9, 12, 55, 55)) // Wed Feb 09 2022 20:55:55 GMT+0800 (中国标准时间)

// GMT时间2000年1月1日零点
new Date(Date.UTC(2000, 0)).toUTCString() // "Sat, 01 Jan 2000 00:00:00 GMT"

// GMT时间2022年2月9日下午12点55分55秒
new Date(Date.UTC(2022, 1, 9, 12, 55, 55)).toUTCString() // "Wed, 09 Feb 2022 12:55:55 GMT"

这个例子创建了两个日期。第一个日期是2000年1月1日零点(GMT),2000代表年,0代表月(1月)。因为没有其他参数(日取1,其他取0),所以结果就是该月第1天零点。

第二个日期表示2022年2月9日下午12点55分55秒(GMT)。小时必须是17,因为这里采用的是24小时制,即取值范围是0~23。其他参数就都很直观了。

Date.parse()一样,Date.UTC()也会被Date构造函数隐式调用,但有一个区别:这种情况下创建的是本地日期,不是GMT日期。不过Date构造函数跟Date.UTC()接收的参数是一样的。因此,如果第一个参数是数值,则构造函数假设它是日期中的年,第二个参数就是月,以此类推。前面的例子也可以这样来写:

// 本地时间2000年1月1日零点
new Date(2000, 0) // Sat Jan 01 2000 00:00:00 GMT+0800 (中国标准时间)

// 本地时间2022年2月9日下午12点55分55秒
new Date(2022, 1, 9, 12, 55, 55) // Wed Feb 09 2022 12:55:55 GMT+0800 (中国标准时间)

以上代码创建了与前面例子中相同的两个日期,通过比较我们可以看出来无论是显示调用还是隐式调用打印出来的结果都是本地时区,但是会相差8小时:

// 28800000 = 8 * 60 * 60 * 1000
new Date(Date.UTC(2000, 0)) - new Date(2000, 0)

8.3、UTC() VS parse()

Date.UTC()日期指的是在没有时区偏差的情况下(将日期转换为GMT时间)的日期值。

Date.parse()方法是基于本地时区建立的,而Date.UTC()方法是基于无时区偏差建立的。

所以如果我们对两个方法传入相同的时间,我们会发现Date.UTC()方法得到的毫秒数相对于Date.parse()方法得到的毫秒数会多八个小时的毫秒数(这里的本地时区指的是北京时间)。

//假设我们传入相同的时间2022年2月9日

Date.UTC(2022, 1, 9) // 1644364800000

Date.parse('2/9/2022') // 1644336000000

// 1646784000000 - 1644336000000 = 28800000 = 8 * 60 * 60 * 1000

8.4、Date.now()

ES5添加了Date.now()方法,Date.now() 方法返回自1970年1月1日 00:00:00(UTC)到当前时间的毫秒数。这个方法可以用来分析函数的运行时间,如下:

// 取得开始时间
let start = Date.now()

// 调用函数
doSomething()

// 获取结束时间
let end = Date.now()

// 得到函数运行时间
let runtime = start - end

在不支持它的浏览器中,我们可以通过+操作符获取Date对象的时间戳:

// 取得开始时间
let start = +new Date()

// 调用函数
doSomething()

// 获取结束时间
let end = +new Date()

// 得到函数运行时间
let runtime = start - end

8.5、继承的方法

与其他类型一样,Date类型重写了toLocaleString()toString()valueOf()方法。但与其他类型不同,重写后这些方法的返回值不一样。Date类型的toLocaleString()方法返回与浏览器运行的本地环境一致的日期和时间。这通常意味着格式中包含针对时间的AM(上午)或PM(下午),但不包含时区信息(具体格式可能因浏览器而不同)。

let now = new Date()
console.log(now.toLocaleString()) // 2022/2/9 下午6:23:11

Date类型的toString()方法会返回带有时区信息的日期和时间,其中时间一般以军用时间(范围0到23)表示。

let now = new Date()
console.log(now.toString()) // Wed Feb 09 2022 18:24:37 GMT+0800 (中国标准时间)

Date类型的valueOf()方法被重写后返回的是日期的毫秒表示。因此,操作符(如小于号和大于号)可以直接使用它返回的值。比如下面的例子:

let date1 = new Date(2022, 0, 1); // 2022 年 1 月 1 日
let date2 = new Date(2022, 1, 1); // 2022 年 2 月 1 日
console.log(date1 < date2); // true
console.log(date1 > date2); // false

在使用比较操作符,会隐式地调用Date对象的valueOf()方法,然后根据得到的毫秒数来进行比较。

其实在实际应用中,使用toLocaleString()toString()来显示日期时间是没有什么价值的,因为它们的返回的日期格式在不同的浏览器里显示不同,无法得到一致化的显示结果,而且得到的格式对用户的交互效果也不是很友好。

8.6、日期格式化方法

Date类型有几个专门用于格式化日期的方法,它们都会返回字符串:

  1. toDateString():返回日期中的周几、月、日、年。
  2. toTimeString():返回日期中的时、分、秒和时区。
  3. toLocaleDateString():返回日期中的周几、月、日、年(格式特定于地区)。
  4. toLocaleTimeString():返回日期中的时、分、秒(格式特定于地区)。
  5. toUTCString():返回完整的UTC日期。
  6. toISOString():返回返回对应时间的ISO 8601写法。
  7. toJSON():返回一个符合JSON格式的ISO 8601日期字符串。与toISOString()方法的返回结果完全相同。
  8. toGMTString():这个方法跟toUTCString()是一样的,目的是为了向后兼容。不过,规范建议新代码使用toUTCString()
let now = new Date()
console.log(now.toDateString()) // Thu Feb 10 2022
console.log(now.toTimeString()) // 15:46:31 GMT+0800 (中国标准时间)
console.log(now.toLocaleDateString()) // 2022/2/10
console.log(now.toLocaleTimeString()) // 下午3:46:31
console.log(now.toUTCString()) // Thu, 10 Feb 2022 07:47:04 GMT
console.log(now.toISOString()) // 2022-02-10T15:38:12.529Z
console.log(now.toJSON()) // 2022-02-10T15:38:12.529Z
console.log(now.toGMTString()) // Thu, 10 Feb 2022 07:47:14 GMT

Date类型这些专门用来将日期转化为字符串的方法,不过与toLocaleString()toString() 的缺点一样,在平常的使用中没有多大价值,所以仅做一下了解就好。

8.7、日期/时间组件方法

Date类型本身的字符串格式化方法在日常使用中用处不大,所以一般我们只有自己编写适用于项目的format方法,这时我们一般需要用到获取日期中特定部分的方法。注意表中UTC 日期,指的是没有时区偏移(将日期转换为 GMT)时的日期。如下:

方法说明
getTime()返回表示日期的毫秒数;与valueOf()方法返回的值相同
setTime(毫秒)设置日期的毫秒表示,从而修改整个日期
getFullYear()取得4位数的年份(如2022而非仅22)
getUTCFullYear()返回UTC日期的4位数年份
setFullYear(年)设置日期的年份。传入的年份值必须是4位数字(如2022而非仅22)
setUTCFullYear(年)设置UTC日期的年份。传入的年份值必须是4位数字(如2022而非仅22)
getMonth()返回日期中的月份,其中0表示一月,11表示十二月
getUTCMonth()返回日期中的月份,其中0表示一月,11表示十二月
setMonth(月)设置日期的月份。传入的月份值必须大于0,超过11则增加年份
setUTCMonth(月)设置UTC日期的月份。传入的月份值必须大于0,超过11则增加年份
getDate()返回日期月份中的天数(1到31)
getUTCDate()返回UTC日期月份中的天数(1到31)
setDate(日)设置日期月份中的天数。如果传入的值超过了该月中应有的天数,则增加月份
setUTCDate(日)设置UTC日期月份中的天数。如果传入的值超过了该月中应有的天数,则增加月份
getDay()返回日期中星期的星期几(其中0表示星期日,6表示星期六)
getUTCDay()返回UTC日期中星期的星期几(其中0表示星期日,6表示星期六)
getHours()返回日期中的小时数(0到23)
getUTCHours()返回UTC日期中的小时数(0到23)
setHours(时)设置日期中的小时数。传入的值超过了23则增加月份中的天数
setUTCHours(时)设置UTC日期中的小时数。传入的值超过了23则增加月份中的天数
getMinutes()返回日期中的分钟数(0到59)
getUTCMinutes()返回UTC日期中的分钟数(0到59)
setMinutes(分)设置日期中的分钟数。传入的值超过59则增加小时数
setUTCMinutes(分)设置UTC日期中的分钟数。传入的值超过59则增加小时数
getSeconds()返回日期中的秒数(0到59)
getUTCSeconds()返回UTC日期中的秒数(0到59)
setSeconds(秒)设置日期中的秒数。传入的值超过了59会增加分钟数
setUTCSeconds(秒)设置UTC日期中的秒数。传入的值超过了59会增加分钟数
getMilliseconds()返回日期中的毫秒数
getUTCMilliseconds()返回UTC日期中的毫秒数
setMilliseconds(毫秒)设置日期中的毫秒数
setUTCMilliseconds(毫秒)设置UTC日期中的毫秒数
getTimezoneOffset()返回本地时间与 UTC 时间相差的分钟数。例如,美国东部标准时间返回300。在某地进入夏令时的情况下,这个值会有所变化

九、使用日期时间需要注意的地方

用 new Date(string) 就等于 Date.parse(string),可以让JS来帮你解析一个字符串并转换成时间。如果传入的字符串符合标准格式的话就没有问题,但如果不符合标准的话就会有不同的结果。

这就是需要特别注意的地方了,因为格式的问题相同的时间会输出不一样的结果,比如说下面的代码:

new Date('2022-02-10') // Thu Feb 10 2022 08:00:00 GMT+0800 (中国标准时间)

new Date('2022/02/10') // Thu Feb 10 2022 00:00:00 GMT+0800 (中国标准时间)

还有另外一种常见的非标准格式:2022-02-10 00:00:00

new Date('2022-02-10') // Thu Feb 10 2022 08:00:00 GMT+0800 (中国标准时间)

new Date('2022-02-10 00:00:00') // Thu Feb 10 2022 00:00:00 GMT+0800 (中国标准时间)

因为2022-02-10是符合ISO 8601格式的。所以我们看到的结果是+8时区的8点。 new Date('2022-02-10')就被解析为UTC+0的2月10号0点0分。在它的内部先获取到了一个2022-02-10 00:00:00 GMT+00:00这样的时间,再被转为本地时区,就多了8个小时。

2022/02/102022-02-10 00:00:00并不符合ISO 8601格式,所以会产生不同的结果,这里被当做GMT+08:00的本地时区,所以得到的时间,就是0点。

十、参考

developer.mozilla.org/zh-CN/docs/…

www.w3.org/TR/NOTE-dat…

cavszhouyou.top/JavaScript%…

www.zhihu.com/question/27…

blog.techbridge.cc/2020/12/26/…

zhuanlan.zhihu.com/p/135951778

www.bilibili.com/video/BV1Gs…

blog.csdn.net/bisal/artic…

pansci.asia/archives/84…

juejin.cn/post/684490…

achuan.me/blog/19/06/…

javascript.ruanyifeng.com/stdlib/date…