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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: ### 关键词 - 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
相关文章
|
7月前
|
JavaScript
【Js】检查Date对象是否为Invalid Date
【Js】检查Date对象是否为Invalid Date
226 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
241 0
|
Python 容器
【Python标准库】argparse的add_argument() 方法介绍
【Python标准库】argparse的add_argument() 方法介绍
|
机器学习/深度学习 存储
argparse库
argparse库
dateparser解析常见的时间字符串
dateparser解析常见的时间字符串
88 0
|
SQL
format函数
format函数
147 0
|
Python
argparse使用方法简单总结
argparse使用方法简单总结 argparse是python自带的命令行参数解析包,可以用来方便地读取命令行参数,当你的代码需要频繁地修改参数的时候,使用这个工具可以将参数和代码分离开来,让你的代码更简洁,适用范围更广。
428 0
|
SQL 监控
Log Parser 2.2 + Log Parser Lizard GUI 分析IIS日志示例
Log Parser 日志分析工具,用命令行操作,可以分析 IIS logs,event logs,active directory,log4net,file system,t-sql Log Parser Lizard 以可视化界面操作,使用类似sql的语法查询   下载地址: Log Parser 2.
2897 0