关于javascript:为什么date.parse会给出错误的结果?

Why does Date.parse give incorrect results?

案例一:

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

输出:

免费的圣诞节:2005年08 00 00:00格林尼治0700(PST)

案例二:

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

输出:

周四,2005年圣诞节07 17:00:00格林尼治0700(PST)

先生/ < >

为什么在第二incorrect parse冰淇淋吗?


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

截至ECMAScript 2017(第8版),要求实现对其输出进行日期到字符串和日期到字符串的解析,但未指定这些字符串的格式。

自ECMAScript 2019(第9版)起,日期至字符串和日期至字符串的格式分别规定为:

  • ddd mmm dd yyyy hh:mm:ss zz[(时区名称)]例如2018年7月10日星期二18:39:58 GMT+0530(IST)
  • ddd,dd-mmm-yyyy hh:mm:ss-ze.g.2018年7月10日星期二13:09:58 GMT
  • 在新的实现中,还提供了两种更加可靠的date.parse解析格式(注意,支持并不普遍,不兼容的实现将在一段时间内继续使用)。

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

    1
    2
    3
    4
    5
    6
    // parse a date in yyyy-mm-dd format
    function parseDate(input) {
      var 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
    }


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

    所有实现都将其日期值内部存储为64位数字,表示自1970年1月1日UTC(GMT与UTC相同)以来的毫秒数。发生在1/1/1970 00:00:00之后的日期为正数,发生在前面的日期为负数。好的。

    因此,以下代码在所有浏览器上产生完全相同的结果。好的。

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

    在我的时区(EST),结果是18000000,因为这就是5小时内的毫秒数(在夏令时月份只有4小时)。该值在不同的时区不同。所有主要的浏览器都是这样做的。好的。

    但这是困难所在。虽然主要浏览器将输入字符串格式解析为日期时存在一些差异,但就时区和夏令时而言,它们基本上是相同的。其中一个是ISO 8601格式。这是ECMA-262 V.5规范中特别列出的唯一格式。对于所有其他字符串格式,解释依赖于实现。具有讽刺意味的是,这是浏览器可以不同的格式。这里是Chrome与Firefox 1970年1月1日在我的机器上使用ISO 8601字符串格式的比较输出。好的。

    1
    2
    3
    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时间,在存储之前不需要偏移量。
    • "-0500"说明符表示输入的格式是GMT-05:00,因此浏览器将输入解释为在本地时区中。这意味着值在存储前转换为UTC。在我的例子中,这意味着在日期的内部值上加上180000000毫秒,因此需要-180000000毫秒(-05:00)的移位才能使我回到本地时间。
    • 但是,当没有说明符时,FF将输入视为本地时间而chrome将其视为UTC时间。对我来说,这会在存储值上造成5小时的差异,这是有问题的。在我的实现中,我最终站到了FF这边,因为我喜欢toString的输出来匹配我的输入值,除非我指定了一个备用时区,这是我从来没有做过的。缺少说明符应假定为本地时间输入。

    但是这里情况更糟,FF对待ISO 8601格式("YYYY-MM-DD")的短格式与对待长格式("YYYY-MM-DDTHH:MM:SS:SSSZ")的方式不同,没有任何逻辑原因。这里是FF的输出,长和短的ISO日期格式没有时区说明符。好的。

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

    因此,直接回答原始提问者的问题,"YYYY-MM-DD"是iso 8601格式"YYYY-MM-DDTHH:mm:ss:sssZ"的简称。因此,它被解释为UTC时间,而另一个被解释为本地时间。这就是为什么,好的。这并不能改变:

    1
    2
    console.log(new Date(Date.parse("Jul 8, 2005")).toString());
    console.log(new Date(Date.parse("2005-07-08")).toString());

    这样做:

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

    底线是用于分析日期字符串。唯一可以跨浏览器安全分析的ISO 8601字符串是长格式。并且,始终使用"z"说明符。如果这样做,您可以在本地时间和UTC时间之间安全地来回走动。好的。这可以在浏览器中使用(在IE9之后):

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

    幸运的是,大多数当前的浏览器对其他输入格式的处理是相同的,包括最常用的"1/1/1970"和"1/1/1970 00:00:00 AM"格式。以下所有格式(和其他格式)在所有浏览器中都被视为本地时间输入,并在存储之前转换为UTC。因此,使它们跨浏览器兼容。此代码的输出在时区内的所有浏览器中都是相同的。好的。

    1
    2
    3
    4
    5
    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"));

    输出端

    在输出端,所有浏览器都以相同的方式转换时区,但它们处理字符串格式的方式不同。下面是toString函数及其输出。请注意,我的机器上的toUTCStringtoISOString功能输出为上午5:00。好的。

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

    1
    2
    3
    4
    5
    6
     - toString
     - toDateString
     - toTimeString
     - toLocaleString
     - toLocaleDateString
     - toLocaleTimeString

    直接打印存储的UTC时间好的。

    1
    2
     - toUTCString
     - toISOString
    1
    In Chrome
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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
    1
    In Firefox
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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()经过以下内部伪转换:好的。

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

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


    有一些方法可以解决这种疯狂。作为一般规则,如果浏览器可以将日期解释为ISO-8601,它会这样做。"2005-07-08"属于这个阵营,所以它被解析为UTC。"2005年7月8日"不能,所以在当地时间解析。

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


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

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

    一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
        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分析日期:

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

    第3个参数决定严格解析(从2.3.0开始提供)。没有它,moment.js也会给出错误的结果。


    根据http://blog.dygraphs.com/2012/03/javascript-and-dates-what-mess.html,格式"YYYY/MM/DD"解决了常见的问题。他说:"只要有可能,你的日期串都要用"年/月/日"。它得到了普遍的支持,而且很明确。使用这种格式,所有时间都是本地的。"我已经设置了测试:http://jsfiddle.net/jlanus/nd2qg/432/这种格式:+通过使用Y M D排序和4位年份避免日和月顺序不明确。+通过使用斜线避免UTC与本地问题不符合ISO格式+Dygraphs的家伙Danvk说这种格式在所有浏览器中都很好。


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

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


    两者都是正确的,但它们被解释为具有两个不同时区的日期。所以你比较了苹果和桔子:

    1
    2
    3
    4
    5
    6
    // 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日期的另一种方法是:

    1
    2
    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,它既简单又强大:

    1
    2
    3
    4
    5
    6
    // 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"

    下面是一个简短、灵活的代码片段,用于以跨浏览器安全方式转换日期时间字符串,如@drankin2112详述的nicel。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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相同的时间戳结果:

    1
    (new Date(tstring)).getTime()


    这个轻量级的数据分析库应该解决所有类似的问题。我喜欢图书馆,因为它很容易扩展。它也有可能是I18N(不是很直,但不是很难)。

    分析示例:

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

    并将格式设置回字符串(您将注意到这两种情况的结果完全相同):

    1
    2
    3
    4
    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") );

    CMS接受的答案是正确的,我刚刚添加了一些功能:

    • 修剪和清洁输入空间
    • 分析斜线、短划线、冒号和空格
    • 有默认的日期和时间
    1
    2
    3
    4
    5
    6
    7
    // 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
    }