修正 Chrome 50 中关于 Date.parse 的问题
关键词
ISO-8601 日期时间字符串
Date.parse
修订记录
2018.12.15, 增加对标准ISO-8601 的解释, 明确了时区标记符 的概念
1 2 3 4 5 function logDate (dateString) { const time = Date .parse (dateString) console .log (time, new Date (time)) }
问题
最近在项目开发中遇到一个问题,在 Chrome 63 中Date.parse
和 Chrome 50 中Date.parse
在解析形如 "2018-01-20T00:29:18"
格式的字符串时,行为不一致。
相关规范
标准的 ISO-8601 格式
ISO-8601 格式的字符串包含以下这些:
Year: YYYY (eg 1997)
Year and month: YYYY-MM (eg 1997-07)
Complete date: YYYY-MM-DD (eg 1997-07-16)
Complete date plus hours and minutes: YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
Complete date plus hours, minutes and seconds: YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
Complete date plus hours, minutes, seconds and a decimal fraction of a second
YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
注意后面三种, 是带有
T 分割符号 (用于划分包括日在内以上的数值和日以下的数值)
和
时区标记符(TZD) 的
时区标记符 跟在秒之后, 合法的时区标记符包括:
简化版的 ISO-8601 格式
Js 中完全可能收到这样的时间字符串
2011-10-10
这种是标准的ISO-8601 格式字符串
2011-10-10T14:48:00
这种是简化版的ISO-8601 格式字符串.
可以看到, 和协议中建议的格式相比, 第 2 种格式省去了时区标记符.
对于这种格式?Date.parse()
应该理所当然地认为它表示"UTC0"时间么?
答案是否定的 .
实际上, ES5 对此有规范:
当一个时间字符串包含了完整的年月日时分秒信息, 却不带时区标记符 时, 它会被 Date.parse()
当成本地时间.
下一节会解释这一点:
JS 中Date.parse()
支持的时间格式
MDN: Date.parse
中的关于 es5 对 ISO-8601 格式的字符串的支持的描述如下:
The date time string may be in a simplified ISO-8601 format. For example, "2011-10-10" (just date) or "2011-10-10T14:48:00" (date and time) can be passed and parsed. Where the string is ISO-8601 date only, the UTC time zone is used to interpret arguments. If the string is date and time in ISO-8601 format, it will be treated as local.
简单翻译:
时间字符串可能是以简化版 ISO-8601 格式 传入的,比如 "2011-10-10"(仅有日期) 或者 "2011-10-10T14:48:00"(含有日期和时间)被传给Date.parse
时:
如果只包含了日期信息,则 UTC 时区 会用于被作为解释器的参数。即会认为传入的字符串是UTC 0 时间
如果字符串同事包含了日期和时间信息,则会被当做是本地时间 处理。
根据这条规则(假设系统所在地为东八区)
Date.parse("2011-10-10")
应该被当做格林威治时间的2011-10-10T00:00:00
,对于东八区而则是2011-10-10T08:00:00
Date.parse("2011-10-10T14:48:00")
应该被当做本地时间,对应格林威治时间的2011-10-10T06:48:00,
即这是东八区的 2011-10-10T14:48:00
用这个时间执行一遍logDate
,结果如下:
显然,在 Chrome 50 中,对于同时包含日期和时间(形如 "2018-01-20T00:29:18")
的字符串并没有正确处理,本应将其看作本地时间,却将其当做了 UTC 0 时间。猜想这是
Chrome 50 对应版本的 v8 的锅。
Chrome 60
Chrome 53
2011-10-10
认为是 UTC 0 时间 (✔)
认为是 UTC 0 时间 (✔)
2011-10-10T14:48:00
认为是本地时间 (✔)
认为是 UTC 0 时间 (❌)
解决 回到问题本身,在项目中有这样的一个
dateFormat
函数
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 function padStart (str = '' , len = 2 , padContent = '0' ) { while (str.length < len) { str = padContent + str } return str } function dateFormat (value, format = 'YYYY-MM-dd HH:mm:ss' ) { const time = new Date (value).getTime () const dateObj = new Date (time) const year = dateObj.getFullYear () const month = dateObj.getMonth () + 1 const date = dateObj.getDate () const hours = dateObj.getHours () const minutes = dateObj.getMinutes () const seconds = dateObj.getSeconds () const rs = format .replace ('YYYY' , padStart (year + '' , 4 )) .replace ('MM' , padStart (month + '' )) .replace ('dd' , padStart (date + '' )) .replace ('HH' , padStart (hours + '' )) .replace ('mm' , padStart (minutes + '' )) .replace ('ss' , padStart (seconds + '' )) return rs }
在 Chrome 50 中,dateFormat("2018-01-20T00:29:18")
这段代码将无法在系统地区为非 GMT 0 时区的环境下得到预期的结果
为了解决这个问题,我们需要检测浏览器对 GMT +0 以外的时区是否遵循了MDN: Date.parse
中所描述的规则,并且在不遵守规则的情况下,将误差值计算出来。
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 function parseDateEnvInfo () { const accurate = new Date (0 ) const iso8601 = new Date ("1970-01-01T00:00:00" ) const offsetMs = accurate.getTimezoneOffset () * 6e4 const in_gmt_0 = offsetMs === 0 const offsetMsCalculated = iso8601 - accurate const follow_iso_8601_outside_gmt0_zone = offsetMs === offsetMsCalculated return { in_gmt_0, follow_iso_8601_outside_gmt0_zone, error_offset_ms_when_date_parse : offsetMsCalculated - offsetMs, offsetMs, offsetMsCalculated } }
对之前的formatDate 略作修改
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 const ISO_8601_REG = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ function dateFormat (value, format = 'YYYY-MM-dd HH:mm:ss' ) { const errorValue = ISO_8601_REG .test (value) ? parseDateEnvInfo ().error_offset_ms_when_date_parse : 0 const time = new Date (value).getTime () - errorValue const dateObj = new Date (time) const year = dateObj.getFullYear () const month = dateObj.getMonth () + 1 const date = dateObj.getDate () const hours = dateObj.getHours () const minutes = dateObj.getMinutes () const seconds = dateObj.getSeconds () const rs = format .replace ('YYYY' , padStart (year + '' , 4 )) .replace ('MM' , padStart (month + '' )) .replace ('dd' , padStart (date + '' )) .replace ('HH' , padStart (hours + '' )) .replace ('mm' , padStart (minutes + '' )) .replace ('ss' , padStart (seconds + '' )) return rs }
在浏览器里尝试运行:
完整代码 完整代码 view raw 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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 function padStart (str = '' , len = 2 , padContent = '0' ) { while (str.length < len) { str = padContent + str } return str } function parseDateEnvInfo () { const accurate = new Date (0 ) const iso8601 = new Date ("1970-01-01T00:00:00" ) const offsetMs = accurate.getTimezoneOffset () * 6e4 const in_gmt_0 = offsetMs === 0 const offsetMsCalculated = iso8601 - accurate const follow_iso_8601_outside_gmt0_zone = offsetMs === offsetMsCalculated return { in_gmt_0, follow_iso_8601_outside_gmt0_zone, error_offset_ms_when_date_parse : offsetMsCalculated - offsetMs, offsetMs, offsetMsCalculated } } const ISO_8601_REG = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/ function dateFormat (value, format = 'YYYY-MM-dd HH:mm:ss' ) { const errorValue = ISO_8601_REG .test (value) ? parseDateEnvInfo ().error_offset_ms_when_date_parse : 0 const time = Date .parse (value) - errorValue const dateObj = new Date (time) const year = dateObj.getFullYear () const month = dateObj.getMonth () + 1 const date = dateObj.getDate () const hours = dateObj.getHours () const minutes = dateObj.getMinutes () const seconds = dateObj.getSeconds () const rs = format .replace ('YYYY' , padStart (year + '' , 4 )) .replace ('MM' , padStart (month + '' )) .replace ('dd' , padStart (date + '' )) .replace ('HH' , padStart (hours + '' )) .replace ('mm' , padStart (minutes + '' )) .replace ('ss' , padStart (seconds + '' )) return rs }
检测工具
这里有一个检测工具可以用来检测您阅读这篇文章的时候使用的浏览器的Date.parse
在解析 ISO-8601 日期时期字符串的时候是否有正确的行为.
(完)
参考