GMT、UTC、CST
GMT (Greenwich Mean Time) 格林威治标准时间,指位于英国伦敦郊区的皇家格林威治天文台的标准时间,太阳每天经过家格林威治天文台的时间就是中午 12 点,本初子午线被定义为通过那里的经线。然而由于地球的不规则自转,导致 GMT 时间有误差,因此目前已不被当作标准时间使用。
1 new Date ().toGMTString ()
UTC (Coordinated Universal Time) 协调世界时间(世界标准时间、世界统一时间),是经过平均太阳时(以格林威治时间 GMT 为准)、地轴运动修正后的新时标以及以「秒」为单位的国际原子时所综合精算而成的时间。UTC 比 GMT 来得更加精准,其误差值必须保持在 0.9 秒以内,若大于 0.9 秒则由位于巴黎的国际地球自转事务中央局发布闰秒,使 UTC 与地球自转周期一致。日常使用中,GMT 与 UTC 的功能与精确度是没有差别的。
GMT 是根据地球自转计算时间,而 UTC 是根据原子钟来计算时间。
1 2 new Date ().toUTCString ()new Date ().toISOString ()
整个地球分为二十四时区,每个时区都有自己的本地时间,世界各国的本地时间的都是基于 UTC 的,规则为 本地时间 = UTC + 偏移量 (时区差)
。比如,北京时间(CST,China Standard Time,古巴的标准时间和美国、澳大利亚中部时间也被称为 CST)采用东八区的地方时(+0800
),即 CST = UTC + 8
(北京时间是东经 120° 地方的地方时间,不是北京地方东经 116.4° 的时间),而英国伦敦的本地时与 GMT 和 UTC 相同。
1 2 new Date ().toLocaleString ()new Date ().toString ()
日期时间的表示标准 日期时间的表示标准有两种,分别是 ISO 8601 和 RFC 2822 。前者是国际标准化组织的日期和时间表示方法,后者是 Internet 标准中关于电子邮件消息格式的标准,其中涵盖了日期时间标准。
ISO 8601
年用 4 位表示,月、日用 2 位表示,时分秒用 2 位表示,时区用 4 位表示(或者 Z)。
1 2 3 4 yyyy 年,比如:2016 yyyy-mm 年月,比如:2016-03 yyyy-mm-dd 年月日,比如:2016-03-08 YYYY-MM-DDThh:mm:ss[.s]TZD 年月日时分秒,比如:2016-03-08T02:54:17.159Z
T
用来分割日期和时间,.
后面代表毫秒,TZD
time zone designator 表示时区,值可以是 Z
(代表 UTC 时间,即 0 时区时间)、+hh:mm
、-hh:mm
。如果不是 UTC 时间,比如北京时间,就需要表示为 2016-03-25T06:26:01.927+08:00
。
JavaScript 中使用 toISOString()
、toJSON()
、JSON.stringify()
来生成 ISO 8601 格式的日期时间,使用 new Date(dateString)
来解析 IOS 8601 格式的时间。
1 2 3 4 new Date ().toISOString () new Date ('2016-01-25T09:14:10.099+00:00' )
如果日期时间格式不带时区,则会以本地时区来解析。
注意:在 Chrome 中时区也可以用 +0800
这种 RFC2822 形式表示,然而 IE 和老的 iPhone 上不支持这种混搭写法,解析时会提示 NaN
异常,对于这个问题,要么后端返回标准的格式,要么前端纠正。
1 2 3 4 5 new Date ('2016-01-25T09:14:10.099+0000' ).getTime () const dateString = '2016-01-25T09:14:10.099+0000' .replace (/([+-]\d{2})(\d{2})$/ , '$1:$2' );new Date (dateString)
注意:数据库应该始终存储 ISO 8601 格式的 UTC 日期时间,而不是本地时间,本地时间的转换交给客户端,以确保在不同地区的用户都能正确地看到适当的本地时间。
RFC 2822
1 2 YYYY/MM/DD HH:MM:SS TZD(时区用4位数字表示),比如:2016/03/08 02:54:17+0000 [Wdy,] DD Mon YYYY HH:MM:SS TZD,比如:Fri Mar 08 2016 02:54:17 GMT+0800 (中国标准时间)
RFC 2822 允许使用括号形式的注释。
1 2 3 4 new Date ().toString () new Date ('Fri Jan 25 2016 17:14:10 GMT+0800' )
注意:现代所有浏览器使用 toString()
生成日期时间,返回的都是 RFC 2822 格式,而 IE 返回的是 RFC-850/1036 格式,另外在解析时,IE8 只支持 RFC 2822,不支持 ISO 8601。
Date API JavaScript 中对日期时间的处理可以归结为解析、生成、计算和格式化这几方面。
日期解析 使用 new Date
解析日期时间。
1 2 3 4 new Date (value) new Date (datString) new Date (dateObject) new Date (year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]])
1 2 3 4 5 6 7 8 function isValidDate (dateString ) { const dateObject = new Date (dateString) return !isNaN (dateObject.getTime ()) } isValidDate ('2023-11-25' ) isValidDate ('invalid-date' )
日期生成
toISOString()、toJSON()、JSON.stringify()
都能生成 ISO 8601 格式的 UTC 时间,toString()
会生成 RFC 2822 格式的本地时间。
需要说明一下 JSON.stringify
工作原理,JSON.stringify
在处理对象时,会调用该对象的 toJSON
方法,Data.prototype.toJSON
返回的是 ISO-8601 格式的 UTC 时间,而不是本地时间,结果比北京时间晚 8 小时。
1 JSON .stringify (new Date ())
可以重载此方法,使其返回本地时间。
1 2 3 4 5 6 7 8 Date .prototype .toJSON = function ( ) { return this .toLocaleString () } const o = new Date ()JSON .stringify (o) o.toString ()
对任何 Object 实例修改 toJSON
都会影响 JSON.stringify
的输出。
1 2 3 4 5 6 function Foo ( ) {}Foo .prototype .toJSON = function ( ){ return 'this is an instance of Foo' } JSON .stringify (new Foo )
toLocaleString
默认返回格式受浏览器或操作系统的语言设置影响,格式不统一,ES6 基于 Intl
API 扩展了 toLocaleString
方法,可设置 option 参数统一格式。
1 2 3 4 5 6 7 8 9 10 const options = { year : 'numeric' , month : '2-digit' , day : '2-digit' , hour : '2-digit' , minute : '2-digit' , second : '2-digit' , hour12 : false , } new Date ().toLocaleString ('zh-CN' , options).replace (/\//g , '-' )
toLocaleDateString
、toLocaleTimeString
同理。
Unix 时间戳表示自 1970 年 1 月 1 日 00:00:00 UTC (Unix 纪元,Unix Epoch) 以来的毫秒数。在 JavaScript 中,有多种方法可以获取时间戳。
1 2 3 4 5 new Date ().getTime ()new Date ().valueOf ()+new Date () Date .now ()Date .parse (dateString)
日期计算
setDate
方法根据本地时间来指定一个日期对象的天数。
1 2 3 4 5 6 7 8 9 10 function getSubtractDate (subtractNum, date = new Date () ) { const d = new Date (date) d.setDate (d.getDate () + subtractNum) return d.toISOString () } getSubtractDate (-10 ) getSubtractDate (10 ) getSubtractDate (10 , '2016-04-08' )
1 2 3 4 5 6 7 8 9 function getStartOfWeek (date = new Date () ) { const today = new Date (date) const dayOfWeek = today.getDay () const startOfWeek = new Date (today) startOfWeek.setDate (today.getDate () - (dayOfWeek + 6 ) % 7 ) return startOfWeek }
1 2 3 4 5 6 function getStartOfMonth (date = new Date () ) { const today = new Date (date) const startOfMonth = new Date (today.getFullYear (), today.getMonth (), 1 ) return startOfMonth }
setDate
参数如果超出了月份的合理范围,会向上个月或下个月设置,<= 0
时,会设置上个月的日期,0
是最后一天,-1
是倒数第二天,以此类推,超出范围的正整数同理。
1 2 3 const d = new Date ()d.setDate (0 ) d
可以利用这个特性来获取月份的天数和判断是否是闰年。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function getDaysInMonth (year, month ) { return new Date (year, month, 0 ).getDate () } getDaysInMonth (2017 , 10 ) function getDaysInYear (year ) { const daysInMonth = Array .from ({ length : 12 }, (_, month ) => { return new Date (year, month + 1 , 0 ).getDate () }) return daysInMonth } getDaysInYear (2016 )
一年中除了 2 月,其它所有月份天数都是固定的,1、3、5、7、8、10、12 月,有 31 天,4、6、9、11 月,有 30 天。2 月通常有 28 天,然而,为了与地球公转周期相匹配,每四年有一个闰年,百年不闰,四百年再闰,这时 2 月有 29 天。
1 2 3 4 5 6 7 8 9 10 11 function isLeapYear (year ) { return (year % 4 == 0 ) && (year % 100 != 0 || year % 400 == 0 ) } function isLeapYear (year ) { return new Date (year, 2 , 0 ).getDate () === 29 } isLeapYear (2000 )
一个闰年是能被 4 整除,但是不能被 100 整除,或者能被 400 整除的年份。例如,2016 年时闰年(能被 4 整除,但是不能被 100 整除),2000 年是闰年(能被 400 整除),而 1900年不是闰年(能被 4 整除、能被 100 整除,但不能被 400 整除)。
注意:当在同一个 Date
对象上连续执行 setDate
操作时,下一个 setDate
会在上一个 setDate
结果上进行操作。
1 2 3 4 5 6 const d = new Date ()d.setDate (0 ) d d.setDate (-1 ) d
1 2 3 4 5 6 7 8 const dateFrom = '2016-04-08T00:00:00Z' const datefromTS = (new Date (dateFrom)).getTime ()const nowTS = new Date ().getTime ()const diff = Math .abs (datefromTS - nowTS)Math .round (diff / (1000 * 60 * 60 * 24 ))
日期对象支持 <
、>
、<=
和 >=
比较,但由于引用类型的缘故,不支持 ==
比较,比较日期字符串也不靠谱,同一个日期在不同日期格式下显然无法对比,所以最好的办法是对比时间戳。
1 2 3 4 new Date ('Apr 08, 2016' ) < new Date ('Apr 09, 2016' ) new Date ('Apr 08, 2016' ) == new Date ('Apr 08, 2016' ) `${new Date ().getFullYear()} -${new Date ().getMonth() + 1 } -${new Date ().getDate()} ` == '2016-12-25' new Date ('Apr 08, 2016' ).getTime () == new Date ('Apr 08, 2016' ).getTime ()
格式化 虽然 ES6 中可通过 Intl.DateTimeFormat
对日期时间格式化,但这个方自定义格式选项不足,开发中还是需要手动实现格式化方法。下面简单实现格式化 YYYY-MM-DDThh:mm:ss
、YYYY/MM/DDThh:mm:ss
和相对时间。
1 2 3 4 5 6 7 8 9 function padZero (s ) { return ('0' + s).slice (-2 ) } const d = new Date ()let date = d.getFullYear () + '-' + padZero (d.getMonth () + 1 ) + '-' + padZero (d.getDate ())date += ' ' date += padZero (d.getHours ()) + ':' + padZero (d.getMinutes ()) + ':' + padZero (d.getSeconds ()) date
1 2 3 4 5 6 7 8 const d = new Date ()const date = [ [d.getFullYear (), d.getMonth () + 1 , d.getDate ()].join ('-' ), [d.getHours (), d.getMinutes (), d.getSeconds ()].join (':' ) ].join (' ' ).replace (/(?=\b\d\b)/g , '0' ) date
1 2 3 4 5 const d = new Date ()d.setMinutes (d.getMinutes () - d.getTimezoneOffset ()) const date = d.toISOString ().slice (0 , -5 ).replace (/[T]/g , ' ' )date
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function formatDateTime (date, format ) { const year = date.getFullYear () const month = padZero (date.getMonth () + 1 ) const day = padZero (date.getDate ()) const hours = padZero (date.getHours ()) const minutes = padZero (date.getMinutes ()) const seconds = padZero (date.getSeconds ()) format = format.replace ('yyyy' , year) format = format.replace ('MM' , month) format = format.replace ('dd' , day) format = format.replace ('HH' , hours) format = format.replace ('mm' , minutes) format = format.replace ('ss' , seconds) return format } formatDateTime (new Date (), 'yyyy-MM-dd HH:mm:ss' )formatDateTime (new Date (), 'yyyy/MM/dd HH:mm:ss' )formatDateTime (new Date (), 'HH:mm:ss' )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const getUnix = ( ) => { const date = new Date () return date.getTime () } const getTodayUnix = ( ) => { const date = new Date () date.setHours (0 ) date.setMinutes (0 ) date.setSeconds (0 ) date.setMilliseconds (0 ) return date.getTime () } const getDate = time => { const date = new Date (time) const month = `0${date.getMonth() + 1 } ` .slice (-2 ) const day = `0${date.getDate()} ` .slice (-2 ) return date.getFullYear () + '-' + month + '-' + day } const getFormatTime = fromTS => { const nowTS = getUnix () const todayTS = getTodayUnix () const diff = (nowTS - fromTS) / 1000 let tip = '' if (diff <= 0 ) { tip = '刚刚' } else if (Math .floor (diff / 60 ) <= 0 ) { tip = '刚刚' } else if (diff < 3600 ) { tip = Math .floor (diff / 60 ) + '分钟前' } else if (diff >= 3600 && fromTS - todayTS >= 0 ) { tip = Math .floor (diff / 3600 ) + '小时前' } else if (diff / 86400 <= 31 ) { tip = Math .ceil (diff / 86400 ) + '天前' } else { tip = getDate (fromTS) } return tip }
Temporal API Date API 设计存在问题,计算 API 缺失,日期时间计算需要手动实现,只支持 UTC 和用户 PC 时间,不支持非公历,开发中一般需要通过 moment、dayjs 这样一些第三方库使用。为了解决 Date API 的问题,TC39 提出了新的日期时间 API Temporal 。
1 2 3 4 5 仅可以创建和处理不可变 Temporal 对象 提供用于日期和时间计算的简单 API 支持所有时区 从 ISO-8601 格式进行严格的日期解析 支持非公历
截止 2022 年 2 月,该 API 尚处 Stage 3 阶段。