日期时间操作权威指南

已发表: 2022-03-11

作为软件开发人员,您不能逃避日期操作。 开发人员构建的几乎每个应用程序都会有一些组件,需要从用户那里获取日期/时间,存储在数据库中,然后显示给用户。

向任何程序员询问他们处理日期和时区的经验,他们可能会分享一些战争故事。 处理日期和时间字段当然不是火箭科学,但通常很乏味且容易出错。

有数百篇关于这个主题的文章,然而,大多数要么过于学术,专注于细节,要么过于零散,提供了简短的代码片段,没有太多解释。 这份关于 DateTime 操作的深入指南将帮助您了解与时间和日期相关的编程概念和最佳实践,而无需浏览有关该主题的大量信息。

在本文中,我将帮助您清楚地思考日期和时间字段,并提出一些可以帮助您避免日期/时间地狱的最佳实践。 在这里,我们将探讨正确操作日期和时间值所必需的一些关键概念、便于存储 DateTime 值并通过 API 传输它们的格式等。

对于初学者来说,生产代码的正确答案几乎总是使用适当的库,而不是自己编写。 本文讨论的 DateTime 计算的潜在困难只是冰山一角,但无论有没有库,了解它们仍然很有帮助。

如果您正确理解它们,DateTime 库会有所帮助

日期库在许多方面都有助于让您的生活更轻松。 它们极大地简化了日期解析、日期算术和逻辑运算以及日期格式。 您可以为前端和后端找到一个可靠的日期库来为您完成大部分繁重的工作。

但是,我们经常使用日期库而不考虑日期/时间的实际工作方式。 日期/时间是一个复杂的概念。 由于其不正确的理解而出现的错误可能非常难以理解和修复,即使在日期库的帮助下也是如此。 作为程序员,您需要了解基础知识并能够理解日期库解决的问题以充分利用它们。

此外,日期/时间库只能带您到此为止。 所有日期库都通过让您访问方便的数据结构来表示 DateTime 来工作。 如果您通过 REST API 发送和接收数据,您最终需要将日期转换为字符串,反之亦然,因为 JSON 没有本机数据结构来表示 DateTime。 我在这里概述的概念将帮助您避免在执行这些日期到字符串和字符串到日期的转换时可能出现的一些常见问题。

注意:尽管我使用 JavaScript 作为本文讨论的编程语言,但这些是在很大程度上适用于几乎所有编程语言及其日期库的一般概念。 因此,即使您以前从未编写过一行 JavaScript,也请随时继续阅读,因为我几乎不假设本文中对 JavaScript 有任何先验知识。

标准化时间

DateTime 是一个非常具体的时间点。 让我们考虑一下。 当我写这篇文章时,我笔记本电脑上的时钟显示为 7 月 21 日下午 1:29。 这就是我们所说的“当地时间”,即我在我周围的挂钟和手表上看到的时间。

给或花几分钟,如果我让我的朋友在下午 3:00 在附近的咖啡馆与我见面,我可以预期大约在那个时间在那里见到她。 同样,如果我说,例如,“让我们在一个半小时后见面”,也不会造成任何混乱。 我们经常以这种方式与生活在同一城市或时区的人谈论时间。

让我们设想一个不同的场景:我想告诉一位住在瑞典乌普萨拉的朋友,我想在下午 5 点与他交谈。 我给他发了一条信息,“嗨,安东,我们下午 5 点谈谈。” 我立即得到回应,“你的时间还是我的时间?”

Anton 告诉我他住在中欧时区,即 UTC+01:00。 我住在 UTC+05:45。 这意味着当我住的地方是下午 5 点时,它是 5 PM - 05:45 = 11:15 AM UTC,在乌普萨拉转换为 11:15 AM UTC + 01:00 = 12:15 PM,非常适合两者我们。

此外,请注意时区(中欧时间)和时区偏移量(UTC+05:45)之间的差异。 出于政治原因,国家/地区也可以决定更改夏令时的时区偏移量。 几乎每年至少在一个国家/地区都会对规则进行更改,这意味着包含这些规则的任何代码都必须保持最新——值得考虑的是,您的应用程序的每一层都依赖于什么代码库。

这是我们建议在大多数情况下仅前端处理时区的另一个充分理由。 如果不匹配,当您的数据库引擎使用的规则与您的前端或后端的规则不匹配时会发生什么?

管理两个不同版本的时间(相对于用户和相对于普遍接受的标准)的问题是困难的,在精确度是关键的编程世界中更是如此,甚至一秒钟都可以产生巨大的差异。 解决这些问题的第一步是将 DateTime 存储在 UTC 中。

标准化格式

时间标准化非常棒,因为我只需要存储 UTC 时间,只要我知道用户的时区,我就可以随时转换为他们的时间。 相反,如果我知道用户的本地时间并知道他们的时区,我可以将其转换为 UTC。

但是日期和时间可以用许多不同的格式指定。 对于日期,您可以写“7 月 30 日”或“7 月 30 日”或“7/30”(或 30/7,取决于您居住的地方)。 当时,你可以写“9:30 PM”或“2130”。

世界各地的科学家齐心协力解决这个问题,并决定采用一种格式来描述程序员真正喜欢的时间,因为它又短又精确。 我们喜欢称其为“ISO 日期格式”,它是 ISO-8601 扩展格式的简化版本,如下所示:

一张图片显示了 ISO-8601 扩展格式的简化版本,称为 ISO 日期格式。

对于 00:00 或 UTC,我们使用“Z”代替,这意味着祖鲁时间,UTC 的另一个名称。

JavaScript 中的日期操作和算术运算

在我们开始使用最佳实践之前,我们将学习使用 JavaScript 进行日期操作以掌握语法和一般概念。 尽管我们使用 JavaScript,但您可以轻松地将这些信息调整为您喜欢的编程语言。

我们将使用日期算术来解决大多数开发人员遇到的常见日期相关问题。

我的目标是让您从字符串中创建一个日期对象并从中提取组件。 这是日期库可以帮助您解决的问题,但最好了解它是如何在幕后完成的。

一旦我们掌握了日期/时间,就更容易思考我们面临的问题,提取最佳实践,然后继续前进。 如果您想跳到最佳实践,请随意这样做,但我强烈建议您至少浏览一下下面的日期算术部分。

JavaScript 日期对象

编程语言包含有用的结构,可以让我们的生活更轻松。 JavaScript Date对象就是这样一种东西。 它提供了方便的方法来获取当前日期和时间、将日期存储在变量中、执行日期算术以及根据用户的区域设置格式化日期。

由于浏览器实现之间的差异以及对夏令时 (DST) 的错误处理,不建议依赖于关键任务应用程序的 Date 对象,您可能应该使用像 Luxon、date-fns 或 dayjs 这样的 DateTime 库。 (无论你使用什么,都要避免使用曾经流行的 Moment.js——通常简称为moment ,因为它出现在代码中——因为它现在已被弃用。)

但出于教育目的,我们将使用 Date() 对象提供的方法来了解 JavaScript 如何处理 DateTime。

获取当前日期

const currentDate = new Date();

如果您不向 Date 构造函数传递任何内容,则返回的日期对象包含当前日期和时间。

然后,您可以对其进行格式化以仅提取日期部分,如下所示:

 const currentDate = new Date(); const currentDayOfMonth = currentDate.getDate(); const currentMonth = currentDate.getMonth(); // Be careful! January is 0, not 1 const currentYear = currentDate.getFullYear(); const dateString = currentDayOfMonth + "-" + (currentMonth + 1) + "-" + currentYear; // "27-11-2020"

注意:“一月是 0”的陷阱很常见,但并不普遍。 在您开始使用它之前,值得仔细检查任何语言(或配置格式:例如,cron 明显是基于 1)的文档。

获取当前时间戳

如果您想要获取当前时间戳,则可以创建一个新的 Date 对象并使用 getTime() 方法。

 const currentDate = new Date(); const timestamp = currentDate.getTime();

在 JavaScript 中,时间戳是自 1970 年 1 月 1 日以来经过的毫秒数。

如果你不打算支持<IE8,你可以使用Date.now()直接获取时间戳,而无需创建新的 Date 对象。

解析日期

将字符串转换为 JavaScript 日期对象以不同的方式完成。

Date 对象的构造函数接受多种日期格式:

 const date1 = new Date("Wed, 27 July 2016 13:30:00"); const date2 = new Date("Wed, 27 July 2016 07:45:00 UTC"); const date3 = new Date("27 July 2016 13:30:00 UTC+05:45");

请注意,您不需要包含星期几,因为 JS 可以确定任何日期的星期几。

您还可以将年、月、日、小时、分钟和秒作为单独的参数传递:

 const date = new Date(2016, 6, 27, 13, 30, 0);

当然,您始终可以使用 ISO 日期格式:

 const date = new Date("2016-07-27T07:45:00Z");

但是,如果您没有明确提供时区,您可能会遇到麻烦!

 const date1 = new Date("25 July 2016"); const date2 = new Date("July 25, 2016");

其中任何一个都将为您提供当地时间 2016 年 7 月 25 日 00:00:00。

如果您使用 ISO 格式,即使您只给出日期而不给出时间和时区,它也会自动接受时区为 UTC。

这意味着:

 new Date("25 July 2016").getTime() !== new Date("2016-07-25").getTime() new Date("2016-07-25").getTime() === new Date("2016-07-25T00:00:00Z").getTime()

格式化日期

幸运的是,现代 JavaScript 在标准Intl命名空间中内置了一些方便的国际化函数,使日期格式化成为一个简单的操作。

为此,我们需要两个对象:一个Date和一个Intl.DateTimeFormat ,使用我们的输出首选项进行初始化。 假设我们想使用美式 (M/D/YYYY) 格式,这看起来像:

 const firstValentineOfTheDecade = new Date(2020, 1, 14); // 1 for February const enUSFormatter = new Intl.DateTimeFormat('en-US'); console.log(enUSFormatter.format(firstValentineOfTheDecade)); // 2/14/2020

如果我们想要荷兰语 (D/M/YYYY) 格式,我们只需将不同的文化代码传递给DateTimeFormat构造函数:

 const nlBEFormatter = new Intl.DateTimeFormat('nl-BE'); console.log(nlBEFormatter.format(firstValentineOfTheDecade)); // 14/2/2020

或美国格式的更长形式,并拼出月份名称:

 const longEnUSFormatter = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric', }); console.log(longEnUSFormatter.format(firstValentineOfTheDecade)); // February 14, 2020

现在,如果我们想要一个正确的月份日期的序数格式——即“14th”而不是“14”——不幸的是,这需要一些解决方法,因为在撰写本文时day的唯一有效值是"numeric""2-digit" 。 借用 Flavio Copes 的 Mathias Bynens 代码版本来利用Intl的另一部分,我们可以通过formatToParts()自定义月份输出:

 const pluralRules = new Intl.PluralRules('en-US', { type: 'ordinal' }) const suffixes = { 'one': 'st', 'two': 'nd', 'few': 'rd', 'other': 'th' } const convertToOrdinal = (number) => `${number}${suffixes[pluralRules.select(number)]}` // At this point: // convertToOrdinal("1") === "1st" // convertToOrdinal("2") === "2nd" // etc. const extractValueAndCustomizeDayOfMonth = (part) => { if (part.type === "day") { return convertToOrdinal(part.value); } return part.value; }; console.log( longEnUSFormatter.formatToParts(firstValentineOfTheDecade) .map(extractValueAndCustomizeDayOfMonth) .join("") ); // February 14th, 2020

不幸的是,在撰写本文时,Internet Explorer (IE) 根本不支持formatToParts ,但所有其他桌面、移动和后端(即 Node.js)技术都支持。 对于那些需要支持 IE 并且绝对需要序数的人,下面的旁注(或者更好的是,适当的日期库)提供了答案。

如果您需要支持 IE 11 之前的旧浏览器,那么 JavaScript 中的日期格式化会更加困难,因为 Python 或 PHP 中没有像strftime这样的标准日期格式化函数。

例如,在 PHP 中,函数strftime("Today is %b %d %Y %X", mktime(5,10,0,12,30,99))为您提供Today is Dec 30 1999 05:10:00

您可以使用以%开头的不同字母组合来获取不同格式的日期。 (注意,并非每种语言都为每个字母赋予相同的含义——特别是,“M”和“m”可能会交换分钟和月份。)

如果您确定要使用的格式,最好使用我们上面介绍的 JavaScript 函数提取各个位并自己创建一个字符串。

 var currentDate = new Date (); var date = currentDate.getDate(); var month = currentDate.getMonth(); var year = currentDate.getFullYear();

我们可以得到 MM/DD/YYYY 格式的日期为

var monthDateYear = (month+ 1 ) + "/" + date + "/" + year;

此解决方案的问题在于,它可能会给出不一致的日期长度,因为该月的某些月份和日期是个位数,而另一些则是两位数。 这可能会有问题,例如,如果您在表格列中显示日期,因为日期没有对齐。

我们可以通过使用添加前导 0 的“pad”函数来解决这个问题。

 function pad ( n ) { return n< 10 ? '0' +n : n; }

现在,我们使用 MM/DD/YYYY 格式获得正确的日期:

 var mmddyyyy = pad(month + 1 ) + "/" + pad(date) + "/" + year;

如果我们想要 DD-MM-YYYY,过程类似:

 var ddmmyyyy = pad(date) + "-" + pad(month + 1 ) + "-" + year;

让我们加大赌注,尝试以“Month Date, Year”格式打印日期。 我们需要将月份索引映射到名称:

 var monthNames = [ "January" , "February" , "March" , "April" , "May" , "June" , "July" , "August" , "September" , "October" , "November" , "December" ]; var dateWithFullMonthName = monthNames[month] + " " + pad(date) + ", " + year;

有些人喜欢将日期显示为 2013 年 1 月 1 日。没问题,我们只需要一个辅助函数ordinal ,它返回 1 的第 1、12 的第 12 和 103 的第 103 等等,其余的很简单:

 var ordinalDate = ordinal(date) + " " + monthNames[month] + ", " + year;

从日期对象很容易确定星期几,所以让我们将其添加到:

 var daysOfWeek = [ "Sun" , "Mon" , "Tue" , "Wed" , "Thu" , "Fri" , "Sat" ]; ordinalDateWithDayOfWeek = daysOfWeek[currentDate.getDay()] + ", " + ordinalDate;

这里更重要的是,一旦你从日期中提取了数字,格式主要与字符串有关。

更改日期格式

一旦您知道如何解析日期并对其进行格式化,将日期从一种格式更改为另一种格式只需将两者结合起来即可。

例如,如果您有一个格式为 2013 年 7 月 21 日的日期,并且想将格式更改为 21-07-2013,则可以这样实现:

 const myDate = new Date("Jul 21, 2013"); const dayOfMonth = myDate.getDate(); const month = myDate.getMonth(); const year = myDate.getFullYear(); function pad(n) { return n<10 ? '0'+n : n } const ddmmyyyy = pad(dayOfMonth) + "-" + pad(month + 1) + "-" + year; // "21-07-2013"

使用 JavaScript 日期对象的本地化函数

我们上面讨论的日期格式化方法应该适用于大多数应用程序,但是如果你真的想本地化日期的格式化,我建议你使用Date对象的toLocaleDateString()方法:

 const today = new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', });

......给了我们类似26 Jul 2016的信息。

将语言环境更改为“en-US”会改为“Jul 26, 2016”。 注意格式是如何改变的,但显示选项仍然保持不变——这是一个非常有用的功能。 如上一节所示,较新的基于Intl.DateTimeFormat的技术的工作方式与此非常相似,但允许您重用格式化程序对象,因此您只需要设置一次选项。

使用toLocaleDateString() ,始终传递格式选项是一个好习惯,即使输出在您的计算机上看起来不错。 这可以保护 UI 不会因为月份名称太长而出现意外的语言环境,或者因为短名称而显得尴尬。

如果我想要整月“七月”,我所做的就是将选项中的月份参数更改为“长”。 JavaScript 为我处理一切。 对于 en-US,我现在得到的是 2016 年 7 月 26 日。

注意:如果您希望浏览器自动使用用户的语言环境,您可以传递“undefined”作为第一个参数。

如果您想显示日期的数字版本并且不想在不同的语言环境中为 MM/DD/YYYY 与 DD/MM/YYYY 大惊小怪,我建议使用以下简单的解决方案:

 const today = new Date().toLocaleDateString(undefined, { day: 'numeric', month: 'numeric', year: 'numeric', });

在我的电脑上,这会输出7/26/2016 。 如果要确保月份和日期有两位数,只需更改选项:

 const today = new Date().toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: 'numeric', });

这将输出07/26/2016 。 正是我们想要的!

您还可以使用其他一些相关功能来本地化时间和日期的显示方式:

代码输出描述
now.toLocaleTimeString()
“凌晨 4 点 21 分 38 秒” 显示唯一时间的本地化版本
now.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit', });
“凌晨 4 点 21 分 38 秒” 根据提供的选项显示本地化时间
now.toLocaleString()
“2016 年 7 月 22 日,凌晨 4 点 21 分 38 秒” 显示用户区域设置的日期和时间
now.toLocaleString(undefined, { day: 'numeric', month: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', });
“2016 年 7 月 22 日,凌晨 4 点 21 分” 根据提供的选项显示本地化日期和时间

计算相对日期和时间

这是一个将 20 天添加到 JavaScript 日期的示例(即,找出已知日期后 20 天的日期):

 const myDate = new Date("July 20, 2016 15:00:00"); const nextDayOfMonth = myDate.getDate() + 20; myDate.setDate(nextDayOfMonth); const newDate = myDate.toLocaleString();

原始日期对象现在表示 7 月 20 日之后 20 天的日期,并且newDate包含表示该日期的本地化字符串。 在我的浏览器中, newDate包含“8/9/2016, 3:00:00 PM”。

要计算比全天更精确的相对时间戳,可以使用Date.getTime()Date.setTime()来处理表示自某个时期(即 1970 年 1 月 1 日)以来的毫秒数的整数。对于例如,如果您想知道现在是 17 小时后的时间:

 const msSinceEpoch = (new Date()).getTime(); const seventeenHoursLater = new Date(msSinceEpoch + 17 * 60 * 60 * 1000);

比较日期

与与日期相关的所有其他事情一样,比较日期有其自身的陷阱。

首先,我们需要创建日期对象。 幸运的是,<、>、<= 和 >= 都有效。 因此,比较 2014 年 7 月 19 日和 2014 年 7 月 18 日很简单:

 const date1 = new Date("July 19, 2014"); const date2 = new Date("July 28, 2014"); if(date1 > date2) { console.log("First date is more recent"); } else { console.log("Second date is more recent"); }

检查相等性比较棘手,因为表示同一日期的两个日期对象仍然是两个不同的日期对象,并且不会相等。 比较日期字符串不是一个好主意,因为例如,“2014 年 7 月 20 日”和“2014 年 7 月 20 日”表示相同的日期,但具有不同的字符串表示。 下面的代码片段说明了第一点:

 const date1 = new Date("June 10, 2003"); const date2 = new Date(date1); const equalOrNot = date1 == date2 ? "equal" : "not equal"; console.log(equalOrNot);

这将输出not equal

这种特殊情况可以通过比较日期的整数等价物(它们的时间戳)来修复,如下所示:

 date1.getTime() == date2.getTime()

我在很多地方都看到过这个例子,但我不喜欢它,因为你通常不会从另一个日期对象创建一个日期对象。 所以我觉得这个例子仅从学术角度来看很重要。 此外,这要求两个 Date 对象都引用完全相同的秒,而您可能只想知道它们是否引用同一天、小时或分钟。

让我们看一个更实际的例子。 您正在尝试比较用户输入的生日是否与您从 API 获得的幸运日期相同。

 const userEnteredString = "12/20/1989"; // MM/DD/YYYY format const dateStringFromAPI = "1989-12-20T00:00:00Z"; const dateFromUserEnteredString = new Date(userEnteredString) const dateFromAPIString = new Date(dateStringFromAPI); if (dateFromUserEnteredString.getTime() == dateFromAPIString.getTime()) { transferOneMillionDollarsToUserAccount(); } else { doNothing(); }

两者都代表相同的日期,但不幸的是您的用户不会获得百万美元。

这就是问题所在:JavaScript 总是假定时区是浏览器提供的时区,除非另有明确说明。

这意味着,对我来说, new Date ("12/20/1989")将创建一个日期 1989-12-20T00:00:00+5:45 或 1989-12-19T18:15:00Z 这与1989-12-20T00:00:00Z 就时间戳而言。

仅更改现有日期对象的时区是不可能的,因此我们现在的目标是创建一个新的日期对象,但使用 UTC 而不是本地时区。

我们将忽略用户的时区并在创建日期对象时使用 UTC。 有两种方法可以做到:

  1. 从用户输入的日期创建一个 ISO 格式的日期字符串,并使用它来创建一个 Date 对象。 使用有效的 ISO 日期格式来创建 Date 对象,同时使 UTC 与本地的意图非常清楚。
 const userEnteredDate = "12/20/1989"; const parts = userEnteredDate.split("/"); const userEnteredDateISO = parts[2] + "-" + parts[0] + "-" + parts[1]; const userEnteredDateObj = new Date(userEnteredDateISO + "T00:00:00Z"); const dateFromAPI = new Date("1989-12-20T00:00:00Z"); const result = userEnteredDateObj.getTime() == dateFromAPI.getTime(); // true

如果您不指定时间,这也有效,因为它将默认为午夜(即 00:00:00Z):

 const userEnteredDate = new Date("1989-12-20"); const dateFromAPI = new Date("1989-12-20T00:00:00Z"); const result = userEnteredDate.getTime() == dateFromAPI.getTime(); // true

请记住:如果向日期构造函数传递了一个正确的 ISO 日期格式 YYYY-MM-DD 的字符串,它会自动假定为 UTC。

  1. JavaScript 提供了一个简洁的 Date.UTC() 函数,您可以使用它来获取日期的 UTC 时间戳。 我们从日期中提取组件并将它们传递给函数。
 const userEnteredDate = new Date("12/20/1989"); const userEnteredDateTimeStamp = Date.UTC(userEnteredDate.getFullYear(), userEnteredDate.getMonth(), userEnteredDate.getDate(), 0, 0, 0); const dateFromAPI = new Date("1989-12-20T00:00:00Z"); const result = userEnteredDateTimeStamp == dateFromAPI.getTime(); // true ...

找出两个日期之间的差异

您会遇到的一个常见情况是找出两个日期之间的差异。

我们讨论两个用例:

查找两个日期之间的天数

将两个日期都转换为 UTC 时间戳,以毫秒为单位求出差异并找到等效的天数。

 const dateFromAPI = "2016-02-10T00:00:00Z"; const now = new Date(); const datefromAPITimeStamp = (new Date(dateFromAPI)).getTime(); const nowTimeStamp = now.getTime(); const microSecondsDiff = Math.abs(datefromAPITimeStamp - nowTimeStamp); // Math.round is used instead of Math.floor to account for certain DST cases // Number of milliseconds per day = // 24 hrs/day * 60 minutes/hour * 60 seconds/minute * 1000 ms/second const daysDiff = Math.round(microSecondsDiff / (1000 * 60 * 60 * 24)); console.log(daysDiff);

根据出生日期查找用户的年龄

const birthDateFromAPI = "12/10/1989";

注意:我们有非标准格式。 阅读 API 文档以确定这是否意味着 10 月 12 日或 12 月 10 日。相应地更改为 ISO 格式。

 const parts = birthDateFromAPI.split("/"); const birthDateISO = parts[2] + "-" + parts[0] + "-" + parts[1]; const birthDate = new Date(birthDateISO); const today = new Date(); let age = today.getFullYear() - birthDate.getFullYear(); if(today.getMonth() < birthDate.getMonth()) { age--; } if(today.getMonth() == birthDate.getMonth() && today.getDate() < birthDate.getDate()) { age--; }

我知道有更简洁的方法来编写这段代码,但我喜欢这样写,因为逻辑非常清晰。

避免约会地狱的建议

既然我们已经熟悉了日期算术,我们就可以了解要遵循的最佳实践以及遵循它们的原因。

从用户获取日期时间

如果您从用户那里获取日期和时间,那么您很可能正在寻找他们的本地 DateTime。 我们在日期算术部分看到Date构造函数可以通过多种不同方式接受日期。

为了消除任何混淆,我总是建议使用new Date(year, month, day, hours, minutes, seconds, milliseconds)格式创建日期,即使您已经拥有有效的可解析格式的日期。 如果您团队中的所有程序员都遵循这个简单的规则,那么从长远来看,维护代码将非常容易,因为它与Date构造函数一样明确。

最酷的部分是您可以使用允许您省略任何最后四个参数(如果它们为零)的变体; 即, new Date(2012, 10, 12)new Date(2012, 10, 12, 0, 0, 0, 0)相同,因为未指定的参数默认为零。

例如,如果您使用的日期和时间选择器为您提供日期 2012-10-12 和时间 12:30,您可以提取部分并创建一个新的 Date 对象,如下所示:

 const dateFromPicker = "2012-10-12"; const timeFromPicker = "12:30"; const dateParts = dateFromPicker.split("-"); const timeParts = timeFromPicker.split(":"); const localDate = new Date(dateParts[0], dateParts[1]-1, dateParts[2], timeParts[0], timeParts[1]);

尽量避免从字符串创建日期,除非它是 ISO 日期格式。 请改用 Date(year, month, date, hours, minutes, seconds, microseconds) 方法。

仅获取日期

如果您只获取日期,例如用户的生日,最好将格式转换为有效的 ISO 日期格式,以消除在转换为 UTC 时可能导致日期向前或向后移动的任何时区信息。 例如:

 const dateFromPicker = "12/20/2012"; const dateParts = dateFromPicker.split("/"); const ISODate = dateParts[2] + "-" + dateParts[0] + "-" + dateParts[1]; const birthDate = new Date(ISODate).toISOString();

如果您忘记了,如果您使用有效 ISO 日期格式 (YYYY-MM-DD) 的输入创建Date对象,它将默认为 UTC,而不是默认为浏览器的时区。

存储日期

始终以 UTC 格式存储 DateTime。 始终向后端发送 ISO 日期字符串或时间戳。

几代计算机程序员在尝试向用户显示正确的本地时间的痛苦经历之后,已经意识到了这个简单的事实。 将本地时间存储在后端是一个坏主意,最好让浏览器在前端处理转换为本地时间。

此外,很明显,您永远不应该将“1989 年 7 月 20 日 12:10 PM”之类的 DateTime 字符串发送到后端。 即使您也发送时区,您也在增加其他程序员了解您的意图并正确解析和存储日期的努力。

使用 Date 对象的toISOString()toJSON()方法将本地 DateTime 转换为 UTC。

 const dateFromUI = "12-13-2012"; const timeFromUI = "10:20"; const dateParts = dateFromUI.split("-"); const timeParts = timeFromUI.split(":"); const date = new Date(dateParts[2], dateParts[0]-1, dateParts[1], timeParts[0], timeParts[1]); const dateISO = date.toISOString(); $.post("http://example.com/", {date: dateISO}, ...)

显示日期和时间

  1. 从 REST API 获取时间戳或 ISO 格式的日期。
  2. 创建一个Date对象。
  3. 使用toLocaleString()toLocaleDateString()toLocaleTimeString()方法或日期库来显示本地时间。
 const dateFromAPI = "2016-01-02T12:30:00Z"; const localDate = new Date(dateFromAPI); const localDateString = localDate.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric', }); const localTimeString = localDate.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit', });

什么时候也应该存储当地时间?

“有时了解事件发生的时区很重要,而转换为单一时区会不可逆转地消除该信息。

“如果您正在进行营销推广并想知道哪些客户在午餐时间下订单,那么看起来是在格林威治标准时间中午下的订单实际上是在纽约的早餐时间下订单并没有太大帮助。”

如果遇到这种情况,最好也节省本地时间。 像往常一样,我们想以 ISO 格式创建日期,但我们必须先找到时区偏移量。

Date 对象的getTimeZoneOffset()函数告诉我们添加到给定本地时间后得到等效 UTC 时间的分钟数。 我建议将其转换为 (+-)hh:mm 格式,因为它更明显地表明它是一个时区偏移。

 const now = new Date(); const tz = now.gettime zoneOffset();

对于我的时区 +05:45,我得到 -345,这不仅是相反的符号,而且像 -345 这样的数字可能会让后端开发人员完全感到困惑。 所以我们将其转换为 +05:45。

 const sign = tz > 0 ? "-" : "+"; const hours = pad(Math.floor(Math.abs(tz)/60)); const minutes = pad(Math.abs(tz)%60); const tzOffset = sign + hours + ":" + minutes;

现在我们得到其余的值并创建一个表示本地日期时间的有效 ISO 字符串。

 const localDateTime = now.getFullYear() + "-" + pad(now.getMonth()+1) + "-" + pad(now.getDate()) + "T" + pad(now.getHours()) + ":" + pad(now.getMinutes()) + ":" + pad(now.getSeconds());

如果需要,可以将 UTC 和本地日期包装在一个对象中。

 const eventDate = { utc: now.toISOString(), local: localDateTime, tzOffset: tzOffset, }

现在,在后端,如果您想知道事件是否发生在当地时间中午之前,您可以解析日期并简单地使用getHours()函数。

 const localDateString = eventDate.local; const localDate = new Date(localDateString); if(localDate.getHours() < 12) { console.log("Event happened before noon local time"); }

我们在这里没有使用tzOffset ,但我们仍然存储它,因为我们将来可能需要它来进行调试。 您实际上可以只发送时区偏移量和 UTC 时间。 但我也喜欢存储本地时间,因为您最终必须将日期存储在数据库中,并且单独存储本地时间允许您直接基于字段进行查询,而不必执行计算来获取本地日期。

有时,即使存储了本地时区,您也希望在特定时区显示日期。 例如,如果事件是虚拟的,则事件的时间在当前用户的时区中可能更有意义,如果不是,则在实际发生的时区中可能更有意义。 在任何情况下,都值得事先查看已建立的使用明确时区名称进行格式化的解决方案。

服务器和数据库配置

始终将您的服务器和数据库配置为使用 UTC 时区。 (请注意,UTC 和 GMT 不是一回事——例如,GMT 可能意味着在夏季切换到 BST,而 UTC 永远不会。)

我们已经看到了时区转换的痛苦程度,尤其是当它们是无意的时候。 始终发送 UTC DateTime 并将您的服务器配置为 UTC 时区可以让您的生活更轻松。 您的后端代码将更加简单和清晰,因为它不需要进行任何时区转换。 来自世界各地服务器的 DateTime 数据可以毫不费力地进行比较和分类。

Code in the back end should be able to assume the time zone of the server to be UTC (but should still have a check in place to be sure). A simple configuration check saves having to think about and code for conversions every time new DateTime code is written.

It's Time for Better Date Handling

Date manipulation is a hard problem. The concepts behind the practical examples in this article apply beyond JavaScript, and are just the beginning when it comes to properly handling DateTime data and calculations. Plus, every helper library will come with its own set of nuances—which is even true of the eventual official standard support{target=”_blank”} for these types of operations.

The bottom line is: Use ISO on the back end, and leave the front end to format things properly for the user. Professional programmers will be aware of some of the nuances, and will (all the more decidedly) use well-supported DateTime libraries on both the back end and the front end. Built-in functions on the database side are another story, but hopefully this article gives enough background to make wiser decisions in that context, too.

Related: Buggy JavaScript Code: The 10 Most Common Mistakes JavaScript Developers Make