为什么 Date.parse 给出不正确的结果?输入端输出侧

2022-08-29 23:29:56

案例一:

new Date(Date.parse("Jul 8, 2005"));

输出:

星期五 七月 08 2005 00:00:00 GMT-0700 (PST)

案例二:

new Date(Date.parse("2005-07-08"));

输出:

星期四 七月 07 2005 17:00:00 GMT-0700 (PST)


为什么第二个解析不正确?


答案 1

在第 5 版规范发布之前,Date.parse 方法完全依赖于实现(等效于 Date.parse(字符串),除非后者返回一个数字而不是一个 )。在第 5 版规范中,添加了支持简化(且略微不正确)的 ISO-8601 的要求(另请参阅 JavaScript 中什么是有效的日期时间字符串?)。但除此之外,除了他们必须接受任何输出(没有说明那是什么)之外,没有要求/应该接受什么。new Date(string)DateDate.parsenew Date(string)Date#toString

从 ECMAScript 2017(第 8 版)开始,实现需要解析和 的输出,但未指定这些字符串的格式。Date#toStringDate#toUTCString

从 ECMAScript 2019(第 9 版)开始,Date#toStringDate#toUTCString 的格式已分别指定为:

  1. ddd MMM DD YYYY HH:mm:ss ZZ [(时区名称)]
    例如 星期二 七月 10 2018 18:39:58 GMT+0530 (IST)
  2. ddd, DD MMM YYYY HH:mm:ss Z
    例如 星期二 10 七月 2018 13:09:58 GMT

提供另外 2 种格式,这些格式应该在新实现中可靠地解析(请注意,支持并不普遍,不合规的实现将持续使用一段时间)。Date.parse

我建议手动解析日期字符串,并将 Date 构造函数与年、月和日参数一起使用,以避免歧义:

// parse a date in yyyy-mm-dd format
function parseDate(input) {

  let parts = input.split('-');

  // new Date(year, month [, day [, hours[, minutes[, seconds[, ms]]]]])
  return new Date(parts[0], parts[1]-1, parts[2]); // Note: months are 0-based
}

答案 2

在最近编写JS解释器的经验中,我对ECMA / JS日期的内部工作原理进行了大量斗争。所以,我想我会在这里投入我的2美分。希望分享这些东西可以帮助其他人解决有关浏览器之间如何处理日期差异的任何问题。

输入端

所有实现都在内部将其日期值存储为 64 位数字,表示自 1970-01-01 UTC(GMT 与 UTC 相同)以来的毫秒数 (ms)。这个日期是 ECMAScript 时代,其他语言(如 Java 和 POSIX 系统,如 UNIX)也使用它。纪元之后出现的日期为正数,之前的日期为负数。

以下代码在所有当前浏览器中都解释为相同的日期,但具有本地时区偏移量:

Date.parse('1/1/1970'); // 1 January, 1970

在我的时区(EST,即 -05:00)中,结果是 18000000,因为这是 5 小时内的毫秒数(夏令时期间只有 4 小时)。该值在不同时区中会有所不同。此行为在 ECMA-262 中指定,因此所有浏览器都以相同的方式执行此操作。

虽然主流浏览器将解析为日期的输入字符串格式存在一些差异,但它们基本上对时区和夏令时的解释是相同的,即使解析在很大程度上取决于实现。

但是,ISO 8601 格式是不同的。它是 ECMAScript 2015 (ed 6) 中概述的仅有的两种格式之一,所有实现都必须以相同的方式解析(另一种是为 Date.prototype.toString 指定的格式)。

但是,即使对于ISO 8601格式字符串,某些实现也会出错。以下是Chrome和Firefox的比较输出,当这个答案最初是在我的机器上为1/1/1970(纪元)编写的,使用ISO 8601格式字符串,在所有实现中都应解析为完全相同的值:

Date.parse('1970-01-01T00:00:00Z');       // Chrome: 0         FF: 0
Date.parse('1970-01-01T00:00:00-0500');   // Chrome: 18000000  FF: 18000000
Date.parse('1970-01-01T00:00:00');        // Chrome: 0         FF: 18000000
  • 在第一种情况下,“Z”说明符指示输入处于 UTC 时间,因此不与 epoch 偏移,结果为 0
  • 在第二种情况下,“-0500”说明符指示输入位于 GMT-05:00 中,并且两个浏览器都将输入解释为位于 -05:00 时区。这意味着 UTC 值与纪元偏移,这意味着在日期的内部时间值上增加 18000000ms。
  • 第三种情况(如果没有说明符)被视为主机系统的本地情况。FF 会正确地将输入内容视为本地时间,而 Chrome 会将其视为 UTC,因此会产生不同的时间值。对我来说,这在存储值中产生了5小时的差异,这是有问题的。具有不同偏移的其他系统将获得不同的结果。

截至2020年,此差异已得到修复,但在解析ISO 8601格式字符串时,浏览器之间存在其他怪癖。

但情况变得更糟。ECMA-262 的一个怪癖是,ISO 8601 纯日期格式 (YYYY-MM-DD) 需要解析为 UTC,而 ISO 8601 要求将其解析为本地格式。以下是FF的输出,具有长和短ISO日期格式,没有时区说明符。

Date.parse('1970-01-01T00:00:00');       // 18000000
Date.parse('1970-01-01');                // 0

因此,第一个被解析为本地,因为它是ISO 8601日期和时间,没有时区,第二个被解析为UTC,因为它只是ISO 8601日期。

因此,要直接回答原始问题,ECMA-262要求将其解释为UTC,而另一个则被解释为本地。这就是为什么:"YYYY-MM-DD"

这不会产生等效的结果:

console.log(new Date(Date.parse("Jul 8, 2005")).toString()); // Local
console.log(new Date(Date.parse("2005-07-08")).toString());  // UTC

这样做可以:

console.log(new Date(Date.parse("Jul 8, 2005")).toString());
console.log(new Date(Date.parse("2005-07-08T00:00:00")).toString());

底线是解析日期字符串。唯一可以跨浏览器安全解析的 ISO 8601 字符串是带有偏移量(±HH:mm 或“Z”)的长格式。如果这样做,则可以安全地在本地时间和UTC时间之间来回切换。

这适用于跨浏览器(在IE9之后):

console.log(new Date(Date.parse("2005-07-08T00:00:00Z")).toString());

大多数当前的浏览器确实平等地对待其他输入格式,包括常用的“1/1/1970”(M/D/YYYY)和“1/1/1970 00:00:00 AM”(M/D/YYYY hh:mm:ss ap)格式。以下所有格式(最后一种格式除外)在所有浏览器中都被视为本地时间输入。此代码的输出在我时区的所有浏览器中都是相同的。无论主机时区如何,最后一个都被视为 -05:00,因为偏移量是在时间戳中设置的:

console.log(Date.parse("1/1/1970"));
console.log(Date.parse("1/1/1970 12:00:00 AM"));
console.log(Date.parse("Thu Jan 01 1970"));
console.log(Date.parse("Thu Jan 01 1970 00:00:00"));
console.log(Date.parse("Thu Jan 01 1970 00:00:00 GMT-0500"));

但是,由于解析 ECMA-262 中指定的格式也不一致,因此建议永远不要依赖内置解析器,并始终手动解析字符串,例如使用库并向解析器提供格式。

例如,在一瞬间.js你可以写:

let m = moment('1/1/1970', 'M/D/YYYY'); 

输出侧

在输出端,所有浏览器都以相同的方式翻译时区,但它们处理字符串格式的方式不同。以下是函数及其输出的内容。请注意,在我的计算机上输出和函数 5:00 AM。此外,时区名称可能是缩写,并且在不同的实现中可能有所不同。toStringtoUTCStringtoISOString

打印前从 UTC 转换为本地时间

 - toString
 - toDateString
 - toTimeString
 - toLocaleString
 - toLocaleDateString
 - toLocaleTimeString

直接打印存储的 UTC 时间

 - toUTCString
 - toISOString 

In Chrome
toString            Thu Jan 01 1970 00:00:00 GMT-05:00 (Eastern Standard Time)
toDateString        Thu Jan 01 1970
toTimeString        00:00:00 GMT-05:00 (Eastern Standard Time)
toLocaleString      1/1/1970 12:00:00 AM
toLocaleDateString  1/1/1970
toLocaleTimeString  00:00:00 AM

toUTCString         Thu, 01 Jan 1970 05:00:00 GMT
toISOString         1970-01-01T05:00:00.000Z

In Firefox
toString            Thu Jan 01 1970 00:00:00 GMT-05:00 (Eastern Standard Time)
toDateString        Thu Jan 01 1970
toTimeString        00:00:00 GMT-0500 (Eastern Standard Time)
toLocaleString      Thursday, January 01, 1970 12:00:00 AM
toLocaleDateString  Thursday, January 01, 1970
toLocaleTimeString  12:00:00 AM

toUTCString         Thu, 01 Jan 1970 05:00:00 GMT
toISOString         1970-01-01T05:00:00.000Z

我通常不使用ISO格式进行字符串输入。使用这种格式对我有益的唯一时刻是当日期需要作为字符串排序时。ISO 格式可按原样排序,而其他格式则不然。如果必须具有跨浏览器兼容性,请指定时区或使用兼容的字符串格式。

代码将经历以下内部伪转换:new Date('12/4/2013').toString()

  "12/4/2013" -> toUCT -> [storage] -> toLocal -> print "12/4/2013"

我希望这个答案是有帮助的。