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


344

情况一:

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

输出:

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

案例二:

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

输出:

Thu Jul 07 2005 17:00:00 GMT-0700(PST)


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


30
第二个解析本身并不是错误的,只是第一个解析是在本地时间解析的,第二个解析是在UTC解析的。请注意,“ Thu Jul 2005 2005 17:00:00 GMT-0700(PST)”与“ 2005-07-08 00:00”相同。
jches 2012年



1
如果有人来这里弄清楚为什么要NaN在Firefox中返回日期,我发现大多数其他浏览器(和Node.js)都可以解析日期,而没有一天,例如2014年4月1日为“ 2014年4月”,但是Firefox返回NaN。您必须通过适当的日期。
2014年

补充一下Jason在上面的评论:如果您在Firefox中收到NaN,则另一个问题可能是Firefox和Safari不喜欢连字符日期。只有Chrome可以。请改用斜线。
Bangkokian

Answers:


451

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

如2017年的ECMAScript(版本8)的,被要求的实现来解析其输出Date#toStringDate#toUTCString,但是未指定这些字符串的格式。

从ECMAScript 2019(版本9)开始,Date#toString和的格式Date#toUTCString分别指定为:

  1. ddd MMM DD YYYY HH:mm:ss ZZ [(时区名称)]
    例如Tue Jul 10 2018 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应在新的实现中可靠解析的格式(请注意,支持并不普遍,并且不兼容的实现将在一段时间内继续使用)。

我建议手动解析日期字符串,并将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不是与英国的日期格式表现为某种原因,我不能工作了

1
时间部分以@CMS代码记录。我使用的日期格式为“ 2012-01-31 12:00:00”的这段代码 return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);非常完美,谢谢!
理查德·

1
@CMS您所说的依赖实现是什么意思 ?
罗伊·纳米尔

3
@RoyiNamir,这意味着结果取决于正在运行您的代码的Web浏览器(或其他JavaScript实现)。
Samuel Edwin Ward

1
我在新的Date(string)在不同的浏览器中也遇到了问题。这甚至不是在旧版本的IE上被破坏的问题,不同的浏览器并不一致。永远不要使用Date.parse或新的Date(string)。
霍夫曼

195

在最近写JS解释器的经验中,我为ECMA / JS日期的内部工作付出了很多努力。因此,我认为我将在这里投入2美分。希望共享这些内容可以帮助其他人解决有关浏览器在处理日期方面的差异的任何问题。

输入端

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

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

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的比较输出,当此答案最初是在我的计算机上使用1970年1月1日(纪元)时使用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时间,因此不偏离历元,结果为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日期。

因此,要直接回答原始问题,"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 的toUTCStringtoISOString函数输出。另外,时区名称可能是缩写,在不同的实现中可能会有所不同。

在打印之前从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" -> toUCT -> [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 2014年

2
@ulidtko-抱歉,我在美国。哇...你就在基辅。我希望您和您的家人保持安全,并希望那里的一切稳定下来。照顾好自己,祝一切顺利。
drankin2112

这里只是一个说明。看来这不适用于Safari浏览器(即iOS或OSX)。那或者我还有其他问题。
keyneom 2014年

1
@Daniel-幸运的是,ECMAScript作者修复了有关日期和时间表示形式缺少时区的错误。现在不带时区的日期和时间字符串使用主机时区偏移量(即“本地”)。令人困惑的是,ISO 8601仅日期格式被视为UTC(即使在规范中不是特别清楚),而ISO 8601将它们视为本地格式,因此它们不能解决所有问题。
RobG

70

疯狂是有办法的。通常,如果浏览器可以将日期解释为ISO-8601,它将被解释为日期。“ 2005-07-08”属于此阵营,因此被解析为UTC。“ 2005年7月8日”不能,因此在当地时间进行了解析。

看到JavaScript和日期,真是麻烦!更多。


3
一般来说,如果浏览器可以将日期解释为ISO-8601,它将被支持。”不支持。许多浏览器将“ 2020-03-20 13:30:30”视为ISO 8601和本地标准,但Safari将其视为“无效日期”。大多数浏览器不支持许多ISO 8601格式,例如2004-W53-7和2020-092。
RobG

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

5

使用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可能还会给出错误的结果。


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/ 此格式:+通过使用ymd排序和4位数字的年份避免了日和月顺序的歧义+避免了UTC与本地问题的冲突dygraphs家伙通过使用斜杠+ danvk来符合ISO格式,他说这种格式在所有浏览器中都很好。


您可能需要查看作者的答案
布拉德·科赫

我想说的是,如果您使用jQuery,则jsFiddle中的示例解决方案效果很好,因为它使用datepicker解析器。在我的情况下,问题出在jqGrid,但发现它具有其parseDate方法。但是无论如何,这个例子通过给我一个想法让我+1,谢谢了我。
Vasil Popov

2
那篇关于音标的文章是错误的,该页面上的第一个示例清楚地说明了为什么使用斜杠代替连字符确实是个坏建议。在撰写本文时,使用“ 2012/03/13”导致浏览器将其解析为本地日期,而不是UTC。ECMAScript规范定义了对使用“ YYYY-MM-DD”(ISO8601)的明确支持,因此请始终使用连字符。应该注意的是,在我撰写此评论时,已对Chrome进行了修补,以将斜杠视为UTC。
Lachlan Hunt

4

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

您的第二个示例没有错。它是UTC中指定的日期,由Date.prototype.toISOString()表示,但以您当地的时区表示。


1
并且日期字符串的解析在ECMAScript 2015中再次更改,因此“ 2005-07-08”是本地的,而不是UTC。顺便说一句,直到2011年6月,ES5才成为标准(目前是ECMAScript 2015)。;-)
RobG 2015年

1
只是为了混淆,TC39于10月(仅在我上一篇文章发表后一个月)决定将“ 2005-07-08”设置为UTC,但是将“ 20052005-08T00:00:00”设置为本地,两者均为ISO 8601兼容格式,两者
都没有时

2

这个轻量级的日期解析库应该解决所有类似的问题。我喜欢该库,因为它很容易扩展。也有可能(不是很简单,但是就不那么难了)。

解析示例:

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") );

2

这是一个简短,灵活的代码段,以跨浏览器安全的方式转换datetime字符串,如@ 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

如果将字符串拆分为组成部分,则最可靠的下一步是将这些部分用作Date构造函数的参数。创建另一个字符串以提供给解析器只是使您返回到步骤1。“ 2014-04-29 13:00:15”应解析为本地,您的代码会将其重新格式化为UTC。:-(
RobG '17年

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它既简单又强大

// 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"

0

从CMS接受的答案是正确的,我刚才已经增加了一些功能:

  • 修剪和清理输入空间
  • 解析斜线,破折号,冒号和空格
  • 具有默认的日期和时间

// parse a date time that can contains spaces, dashes, slashes, colons
function parseDate(input) {
    // trimes and remove multiple spaces and split by expected characters
    var parts = input.trim().replace(/ +(?= )/g,'').split(/[\s-\/:]/)
    // new Date(year, month [, day [, hours[, minutes[, seconds[, ms]]]]])
    return new Date(parts[0], parts[1]-1, parts[2] || 1, parts[3] || 0, parts[4] || 0, parts[5] || 0); // Note: months are 0-based
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.