日期時間操作權威指南

已發表: 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 data coming in from servers across the world can be compared and sorted effortlessly.

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