关键词
- ISO-8601 日期时间字符串
- Date.parse
// 用于打印 Unix 时间戳和其结构化的 Date 对象
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)的字符串时,行为不一致。
相关规范
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函数
// helper: padStart
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
中所描述的规则,并且在不遵守规则的情况下,将误差值计算出来。
// 分析环境中的 Date 对象信息
function parseDateEnvInfo () {
// new Date(numberOrString) 时, 如果传入的是字符串, 内部会调用 Date.parse 解析传入的参数
// 为了对比检测环境对 ISO-8601 格式字符串的解析是否正确,我们
// 使用 new Date(0) 来创建系统初始时间
const accurate = new Date(0)
const iso8601 = new Date("1970-01-01T00:00:00")
// 从系统中获取时间差, 判定环境是否属于 GMT 0 时区
const offsetMs = accurate.getTimezoneOffset() * 6e4
const in_gmt_0 = offsetMs === 0
const offsetMsCalculated = iso8601 - accurate
// 对比两个值:
// 1. 正确的时区 millisecond 值: offsetMs;
// 2. 解析 ISO-8601 日期时间字符串得到的"本地时间"和 UTC 0 时间之间的 millisecond 值: offsetMsCalculated
//
// 如果两个值不相等, 说明环境不遵守解析 ISO-8601 日期时间字符串的规则, 在解析字符串的时候,
// 总会产生一个误差值; 反之, 说明环境正确解析了 ISO-8601 日期时间字符串
const follow_iso_8601_outside_gmt0_zone = offsetMs === offsetMsCalculated
return {
in_gmt_0,
follow_iso_8601_outside_gmt0_zone,
// 这个值便是环境中解析 ISO-8601 日期时间字符串时的误差值, 如果没有误差, 这个值为 0
error_offset_ms_when_date_parse: offsetMsCalculated - offsetMs,
offsetMs,
offsetMsCalculated
}
}
对之前的 formatDate
略作修改
// 该正则表达式并不能匹配出所有的 ISO_8601 格式的字符串, 此处仅用作示例
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
}
在浏览器里尝试运行:
完整代码
function padStart (str = '', len = 2, padContent = '0') {
while (str.length < len) {
str = padContent + str
}
return str
}
// 分析环境中的 Date 对象信息
function parseDateEnvInfo () {
// new Date(numberOrString) 时, 如果传入的是字符串, 内部会调用 Date.parse 解析传入的参数
// 为了对比检测环境对 ISO-8601 格式字符串的解析是否正确,我们
// 使用 new Date(0) 来创建系统初始时间
const accurate = new Date(0)
const iso8601 = new Date("1970-01-01T00:00:00")
// 从系统中获取时间差, 判定环境是否属于 GMT 0 时区
const offsetMs = accurate.getTimezoneOffset() * 6e4
const in_gmt_0 = offsetMs === 0
const offsetMsCalculated = iso8601 - accurate
// 对比两个值:
// 1. 正确的时区 millisecond 值: offsetMs;
// 2. 解析 ISO-8601 日期时间字符串得到的"本地时间"和 UTC 0 时间之间的 millisecond 值: offsetMsCalculated
//
// 如果两个值不相等, 说明环境不遵守解析 ISO-8601 日期时间字符串的规则, 在解析字符串的时候,
// 总会产生一个误差值; 反之, 说明环境正确解析了 ISO-8601 日期时间字符串
const follow_iso_8601_outside_gmt0_zone = offsetMs === offsetMsCalculated
return {
in_gmt_0,
follow_iso_8601_outside_gmt0_zone,
// 这个值便是环境中解析 ISO-8601 日期时间字符串时的误差值, 如果没有误差, 这个值为 0
error_offset_ms_when_date_parse: offsetMsCalculated - offsetMs,
offsetMs,
offsetMsCalculated
}
}
// 该正则表达式并不能匹配出所有的 ISO_8601 格式的字符串, 此处仅用作示例
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 日期时期字符串的时候是否有正确的行为.
检测工具请狠狠戳 这里 并拉到底部
参考
- [ISO-8601]
- [MDN] Date.parse