为什么 Date.parse 给出不正确的结果?

IT技术 javascript date
2021-01-02 21:41:54

案例一:

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

输出:

2005 年 7 月 8 日星期五 00:00:00 GMT-0700 (PST)

案例二:

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

输出:

2005 年 7 月 7 日星期四 17:00:00 GMT-0700 (PST)


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

6个回答

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

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

自 ECMAScript 2019(第 9 版)起,Date#toString的格式Date#toUTCString已(分别)指定为:

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

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

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

// 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
}
太好了,我不得不使用它,因为Date.parse由于某种原因我无法解决英国日期格式
2021-02-10 21:41:54
时间部分记录在@CMS 代码中。我使用此代码的日期格式为“2012-01-31 12:00:00” return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);完美运行,谢谢!
2021-02-22 21:41:54
我也遇到过 new Date(string) 在不同浏览器中表现不同的问题。它甚至不是在旧版本的 IE 上被破坏的问题,不同的浏览器是不一致的。永远不要使用 Date.parse 或 new Date(string) 。
2021-03-03 21:41:54
@CMS 你所说的依赖实现是什么意思
2021-03-04 21:41:54
@RoyiNamir,这意味着结果取决于运行代码的 Web 浏览器(或其他 JavaScript 实现)。
2021-03-08 21:41:54

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

输入端

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

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

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

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

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

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

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

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 时间,因此与纪元没有偏移,结果为 0
  • 在第二种情况下,“-0500”说明符表示输入在 GMT-05:00 并且两个浏览器都将输入解释为在 -05:00 时区。这意味着 UTC 值与纪元有偏移,这意味着将 18000000 毫秒添加到日期的内部时间值。
  • 第三种情况,没有说明符,应该被视为主机系统的本地。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 日期。

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

这不会产生等效的结果:

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 中指定的格式也不一致,因此建议永远不要依赖内置解析器并始终手动解析字符串,例如使用库并将格式提供给解析器。

例如,在 moment.js 中,您可能会这样写:

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

输出端

在输出端,所有浏览器都以相同的方式转换时区,但它们处理字符串格式的方式不同。这是toString函数及其输出的内容。注意我机器上 5:00 AMtoUTCStringtoISOString函数输出。此外,时区名称可能是缩写,在不同的实现中可能会有所不同。

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

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

直接打印存储的UTC时间

 - toUTCString
 - toISOString 

在 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

在火狐浏览器中
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"

我希望这个答案有帮助。

...包括最常用的“1/1/1970”和“1/1/1970 00:00:00 AM”格式。— 最常使用在哪里那肯定不是在我的国家。
2021-02-21 21:41:54
@ulidtko - 抱歉,我在美国。哇...你就在基辅。我希望你和你的家人保持安全,那里的情况很快就会稳定下来。照顾好自己,祝一切顺利。
2021-02-21 21:41:54
@Daniel——幸运的是,ECMAScript 作者修复了他们关于缺少日期和时间表示的时区的错误。现在没有时区的日期和时间字符串使用主机时区偏移量(即“本地”)。令人困惑的是,仅 ISO 8601 日期形式被视为 UTC(即使规范中没有特别明确),而 ISO 8601 将它们视为本地形式,因此它们并没有解决所有问题。
2021-02-25 21:41:54
首先,这是一篇很棒的文章。但是,我想指出一个依赖项。关于时区说明符,你说:“没有说明符应该假定本地时间输入。” 值得庆幸的是,ECMA-262 标准消除了任何假设的必要。它指出:“不存在的时区偏移的值是“Z”。” 因此,没有指定时区的日期/时间字符串被假定为 UTC 而不是本地时间。当然,与 JavaScript 的许多东西一样,实现之间似乎几乎没有一致。
2021-03-02 21:41:54
这里只是一个注释。这似乎不适用于 Safari 浏览器(即 iOS 或 OSX)。那或者我有其他一些问题。
2021-03-03 21:41:54

有一些疯狂的方法。作为一般规则,如果浏览器可以将日期解释为 ISO-8601,它就会。“2005-07-08”属于这个阵营,所以解析为UTC。“2005 年 7 月 8 日”不能,因此在本地时间解析。

看看JavaScript 和日期,真是一团糟!更多。

作为一般规则,如果浏览器可以将日期解释为 ISO-8601,它就会。 ”是不支持的。“2020-03-20 13:30:30”被许多浏览器视为 ISO 8601 和本地标准,但被 Safari 视为无效日期。大多数浏览器不支持许多 ISO 8601 格式,例如 2004-W53-7 和 2020-092。
2021-03-07 21:41:54

另一种解决方案是使用日期格式构建关联数组,然后重新格式化数据。

此方法对于以不寻常方式格式化的日期很有用。

一个例子:

    mydate='01.02.12 10:20:43':
    myformat='dd/mm/yy HH:MM:ss';


    dtsplit=mydate.split(/[\/ .:]/);
    dfsplit=myformat.split(/[\/ .:]/);

    // creates assoc array for date
    df = new Array();
    for(dc=0;dc<6;dc++) {
            df[dfsplit[dc]]=dtsplit[dc];
            }

    // uses assc array for standard mysql format
    dstring[r] = '20'+df['yy']+'-'+df['mm']+'-'+df['dd'];
    dstring[r] += ' '+df['HH']+':'+df['MM']+':'+df['ss'];

使用moment.js解析日期:

var caseOne = moment("Jul 8, 2005", "MMM D, YYYY", true).toDate();
var caseTwo = moment("2005-07-08", "YYYY-MM-DD", true).toDate();

第三个参数确定严格解析(从 2.3.0 开始可用)。如果没有它,moment.js 也可能给出不正确的结果。