修正 Chrome 50 中关于 Date.parse 的问题

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: ### 关键词 - ISO-8601 日期时间字符串 - Date.parse ``` // 用于打印 Unix 时间戳和其结构化的 Date 对象 function logDate (dateString) { const time = Date.parse(dateString) console.log(time, new Date(time)) } ```

关键词

  • 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)的字符串时,行为不一致。

image.png
image.png

相关规范

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,结果如下:

image.png
image.png

显然,在 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 时区的环境下得到预期的结果

image.png

为了解决这个问题,我们需要检测浏览器对 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
}

在浏览器里尝试运行:

image.png

完整代码

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
相关文章
|
6月前
|
JavaScript
【Js】检查Date对象是否为Invalid Date
【Js】检查Date对象是否为Invalid Date
196 0
|
Python
Python参数解析工具argparse.ArgumentParser()
Python参数解析工具argparse.ArgumentParser()
|
存储 数据库
Gson (自定义转化器) 日期转换异常:Caused by: java.text.ParseException: Failed to parse date
Gson (自定义转化器) 日期转换异常:Caused by: java.text.ParseException: Failed to parse date
220 0
类型转换——convert、parse和try parse
类型转换——convert、parse和try parse
120 0
categorical_crossentropy与sparse_categorical_crossentropy的区别
categorical_crossentropy与sparse_categorical_crossentropy的区别
dateparser解析常见的时间字符串
dateparser解析常见的时间字符串
84 0
C#(四十九)之Convert类 和 Parse方法的区别
Convert.ToDouble与Double.Parse的区别:实际上Convert.ToDouble与 Double.Parse 较为类似,实际上 Convert.ToDouble内部调用了 Double.Parse 。
319 0
C#(四十九)之Convert类 和 Parse方法的区别
|
C++
Win10 Parse error at “IID“
Win10 Parse error at “IID“
505 0
Win10 Parse error at “IID“
|
Web App开发 JavaScript 前端开发
IE new Date()无效
IE 时间兼容