JavaScript Temporal API —— Date API 問(wèn)題的一個(gè)解決方案
原文地址:JavaScript Temporal API- A Fix for the Date API 原文作者:Nathan Sebhastian 譯文出自:掘金翻譯計(jì)劃 本文永久鏈接:https://github.com/xitu/gold-miner/blob/master/article/2021/javascript-temporal-api-a-fix-for-the-date-api.md 譯者:霜羽 Hoarfroster 校對(duì)者:Chorer、Usualminds

JavaScript 的日期處理 API 比較糟糕,因?yàn)樗侵苯訉?duì) Java 的 Date 類(lèi) 進(jìn)行復(fù)制來(lái)實(shí)現(xiàn)了 Date 對(duì)象,而 Java 維護(hù)者最終棄用了許多 Date 類(lèi)的方法,并于 1997 年創(chuàng)建了 Calendar 類(lèi)以取代它。
但是 JavaScript 的 Date API 還沒(méi)有進(jìn)行進(jìn)一步修復(fù),這就是為什么我們今天會(huì)遇到以下問(wèn)題:
Date對(duì)象是可變的用于日期和時(shí)間計(jì)算的混亂 API(例如,天數(shù)的加減) 僅支持 UTC 和本地時(shí)區(qū) 從字符串中解析日期的不可靠 不支持公歷以外的其他歷法
但由于目前 Date API 被廣泛地應(yīng)用于各種庫(kù)和瀏覽器引擎中,我們暫時(shí)不可能修復(fù)其錯(cuò)誤部分。如果我們更改它的底層實(shí)現(xiàn),就會(huì)很可能對(duì)許多現(xiàn)有的網(wǎng)站和庫(kù)造成破壞性影響。
新的 Temporal API 提案旨在解決 Date API 的問(wèn)題,它對(duì) JavaScript 的日期和時(shí)間操作進(jìn)行了以下改進(jìn):
僅創(chuàng)建和處理不可變的 Temporal對(duì)象用于日期和時(shí)間計(jì)算的簡(jiǎn)單 API 支持所有時(shí)區(qū) 遵循 ISO-8601 格式進(jìn)行嚴(yán)格的日期解析 支持非公歷的歷法
請(qǐng)記住,
Temporal提案當(dāng)前處于第二階段,尚未準(zhǔn)備好用于生產(chǎn)環(huán)境中。
讓我們借助代碼示例理解 Temporal API 的功能吧。下文中的所有 Temporal API 代碼都是使用 Temporal Polyfill 創(chuàng)建的。
不可變的日期對(duì)象
使用 JavaScript 的 new Date() 構(gòu)造器創(chuàng)建的 Date 對(duì)象是可變的,意味著你可以在初始化以后修改它的值:
let date = new Date("2021-02-20");
console.log(date); // 2021-02-20T00:00:00.000Z
date.setYear(2000);
console.log(date); // 2000-02-20T00:00:00.000Z
盡管看似無(wú)關(guān)緊要,但這種可變的對(duì)象在處理不當(dāng)時(shí)可能會(huì)導(dǎo)致錯(cuò)誤,其中一種情況就是當(dāng)我們嘗試將天數(shù)添加到當(dāng)前日期時(shí)。
例如,這是一個(gè)將當(dāng)前日期增加一周的功能。由于 setDate 會(huì)修改對(duì)象本身,因此我們會(huì)得到兩個(gè)具有相同日期值的對(duì)象:
function addOneWeek(date) {
date.setDate(date.getDate() + 7);
return date;
}
let today = new Date();
let oneWeekLater = addOneWeek(today);
console.log(today);
console.log(oneWeekLater); // 值和變量 today 一樣
Temporal 提供了不直接修改對(duì)象的方法,進(jìn)而修復(fù)了這個(gè)問(wèn)題,例如下面就是使用 Temporal API 添加一周的例子:
const date = Temporal.now.plainDateISO();
console.log(date); // 2021-02-20
console.log(date.add({days: 7})); // 2021-02-27
console.log(date); // 2021-02-20
如上面的代碼所示,Temporal 為我們提供了 .add() 方法,讓我們能將天、周、月或年添加到當(dāng)前日期對(duì)象中而不會(huì)修改原始值。
用于日期和時(shí)間計(jì)算的 API
前面的 Temporal 示例中我們了解到了 .add() 方法,它能幫助我們對(duì)日期對(duì)象執(zhí)行計(jì)算。我們現(xiàn)在使用的 Date API 僅提供了獲取和設(shè)置日期值的方法,不如 Temporal 來(lái)得簡(jiǎn)單直接。
Temporal 還為我們提供了多個(gè) API 來(lái)計(jì)算日期值。比如說(shuō) until() 方法,它可以計(jì)算 firstDate 和 secondDate 之間的時(shí)間差。
而如果使用 Date API,我們需要手動(dòng)計(jì)算兩個(gè)日期之間的天數(shù),如下所示:
const oneDay = 24 * 60 * 60 * 1000;
const firstDate = new Date(2008, 1, 12);
const secondDate = new Date(2008, 1, 22);
const diffDays = Math.round(Math.abs((firstDate - secondDate) / oneDay));
console.log(diffDays); // 10
如果是 Temporal API,我們可以通過(guò) until() 方法簡(jiǎn)單地計(jì)算 diffDays:
const firstDate = Temporal.PlainDate.from('2008-01-12');
const secondDate = Temporal.PlainDate.from('2008-01-22');
const diffDays = firstDate.until(secondDate).days;
console.log(diffDays); // 10
其他的幫助我們計(jì)算的方法還有:
.subtract()方法,用于減少當(dāng)前日期的天數(shù)、月數(shù)或年數(shù)。.since()方法,用于計(jì)算一個(gè)特定日期迄今為止所經(jīng)歷的天數(shù)、月數(shù)或年數(shù)。.equals()方法,用于比較兩個(gè)日期是否相同。
這些 API 能夠幫助我們?nèi)ネ瓿捎?jì)算,而無(wú)需自己創(chuàng)建解決方案。
支持所有時(shí)區(qū)
當(dāng)前的 Date API 在系統(tǒng)中以 UTC 標(biāo)準(zhǔn)跟蹤時(shí)間,通常會(huì)在計(jì)算機(jī)的時(shí)區(qū)中生成日期對(duì)象,操縱時(shí)區(qū)沒(méi)有簡(jiǎn)單的方法。
我發(fā)現(xiàn)操縱時(shí)區(qū)的一種方式是使用 Date.toLocaleString() 方法,如下所示:
let date = new Date();
let tokyoDate = date.toLocaleString("en-US", {
timeZone: "Asia/Tokyo"
});
let singaporeDate = date.toLocaleString("en-US", {
timeZone: "Asia/Singapore",
});
console.log(tokyoDate); // 2/21/2021, 1:36:46 PM
console.log(singaporeDate); // 2/21/2021, 12:36:46 PM
但是由于此方法返回一個(gè)字符串,因此進(jìn)一步的日期和時(shí)間操作要求我們先將字符串轉(zhuǎn)換回日期。
而 Temporal API 允許我們?cè)谑褂?zonedDateTimeISO() 方法創(chuàng)建日期的時(shí)候去定義時(shí)區(qū)。我們可以使用 .now 對(duì)象去獲取當(dāng)前的日期、時(shí)間:
let tokyoDate = Temporal.now.zonedDateTimeISO('Asia/Tokyo');
let singaporeDate = Temporal.now.zonedDateTimeISO('Asia/Singapore');
console.log(tokyoDate);
// 2021-02-20T13:48:24.435904429+09:00[Asia/Tokyo]
console.log(singaporeDate);
// 2021-02-20T12:48:24.429904404+08:00[Asia/Singapore]
由于返回的值仍然是 Temporal 日期,因此我們可以使用 Temporal 本身的方法進(jìn)一步對(duì)其進(jìn)行操作:
let date = Temporal.now.zonedDateTimeISO('Asia/Tokyo');
let oneWeekLater = date.add({weeks: 1});
console.log(oneWeekLater);
// 2021-02-27T13:48:24.435904429+09:00[Asia/Tokyo]
Temporal API 遵循使用類(lèi)型的約定,其中以 Plain 開(kāi)頭的名稱(chēng)是沒(méi)有時(shí)區(qū)的(.PlainDate、.PlainTime、.PlainDateTime),而 .ZonedDateTime 則相反。
遵循 ISO-8601 標(biāo)準(zhǔn)進(jìn)行嚴(yán)格的日期解析
現(xiàn)有的從字符串解析日期的方式是不可靠的,因?yàn)楫?dāng)我們傳遞 ISO-8601 格式的日期字符串時(shí),返回值將根據(jù)是否傳遞了時(shí)區(qū)偏移量而有所不同。
考慮以下示例:
new Date("2021-02-20").toISOString();
// 2021-02-20T00:00:00.000Z
new Date("2021-02-20T05:30").toISOString();
// 2021-02-20T10:30:00.000Z
上面的第一個(gè) Date 構(gòu)造器將字符串視為 UTC+0 時(shí)區(qū),而第二個(gè)構(gòu)造器將字符串視為 UTC-5 時(shí)區(qū)(我當(dāng)前所在的時(shí)區(qū)),因此返回值會(huì)被調(diào)整到 UTC+0 時(shí)區(qū)**(5:30 UTC-5 相當(dāng)于 10:30 UTC+0)**。
Temposal 提案通過(guò)區(qū)分 PlainDateTime 和 ZonedDateTime 來(lái)解決此問(wèn)題,如下所示:

當(dāng)我們想要使日期成為包含時(shí)區(qū)的對(duì)象時(shí),我們需要使用 ZonedDateTime 對(duì)象,反之則使用 PlainDateTime 對(duì)象。
通過(guò)分開(kāi)創(chuàng)建包含時(shí)區(qū)和不包含時(shí)區(qū)的日期,Temporal API 可幫助我們從提供的字符串中解析正確的日期、時(shí)間組合:
Temporal.PlainDateTime.from("2021-02-20");
// 2021-02-20T00:00:00
Temporal.PlainDateTime.from("2021-02-20T05:30");
// 2021-02-20T05:30:00
Temporal.ZonedDateTime.from("2021-02-20T05:30[Asia/Tokyo]");
// 2021-02-20T05:30:00+09:00[Asia/Tokyo]
從上面的示例中可以看到,Temporal API 不會(huì)對(duì)你所在的時(shí)區(qū)進(jìn)行預(yù)設(shè)。
支持公歷以外的歷法
盡管公歷是世界上使用最廣泛的日歷系統(tǒng),但有時(shí)我們可能需要使用其他日歷系統(tǒng)以查看具有文化或宗教意義的特殊日期。
Temporal API 允許我們指定要用于日期、時(shí)間計(jì)算的日歷系統(tǒng)。
日歷的 NPM Polyfill 實(shí)現(xiàn)尚未完成,因此我們需要嘗試使用 Browser Polyfill 中的 withCalendar() 方法。請(qǐng)?jiān)L問(wèn) Temporal 文檔頁(yè)面,然后將以下代碼粘貼到瀏覽器的控制臺(tái)中:
Temporal.PlainDate.from("2021-02-06").withCalendar("gregory").day;
// 6
Temporal.PlainDate.from("2021-02-06").withCalendar("chinese").day;
// 25
Temporal.PlainDate.from("2021-02-06").withCalendar("japanese").day;
// 6
Temporal.PlainDate.from("2021-02-06").withCalendar("hebrew").day;
// 24
Temporal.PlainDate.from("2021-02-06").withCalendar("islamic").day;
// 24
一旦提案通過(guò),Intl.DateTimeFormat 中所有可能的日歷值都將被實(shí)現(xiàn)。
結(jié)論
Temporal API 是針對(duì) JavaScript 的一項(xiàng)新提案,有望為該語(yǔ)言提供現(xiàn)代化的日期和時(shí)間 API。而根據(jù)我基于 Polyfill 的測(cè)試,該 API 確實(shí)提供了更簡(jiǎn)單的日期和時(shí)間操作,同時(shí)也考慮到了時(shí)區(qū)和日歷的差異。
該提案本身仍處于第二階段,因此,如果你有興趣了解更多信息并提供反饋,你可以訪問(wèn) Temporal 文檔 并嘗試其提供的 Polyfill NPM 包。
如果發(fā)現(xiàn)譯文存在錯(cuò)誤或其他需要改進(jìn)的地方,歡迎到 掘金翻譯計(jì)劃 對(duì)譯文進(jìn)行修改并 PR,也可獲得相應(yīng)獎(jiǎng)勵(lì)積分。文章開(kāi)頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。
掘金翻譯計(jì)劃 是一個(gè)翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū),文章來(lái)源為 掘金 上的英文分享文章。內(nèi)容覆蓋 Android、iOS、前端、后端、區(qū)塊鏈、產(chǎn)品、設(shè)計(jì)、人工智能等領(lǐng)域,想要查看更多優(yōu)質(zhì)譯文請(qǐng)持續(xù)關(guān)注 掘金翻譯計(jì)劃、官方微博、知乎專(zhuān)欄。
最后
如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「huab119」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

