在最近编写 JS 解释器的经验中,我与 ECMA/JS 日期的内部工作作了很多斗争。所以,我想我会在这里投入我的 2 美分。希望分享这些内容可以帮助其他人解决有关浏览器在处理日期方面的差异的任何问题。
输入端
所有实现都将其日期值在内部存储为 64 位数字,表示自 1970-01-01 UTC 以来的毫秒数 (ms)(GMT 与 UTC 相同)。该日期是 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 的比较输出,当这个答案最初是在我的机器上使用 ISO 8601 格式字符串为 1970 年 1 月 1 日(纪元)编写的时所有实现:
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 日期。
因此,要直接回答原始问题,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 函数及其输出内容。请注意 toUTCString 和 toISOString 函数在我的机器上输出 5:00 AM。此外,时区名称可能是缩写,并且在不同的实现中可能不同。
在打印前从 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"
我希望这个答案有帮助。