为什么Date.parse会给出错误的结果?

405

案例一:

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

输出:

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

Case Two:

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

输出:

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


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


33
第二个解析本身并没有错误,只是第一个解析是按本地时间解析的,而第二个解析是按协调世界时解析的。请注意,“Thu Jul 07 2005 17:00:00 GMT-0700 (PST)”与“2005-07-08 00:00”是相同的。 - jches
1
jsPerf: http://jsperf.com/value-of-date-input-to-date-objectjsPerf:http://jsperf.com/value-of-date-input-to-date-object - Web_Designer
23
ISO 8601是一个国际标准,用于表示日期和时间。它使用了一种简单的格式来表示日期和时间,例如“2023-03-16T14:30:00”。这个标准的重要性在于它能够避免由于不同地区使用不同格式而导致的混淆和误解。 - ulidtko
1
如果有人来这里查找为什么在Firefox中返回NaN日期,我发现大多数其他浏览器(和Node.js)将解析没有日期的日期,例如“2014年4月”作为2014年4月1日,但Firefox返回NaN。您必须传递正确的日期。 - Jazzy
1
补充Jason上面的评论:如果你在Firefox中收到NaN,另一个问题可能是Firefox和Safari不喜欢带连字符的日期格式,只有Chrome支持。请使用斜杠代替。 - Bangkokian
显示剩余2条评论
11个回答

487

在第五版规范发布之前,Date.parse 方法是完全依赖于实现的(new Date(string) 等同于 Date.parse(string),只不过后者返回一个数字而不是 Date)。在第五版规范中添加了对简化版(稍有错误)ISO-8601标准的支持(也请参阅JavaScript 中有效的日期时间字符串是什么?)。但除此之外,Date.parse/new Date(string) 应接受的内容没有其他要求(也没有说明这些内容是什么)。

截至 ECMAScript 2017(第8版),实现必须解析 Date#toStringDate#toUTCString 的输出,但未指定这些字符串的格式。

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

  1. ddd MMM DD YYYY HH:mm:ss ZZ [(timezone name)]
    例如:Tue Jul 10 2018 18:39:58 GMT+0530 (IST)
  2. ddd, DD MMM YYYY HH:mm:ss Z
    例如:Tue 10 Jul 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
}

太好了,我不得不使用这个方法,因为Date.parse无法处理英国日期格式,而我又找不出原因。 - Ben
1
时间部分在@CMS代码中有记录。我使用了这段代码,并采用了日期格式“2012-01-31 12:00:00”。return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]); 完美地运行,谢谢! - Richard Rout
2
@CMS中的“_implementation dependent_”是什么意思? - Royi Namir
3
@RoyiNamir,它的意思是结果取决于运行你的代码的Web浏览器(或其他JavaScript实现)。 - Samuel Edwin Ward
1
我也遇到过在不同浏览器中使用new Date(string)时出现不同行为的问题。这甚至不是在旧版本的IE上出现问题,而是不同的浏览器之间不一致。永远不要使用Date.parse或new Date(string)。 - Hoffmann
是的,在所有情况下,提供的答案都不是正确的工作方式。 - aggsol

232

最近编写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格式字符串,某些实现也会出错。以下是使用ISO 8601格式字符串在我的计算机上为1970年1月1日(纪元)编写答案时Chrome和Firefox的比较输出,这些字符串“应该”在所有实现中被解析为完全相同的值:
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要求它被解析为本地时间。以下是使用长和短ISO日期格式且没有时区指示符的FF输出。

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

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

因此,直接回答原始问题,ECMA-262要求将"YYYY-MM-DD"解释为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函数及其输出内容。请注意,toUTCStringtoISOString函数在我的机器上输出为上午5:00。此外,时区名称可能是缩写,并且在不同的实现中可能不同。

在打印之前将时间从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

在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" -> toUTC -> [storage] -> toLocal -> print "12/4/2013"

我希望这个答案对你有所帮助。


3
首先,这是一篇精彩的文章。然而,我想指出一个依赖关系。关于时区标识符,您提到:“缺少标识符应该假定为本地时间输入。”幸运的是,ECMA-262标准消除了任何需要假定的必要性。它规定:“缺少时区偏移量的值为“Z””。因此,没有指定时区的日期/时间字符串被假定为UTC而不是本地时间。当然,就像JavaScript中的许多事情一样,各种实现之间似乎很少达成一致。 - Daniel
2
...包括最常用的“1/1/1970”和“1/1/1970 00:00:00 AM”格式。——最常用于哪里?这肯定不是在我的国家。 - ulidtko
3
@ulidtko - 抱歉,我在美国。哇...你就在基辅。希望你和你的家人保持安全,希望那里的情况很快稳定下来。照顾好自己,祝一切顺利。 - drankin2112
1
@Daniel——幸运的是,ECMAScript的作者们已经修复了有关日期和时间表示中缺少时区的错误。现在,没有时区的日期和时间字符串使用主机时区偏移量(即“本地”)。令人困惑的是,ISO 8601仅包含日期格式被视为UTC(尽管从规范上来看并不特别清楚),而ISO 8601将其视为本地时间,因此他们并没有修复所有问题。 - RobG
因此,“因此,以下代码……”是一个非续集。用于存储时间值的格式与解析无关。“具有讽刺意味的是,[ISO 8601]是浏览器可能不同的格式”。实际上,它们也因为许多非ISO格式而不同。这个答案中有一些好的信息,但通常不能建议使用内置解析器来自信地解析任何格式。 - RobG
显示剩余4条评论

74

这种方法有一定的规律。通常,如果浏览器可以将日期解释为ISO-8601格式,则会这样做。"2005-07-08"符合此条件,因此被解析为UTC时间。而"Jul 8, 2005"则不行,它会被解析为本地时间。

更多内容请参见JavaScript and Dates, What a Mess!


3
通常情况下,如果浏览器能够将日期解释为ISO-8601格式,它就会这样做。然而,“2020-03-20 13:30:30”被许多浏览器视为本地的ISO 8601格式,但在Safari中则被视为无效日期。许多ISO 8601格式并不被大多数浏览器支持,例如“2004-W53-7”和“2020-092”。 - RobG

7

使用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可能会给出不正确的结果。

7

另一个解决方案是构建一个具有日期格式的关联数组,然后重新格式化数据。

这种方法适用于以不寻常方式格式化的日期。

例如:

    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'];

4

虽然CMS是正确的,传递字符串到解析方法通常是不安全的,但新的ECMA-262第5版(也称为ES5)规范在第15.9.4.2节中建议Date.parse()实际上应该处理ISO格式的日期。旧规范没有这样的说法。当然,旧浏览器和一些当前浏览器仍然不提供此ES5功能。

你的第二个示例并没有错。它是UTC指定日期,如Date.prototype.toISOString()所示,但以你的本地时区表示。


1
日期字符串的解析在 ECMAScript 2015 中再次更改,因此 "2005-07-08" 是本地时间,而不是 UTC。顺便说一下,ES5 直到 2011 年 6 月才成为标准(当前标准是 ECMAScript 2015)。;-) - RobG
1
为了让事情更加混乱,TC39决定在我上一篇文章发布后的一个月(即10月份),“2005-07-08” 应该是UTC,但是“2005-07-08T00:00:00”应该是本地时间。两者都是符合ISO 8601标准的格式,两者都没有时区,但处理方式不同。去想吧。 - RobG

4
根据http://blog.dygraphs.com/2012/03/javascript-and-dates-what-mess.html所述,“yyyy/mm/dd”格式解决了通常的问题。 他说:“尽可能使用“YYYY/MM/DD”作为日期字符串。它得到了普遍支持并且没有歧义。使用这种格式,所有时间都是本地时间。” 我已经设置了测试:http://jsfiddle.net/jlanus/ND2Qg/432/ 该格式: + 通过使用y m d排序和4位数年份避免了日期和月份的歧义。 + 通过使用斜杠避免了不符合ISO格式的UTC与本地时间问题。 + dygraphs的说这种格式在所有浏览器中都很好。

您可能想查看作者的答案 - Brad Koch
如果你使用jQuery,那么在jsFiddle中的示例解决方案应该足够好,因为它使用了datepicker解析器。在我的情况下,问题出现在jqGrid中,但我发现它有自己的parseDate方法。无论如何,这个示例给了我灵感,所以+1,谢谢。 - Vasil Popov
3
那篇关于dygraphs的文章是错误的,页面上的第一个示例清楚地说明为什么使用斜杠而不是连字符是非常糟糕的建议。在写作那篇文章时,使用“2012/03/13”会导致浏览器将其解析为本地日期,而不是UTC。ECMAScript规范明确支持使用“YYYY-MM-DD”(ISO8601),因此应始终使用连字符。需要注意的是,在我撰写这篇评论时,Chrome已被修补以将斜杠视为UTC。 - Lachlan Hunt

2

两种方式都是正确的,但它们被解释为带有两个不同时区的日期。所以你比较的是苹果和橙子:

// local dates
new Date("Jul 8, 2005").toISOString()            // "2005-07-08T07:00:00.000Z"
new Date("2005-07-08T00:00-07:00").toISOString() // "2005-07-08T07:00:00.000Z"
// UTC dates
new Date("Jul 8, 2005 UTC").toISOString()        // "2005-07-08T00:00:00.000Z"
new Date("2005-07-08").toISOString()             // "2005-07-08T00:00:00.000Z"

我删除了Date.parse()的调用,因为它会自动用于字符串参数。我还使用ISO8601格式比较日期,这样您可以在本地日期和UTC日期之间直观地进行比较。这些时间相差7个小时,这是时区差异,也是您的测试显示两个不同日期的原因。
创建这些相同的本地/UTC日期的另一种方法是:
new Date(2005, 7-1, 8)           // "2005-07-08T07:00:00.000Z"
new Date(Date.UTC(2005, 7-1, 8)) // "2005-07-08T00:00:00.000Z"

但我仍然强烈推荐Moment.js,它既简单又强大:Moment.js文档

// parse string
moment("2005-07-08").format()       // "2005-07-08T00:00:00+02:00"
moment.utc("2005-07-08").format()   // "2005-07-08T00:00:00Z"
// year, month, day, etc.
moment([2005, 7-1, 8]).format()     // "2005-07-08T00:00:00+02:00"
moment.utc([2005, 7-1, 8]).format() // "2005-07-08T00:00:00Z"

2

以下是一段简短灵活的代码,可以跨浏览器地将日期时间字符串转换成格式详细说明的方式,由@drankin2112提供。

var inputTimestamp = "2014-04-29 13:00:15"; //example

var partsTimestamp = inputTimestamp.split(/[ \/:-]/g);
if(partsTimestamp.length < 6) {
    partsTimestamp = partsTimestamp.concat(['00', '00', '00'].slice(0, 6 - partsTimestamp.length));
}
//if your string-format is something like '7/02/2014'...
//use: var tstring = partsTimestamp.slice(0, 3).reverse().join('-');
var tstring = partsTimestamp.slice(0, 3).join('-');
tstring += 'T' + partsTimestamp.slice(3).join(':') + 'Z'; //configure as needed
var timestamp = Date.parse(tstring);

您的浏览器应该提供与Date.parse相同的时间戳结果:

(new Date(tstring)).getTime()

我建议在正则表达式中添加T,以便捕获已经格式化为JS日期的日期:inputTimestamp.split(/[T /:-]/g)。 - andig
如果您将字符串拆分为组件部分,则最可靠的下一步是使用这些部分作为参数传递给日期构造函数。创建另一个字符串以提供给解析器只会让您回到第1步。 "2014-04-29 13:00:15" 应解析为本地时间,但您的代码将其重新格式化为UTC。 :-( - RobG

2

这个轻量级的日期解析库(flexible-js-formatting)可以解决所有类似的问题。我喜欢这个库,因为它非常易于扩展。它也可以进行国际化(不是很直观,但也不难)。

解析示例:

var caseOne = Date.parseDate("Jul 8, 2005", "M d, Y");
var caseTwo = Date.parseDate("2005-07-08", "Y-m-d");

将格式化后的内容转回字符串(您会注意到两种情况都会得到完全相同的结果):

console.log( caseOne.dateFormat("M d, Y") );
console.log( caseTwo.dateFormat("M d, Y") );
console.log( caseOne.dateFormat("Y-m-d") );
console.log( caseTwo.dateFormat("Y-m-d") );

无法找到此库。 - Deian
有一个存档在谷歌代码上,但似乎这个链接也是一样的:https://github.com/flavorjones/flexible-js-formatting - Nux

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接