2022 年 10 月 14 日

日期和時間

讓我們認識一個新的內建物件:Date。它儲存日期、時間,並提供日期/時間管理的方法。

例如,我們可以使用它來儲存建立/修改時間,測量時間,或僅列印出目前的日期。

建立

要建立新的 Date 物件,請使用下列其中一個引數呼叫 new Date()

new Date()

沒有引數 – 為目前的日期和時間建立一個 Date 物件

let now = new Date();
alert( now ); // shows current date/time
new Date(milliseconds)

建立一個 Date 物件,其時間等於自 1970 年 1 月 1 日 UTC+0 之後經過的毫秒數(1/1000 秒)。

// 0 means 01.01.1970 UTC+0
let Jan01_1970 = new Date(0);
alert( Jan01_1970 );

// now add 24 hours, get 02.01.1970 UTC+0
let Jan02_1970 = new Date(24 * 3600 * 1000);
alert( Jan02_1970 );

自 1970 年初以來經過的毫秒數的整數稱為時間戳記

它是日期的輕量級數字表示法。我們可以使用 `new Date(timestamp)` 隨時從時間戳記建立日期,並使用 `date.getTime()` 方法將現有的 `Date` 物件轉換為時間戳記(見下文)。

1970 年 01 月 01 日之前的日期具有負時間戳記,例如

// 31 Dec 1969
let Dec31_1969 = new Date(-24 * 3600 * 1000);
alert( Dec31_1969 );
new Date(datestring)

如果只有一個引數,且為字串,則會自動進行剖析。演算法與 `Date.parse` 使用的相同,我們稍後會介紹。

let date = new Date("2017-01-26");
alert(date);
// The time is not set, so it's assumed to be midnight GMT and
// is adjusted according to the timezone the code is run in
// So the result could be
// Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time)
// or
// Wed Jan 25 2017 16:00:00 GMT-0800 (Pacific Standard Time)
new Date(year, month, date, hours, minutes, seconds, ms)

使用當地時區的指定元件建立日期。只有前兩個引數是必要的。

  • year 應有 4 個數字。為了相容性,也接受 2 個數字並視為 19xx,例如 98 在這裡與 1998 相同,但強烈建議始終使用 4 個數字。
  • month 計數從 0(1 月)開始,到 11(12 月)結束。
  • date 參數實際上是該月份的天數,如果不存在,則假設為 1
  • 如果 hours/minutes/seconds/ms 不存在,則假設它們等於 0

例如

new Date(2011, 0, 1, 0, 0, 0, 0); // 1 Jan 2011, 00:00:00
new Date(2011, 0, 1); // the same, hours etc are 0 by default

最大精度為 1 毫秒(1/1000 秒)

let date = new Date(2011, 0, 1, 2, 3, 4, 567);
alert( date ); // 1.01.2011, 02:03:04.567

存取日期元件

有方法可以從 Date 物件存取年份、月份等

getFullYear()
取得年份(4 個數字)
getMonth()
取得月份,從 0 到 11
getDate()
取得該月份的天數,從 1 到 31,方法名稱看起來有點奇怪。
getHours()getMinutes()getSeconds()getMilliseconds()
取得對應的時間元件。
不是 getYear(),而是 getFullYear()

許多 JavaScript 引擎實作非標準方法 getYear()。此方法已棄用。它有時會傳回 2 位數的年份。請不要使用它。年份有 getFullYear()

此外,我們可以取得星期幾

getDay()
取得星期幾,從 0(星期日)到 6(星期六)。星期日永遠是第一天,在某些國家並非如此,但無法變更。

以上所有方法都傳回相對於當地時區的元件。

還有它們的 UTC 對應版本,會傳回 UTC+0 時區的日期、月份、年份等:getUTCFullYear()getUTCMonth()getUTCDay()。只要在 "get" 後面插入 "UTC" 即可。

如果您的當地時區相對於 UTC 有所偏移,那麼以下程式碼會顯示不同的時間

// current date
let date = new Date();

// the hour in your current time zone
alert( date.getHours() );

// the hour in UTC+0 time zone (London time without daylight savings)
alert( date.getUTCHours() );

除了這些方法之外,還有兩個特殊的方法沒有 UTC 變體

getTime()

傳回日期的時間戳記,即自 1970 年 1 月 1 日 UTC+0 以來經過的毫秒數。

getTimezoneOffset()

傳回 UTC 和當地時區的差,單位為分鐘

// if you are in timezone UTC-1, outputs 60
// if you are in timezone UTC+3, outputs -180
alert( new Date().getTimezoneOffset() );

設定日期元件

以下方法允許設定日期/時間元件

除了 setTime() 之外,每個方法都有 UTC 變體,例如:setUTCHours()

正如我們所見,有些方法可以一次設定多個元件,例如 setHours。未提及的元件不會被修改。

例如

let today = new Date();

today.setHours(0);
alert(today); // still today, but the hour is changed to 0

today.setHours(0, 0, 0, 0);
alert(today); // still today, now 00:00:00 sharp.

自動校正

自動校正Date 物件非常方便的功能。我們可以設定超出範圍的值,它會自動調整自身。

例如

let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!?
alert(date); // ...is 1st Feb 2013!

超出範圍的日期元件會自動分配。

假設我們需要將「2016 年 2 月 28 日」的日期增加 2 天。如果是閏年,它可能是「3 月 2 日」或「3 月 1 日」。我們不必考慮這些。只要加上 2 天即可。Date 物件會處理其餘部分

let date = new Date(2016, 1, 28);
date.setDate(date.getDate() + 2);

alert( date ); // 1 Mar 2016

此功能通常用於取得特定時間段後的日期。例如,讓我們取得「現在之後 70 秒」的日期

let date = new Date();
date.setSeconds(date.getSeconds() + 70);

alert( date ); // shows the correct date

我們也可以設定零或甚至負值。例如

let date = new Date(2016, 0, 2); // 2 Jan 2016

date.setDate(1); // set day 1 of month
alert( date );

date.setDate(0); // min day is 1, so the last day of the previous month is assumed
alert( date ); // 31 Dec 2015

日期轉數字、日期差

Date 物件轉換為數字時,它會變成與 date.getTime() 相同的時間戳記

let date = new Date();
alert(+date); // the number of milliseconds, same as date.getTime()

重要的副作用:可以減去日期,結果是它們的毫秒差。

這可以用於時間測量

let start = new Date(); // start measuring time

// do the job
for (let i = 0; i < 100000; i++) {
  let doSomething = i * i * i;
}

let end = new Date(); // end measuring time

alert( `The loop took ${end - start} ms` );

Date.now()

如果我們只想要測量時間,我們不需要Date物件。

有一個特殊的方法Date.now()會傳回目前的 timestamp。

它的語意等同於new Date().getTime(),但它不會建立一個中間的Date物件。所以它比較快,而且不會對垃圾回收造成壓力。

它大多用於方便性或效能很重要的時候,例如 JavaScript 遊戲或其他特殊應用程式。

所以這或許比較好

let start = Date.now(); // milliseconds count from 1 Jan 1970

// do the job
for (let i = 0; i < 100000; i++) {
  let doSomething = i * i * i;
}

let end = Date.now(); // done

alert( `The loop took ${end - start} ms` ); // subtract numbers, not dates

效能測試

如果我們想要一個可靠的 CPU 密集函式的效能測試,我們應該小心。

例如,我們來測量兩個計算兩個日期之間差值的函式:哪一個比較快?

這種效能測量通常稱為「效能測試」。

// we have date1 and date2, which function faster returns their difference in ms?
function diffSubtract(date1, date2) {
  return date2 - date1;
}

// or
function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

這兩個函式做的事情完全一樣,但其中一個使用明確的date.getTime()取得日期的毫秒數,另一個則依賴日期轉數字的轉換。它們的結果總是相同。

所以,哪一個比較快?

第一個想法可能是連續執行它們很多次,然後測量時間差。對於我們的案例,函式非常簡單,所以我們必須執行至少 100000 次。

讓我們測量

function diffSubtract(date1, date2) {
  return date2 - date1;
}

function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

function bench(f) {
  let date1 = new Date(0);
  let date2 = new Date();

  let start = Date.now();
  for (let i = 0; i < 100000; i++) f(date1, date2);
  return Date.now() - start;
}

alert( 'Time of diffSubtract: ' + bench(diffSubtract) + 'ms' );
alert( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' );

哇!使用getTime()快很多!這是因為沒有類型轉換,引擎比較容易最佳化。

好,我們有了一些東西。但這還不是一個好的效能測試。

想像一下在執行bench(diffSubtract)的時候,CPU 正在平行做一些事情,而且它佔用了資源。而等到執行bench(diffGetTime)的時候,那個工作已經完成了。

對於現代的多處理器作業系統來說,這是相當實際的場景。

結果是,第一個效能測試會比第二個效能測試有更少的 CPU 資源。這可能會導致錯誤的結果。

為了更可靠的效能測試,應該多次重新執行整組效能測試。

例如,像這樣

function diffSubtract(date1, date2) {
  return date2 - date1;
}

function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

function bench(f) {
  let date1 = new Date(0);
  let date2 = new Date();

  let start = Date.now();
  for (let i = 0; i < 100000; i++) f(date1, date2);
  return Date.now() - start;
}

let time1 = 0;
let time2 = 0;

// run bench(diffSubtract) and bench(diffGetTime) each 10 times alternating
for (let i = 0; i < 10; i++) {
  time1 += bench(diffSubtract);
  time2 += bench(diffGetTime);
}

alert( 'Total time for diffSubtract: ' + time1 );
alert( 'Total time for diffGetTime: ' + time2 );

現代的 JavaScript 引擎只會對執行很多次的「熱門程式碼」套用進階最佳化(不需要最佳化很少執行的東西)。所以,在上面的範例中,第一次執行並沒有經過良好的最佳化。我們可能會想要加入熱身執行

// added for "heating up" prior to the main loop
bench(diffSubtract);
bench(diffGetTime);

// now benchmark
for (let i = 0; i < 10; i++) {
  time1 += bench(diffSubtract);
  time2 += bench(diffGetTime);
}
小心執行微效能測試

現代的 JavaScript 引擎執行許多最佳化。它們可能會調整「人工測試」的結果,與「正常使用」相比,特別是當我們測試非常小的東西時,例如運算子如何運作,或內建函式。所以如果你真的想要了解效能,請研究 JavaScript 引擎如何運作。然後你可能根本不需要微效能測試。

可以在 https://mrale.ph 找到關於 V8 的大量文章。

從字串解析 Date.parse

方法 Date.parse(str) 可以從字串讀取日期。

字串格式應為:YYYY-MM-DDTHH:mm:ss.sssZ,其中

  • YYYY-MM-DD – 是日期:年-月-日。
  • 字元 "T" 用作分隔符。
  • HH:mm:ss.sss – 是時間:小時、分鐘、秒和毫秒。
  • 選用的 'Z' 部分表示時區,格式為 +-hh:mm。單一字母 Z 表示 UTC+0。

也可以使用較短的變體,例如 YYYY-MM-DDYYYY-MM 甚至 YYYY

呼叫 Date.parse(str) 會以指定的格式分析字串,並傳回時間戳記(從 1970 年 1 月 1 日 UTC+0 開始的毫秒數)。如果格式無效,則傳回 NaN

例如

let ms = Date.parse('2012-01-26T13:51:50.417-07:00');

alert(ms); // 1327611110417  (timestamp)

我們可以立即從時間戳記建立新的 Date 物件

let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') );

alert(date);

摘要

  • JavaScript 中的日期和時間以 Date 物件表示。我們無法建立「只有日期」或「只有時間」:Date 物件總是同時包含兩者。
  • 月份從 0 開始計算(是的,一月是 0 月)。
  • getDay() 中的星期幾也從 0 開始計算(星期日)。
  • 當設定超出範圍的組成時,Date 會自動修正。這對於加/減天數/月份/小時很有用。
  • 可以減去日期,得到它們的毫秒差。這是因為 Date 在轉換為數字時會變成時間戳記。
  • 使用 Date.now() 快速取得目前的時間戳記。

請注意,與許多其他系統不同,JavaScript 中的時間戳記以毫秒為單位,而不是秒。

有時我們需要更精確的時間測量。JavaScript 本身沒有辦法以微秒(一秒的百萬分之一)為單位測量時間,但大多數環境都提供此功能。例如,瀏覽器具有 performance.now(),它會提供從頁面載入開始的毫秒數,並具有微秒精度(小數點後 3 位數)

alert(`Loading started ${performance.now()}ms ago`);
// Something like: "Loading started 34731.26000000001ms ago"
// .26 is microseconds (260 microseconds)
// more than 3 digits after the decimal point are precision errors, only the first 3 are correct

Node.js 有 microtime 模組和其他方法。技術上來說,幾乎所有裝置和環境都可以獲得更高的精度,只是不在 Date 中。

任務

重要性:5

建立一個 Date 物件,日期為:2012 年 2 月 20 日上午 3:12。時區為當地時區。

使用 alert 顯示日期。

new Date 建構函式使用當地時區。因此,唯一需要記住的重要事項是月份從 0 開始。

因此,2 月的數字為 1。

以下是使用數字作為日期組成的範例

//new Date(year, month, date, hour, minute, second, millisecond)
let d1 = new Date(2012, 1, 20, 3, 12);
alert( d1 );

我們也可以使用字串建立日期,如下所示

//new Date(datastring)
let d2 = new Date("2012-02-20T03:12");
alert( d2 );
重要性:5

撰寫函式 getWeekDay(date),以簡短格式顯示星期:『一』、『二』、『三』、『四』、『五』、『六』、『日』。

例如

let date = new Date(2012, 0, 3);  // 3 Jan 2012
alert( getWeekDay(date) );        // should output "TU"

開啟一個包含測試的沙盒。

方法 date.getDay() 會傳回星期數,從星期日開始。

我們來建立一個星期陣列,以便我們可以透過數字取得正確的星期名稱

function getWeekDay(date) {
  let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];

  return days[date.getDay()];
}

let date = new Date(2014, 0, 3); // 3 Jan 2014
alert( getWeekDay(date) ); // FR

在沙盒中開啟包含測試的解答。

重要性:5

歐洲國家的星期從星期一(數字 1)開始,然後是星期二(數字 2)到星期日(數字 7)。撰寫函式 getLocalDay(date),傳回 date 的「歐洲」星期。

let date = new Date(2012, 0, 3);  // 3 Jan 2012
alert( getLocalDay(date) );       // tuesday, should show 2

開啟一個包含測試的沙盒。

function getLocalDay(date) {

  let day = date.getDay();

  if (day == 0) { // weekday 0 (sunday) is 7 in european
    day = 7;
  }

  return day;
}

在沙盒中開啟包含測試的解答。

重要性:4

建立函式 getDateAgo(date, days),傳回 datedays 天的日期。

例如,如果今天是 20 日,則 getDateAgo(new Date(), 1) 應該是 19 日,而 getDateAgo(new Date(), 2) 應該是 18 日。

對於 days=365 或更多天應可靠運作

let date = new Date(2015, 0, 2);

alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015)
alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014)
alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014)

附註:函式不應修改指定的 date

開啟一個包含測試的沙盒。

概念很簡單:從 date 減去指定的日期數

function getDateAgo(date, days) {
  date.setDate(date.getDate() - days);
  return date.getDate();
}

…但函式不應變更 date。這一點很重要,因為提供我們日期的外部程式碼並未預期會變更日期。

為了實作它,我們來複製日期,如下所示

function getDateAgo(date, days) {
  let dateCopy = new Date(date);

  dateCopy.setDate(date.getDate() - days);
  return dateCopy.getDate();
}

let date = new Date(2015, 0, 2);

alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015)
alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014)
alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014)

在沙盒中開啟包含測試的解答。

重要性:5

撰寫函式 getLastDayOfMonth(year, month),傳回本月的最後一天。有時是 30 日、31 日,甚至 2 月的 28/29 日。

參數

  • year – 四位數年份,例如 2012 年。
  • month – 月份,範圍為 0 到 11。

例如,getLastDayOfMonth(2012, 1) = 29(閏年,2 月)。

開啟一個包含測試的沙盒。

我們來使用下個月建立一個日期,但將天數傳入為 0

function getLastDayOfMonth(year, month) {
  let date = new Date(year, month + 1, 0);
  return date.getDate();
}

alert( getLastDayOfMonth(2012, 0) ); // 31
alert( getLastDayOfMonth(2012, 1) ); // 29
alert( getLastDayOfMonth(2013, 1) ); // 28

通常,日期從 1 開始,但技術上我們可以傳入任何數字,日期會自動調整。因此,當我們傳入 0 時,表示「本月 1 日前一天」,換句話說:「上個月的最後一天」。

在沙盒中開啟包含測試的解答。

重要性:5

撰寫函式 getSecondsToday(),傳回從今天開始的秒數。

例如,如果現在是 上午 10:00,而且沒有夏令時間,則

getSecondsToday() == 36000 // (3600 * 10)

函式應在任何一天都能運作。也就是說,它不應有「今天」的硬式編碼值。

若要取得秒數,我們可以使用當前日期和時間 00:00:00 產生一個日期,然後從「現在」減去它。

差異在於從一天開始的毫秒數,我們應將其除以 1000 以取得秒數

function getSecondsToday() {
  let now = new Date();

  // create an object using the current day/month/year
  let today = new Date(now.getFullYear(), now.getMonth(), now.getDate());

  let diff = now - today; // ms difference
  return Math.round(diff / 1000); // make seconds
}

alert( getSecondsToday() );

另一種解決方案是取得小時/分鐘/秒數,並將其轉換為秒數

function getSecondsToday() {
  let d = new Date();
  return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
}

alert( getSecondsToday() );
重要性:5

建立一個函式 `getSecondsToTomorrow()`,傳回明天還有多少秒數。

例如,如果現在是 `23:00`,則

getSecondsToTomorrow() == 3600

附註:此函式應在任何一天都能運作,「今天」並未硬編碼。

若要取得到明天的毫秒數,我們可以從「明天 00:00:00」減去目前的日期。

首先,我們產生那個「明天」,然後執行

function getSecondsToTomorrow() {
  let now = new Date();

  // tomorrow date
  let tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1);

  let diff = tomorrow - now; // difference in ms
  return Math.round(diff / 1000); // convert to seconds
}

另一種解決方案

function getSecondsToTomorrow() {
  let now = new Date();
  let hour = now.getHours();
  let minutes = now.getMinutes();
  let seconds = now.getSeconds();
  let totalSecondsToday = (hour * 60 + minutes) * 60 + seconds;
  let totalSecondsInADay = 86400;

  return totalSecondsInADay - totalSecondsToday;
}

請注意,許多國家有夏令時間 (DST),因此可能會有 23 或 25 小時的日期。我們可能想要分別處理這些日期。

重要性:4

撰寫一個函式 `formatDate(date)`,應將 `date` 格式化如下

  • 如果自 `date` 傳遞的時間少於 1 秒,則為 `「剛剛」`。
  • 否則,如果自 `date` 傳遞的時間少於 1 分鐘,則為 `「n 秒前」`。
  • 否則,如果少於一小時,則為 `「m 分鐘前」`。
  • 否則,以 `「DD.MM.YY HH:mm」` 格式顯示完整日期。也就是:`「日.月.年 時:分」`,全部以 2 位數格式顯示,例如 `31.12.16 10:00`。

例如

alert( formatDate(new Date(new Date - 1)) ); // "right now"

alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago"

alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago"

// yesterday's date like 31.12.16 20:00
alert( formatDate(new Date(new Date - 86400 * 1000)) );

開啟一個包含測試的沙盒。

若要從 `date` 取得到現在的時間,讓我們減去日期。

function formatDate(date) {
  let diff = new Date() - date; // the difference in milliseconds

  if (diff < 1000) { // less than 1 second
    return 'right now';
  }

  let sec = Math.floor(diff / 1000); // convert diff to seconds

  if (sec < 60) {
    return sec + ' sec. ago';
  }

  let min = Math.floor(diff / 60000); // convert diff to minutes
  if (min < 60) {
    return min + ' min. ago';
  }

  // format the date
  // add leading zeroes to single-digit day/month/hours/minutes
  let d = date;
  d = [
    '0' + d.getDate(),
    '0' + (d.getMonth() + 1),
    '' + d.getFullYear(),
    '0' + d.getHours(),
    '0' + d.getMinutes()
  ].map(component => component.slice(-2)); // take last 2 digits of every component

  // join the components into date
  return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':');
}

alert( formatDate(new Date(new Date - 1)) ); // "right now"

alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago"

alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago"

// yesterday's date like 31.12.2016 20:00
alert( formatDate(new Date(new Date - 86400 * 1000)) );

另一種解決方案

function formatDate(date) {
  let dayOfMonth = date.getDate();
  let month = date.getMonth() + 1;
  let year = date.getFullYear();
  let hour = date.getHours();
  let minutes = date.getMinutes();
  let diffMs = new Date() - date;
  let diffSec = Math.round(diffMs / 1000);
  let diffMin = diffSec / 60;
  let diffHour = diffMin / 60;

  // formatting
  year = year.toString().slice(-2);
  month = month < 10 ? '0' + month : month;
  dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth;
  hour = hour < 10 ? '0' + hour : hour;
  minutes = minutes < 10 ? '0' + minutes : minutes;

  if (diffSec < 1) {
    return 'right now';
  } else if (diffMin < 1) {
    return `${diffSec} sec. ago`
  } else if (diffHour < 1) {
    return `${diffMin} min. ago`
  } else {
    return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}`
  }
}

在沙盒中開啟包含測試的解答。

教學地圖

留言

留言前請先閱讀此內容…
  • 如果您有建議事項 - 請 提交 GitHub 議題 或提交 pull request,而不是留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 若要插入幾行程式碼,請使用 `<code>` 標籤,對於多行內容,請將其包覆在 `<pre>` 標籤中,對於超過 10 行的內容,請使用沙盒 (plnkrjsbincodepen…)