讓我們來學習壹個新的內建對象:日期(Date)。該對象存儲日期和時間,並提供了日期/時間的管理方法。
我們可以使用它來存儲創建/修改時間,測量時間,或者僅用來打印當前時間。
調用 new Date()
來創建壹個新的 Date
對象。在調用時可以帶有壹些參數,如下所示:
new Date()
不帶參數 —— 創建壹個表示當前日期和時間的 Date
對象:
let now = new Date(); alert( now ); // 顯示當前的日期/時間
new Date(milliseconds)
創建壹個 Date
對象,其時間等于 1970 年 1 月 1 日 UTC+0 之後經過的毫秒數(1/1000 秒)。
// 0 表示 01.01.1970 UTC+0 let Jan01_1970 = new Date(0); alert( Jan01_1970 ); // 現在增加 24 小時,得到 02.01.1970 UTC+0 let Jan02_1970 = new Date(24 * 3600 * 1000); alert( Jan02_1970 );
傳入的整數參數代表的是自 1970-01-01 00:00:00 以來經過的毫秒數,該整數被稱爲 時間戳。
這是壹種日期的輕量級數字表示形式。我們通常使用 new Date(timestamp)
通過時間戳來創建日期,並可以使用 date.getTime()
將現有的 Date
對象轉化爲時間戳(下文會講到)。
在 01.01.1970 之前的日期帶有負的時間戳,例如:
// 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); // 未指定具體時間,所以假定時間爲格林尼治標准時間(GMT)的午夜零點 // 並根據運行代碼時的用戶的時區進行調整 // 因此,結果可能是 // Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time) // 或 // Wed Jan 25 2017 16:00:00 GMT-0800 (Pacific Standard Time)
new Date(year, month, date, hours, minutes, seconds, ms)
使用當前時區中的給定組件創建日期。只有前兩個參數是必須的。
例如:
new Date(2011, 0, 1, 0, 0, 0, 0); // 1 Jan 2011, 00:00:00 new Date(2011, 0, 1); // 同樣,時分秒等均爲默認值 0
時間度量最大精確到 1 毫秒(1/1000 秒):
let date = new Date(2011, 0, 1, 2, 3, 4, 567); alert( date ); // 1.01.2011, 02:03:04.567
year
應該是四位數。爲了兼容性,也接受 2 位數,並將其視爲 19xx
,例如 98
與 1998
相同,但強烈建議始終使用 4 位數。
month
計數從 0
(壹月)開始,到 11
(十二月)結束。
date
是當月的具體某壹天,如果缺失,則爲默認值 1
。
如果 hours/minutes/seconds/ms
缺失,則均爲默認值 0
。
從 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 有偏移,那麽下面代碼會顯示不同的小時數:
// 當前日期 let date = new Date(); // 當地時區的小時數 alert( date.getHours() ); // 在 UTC+0 時區的小時數(非夏令時的倫敦時間) alert( date.getUTCHours() );
除了上述給定的方法,還有兩個沒有 UTC 變體的特殊方法:
getTime()
返回日期的時間戳 —— 從 1970-1-1 00:00:00 UTC+0 開始到現在所經過的毫秒數。
getTimezoneOffset()
返回 UTC 與本地時區之間的時差,以分鍾爲單位:
// 如果妳在時區 UTC-1,輸出 60 // 如果妳在時區 UTC+3,輸出 -180 alert( new Date().getTimezoneOffset() );
下列方法可以設置日期/時間組件:
setFullYear(year, [month], [date])
setMonth(month, [date])
setDate(date)
setHours(hour, [min], [sec], [ms])
setMinutes(min, [sec], [ms])
setSeconds(sec, [ms])
setMilliseconds(ms)
setTime(milliseconds)
(使用自 1970-01-01 00:00:00 UTC+0 以來的毫秒數來設置整個日期)
以上方法除了 setTime()
都有 UTC 變體,例如:setUTCHours()
。
我們可以看到,有些方法可以壹次性設置多個組件,比如 setHours
。未提及的組件不會被修改。
舉個例子:
let today = new Date(); today.setHours(0); alert(today); // 日期依然是今天,但是小時數被改爲了 0 today.setHours(0, 0, 0, 0); alert(today); // 日期依然是今天,時間爲 00:00:00。
自動校准 是 Date
對象的壹個非常方便的特性。我們可以設置超範圍的數值,它會自動校准。
舉個例子:
let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!? alert(date); // ……是 1st Feb 2013!
超出範圍的日期組件將會被自動分配。
假設我們要在日期 “28 Feb 2016” 上加 2 天。結果可能是 “2 Mar” 或 “1 Mar”,因爲存在閏年。但是我們不需要考慮這些,只需要直接加 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 ); // 顯示正確的日期信息
我們還可以設置 0 甚至可以設置負值。例如:
let date = new Date(2016, 0, 2); // 2016 年 1 月 2 日 date.setDate(1); // 設置爲當月的第壹天 alert( date ); date.setDate(0); // 天數最小可以設置爲 1,所以這裏設置的是上壹月的最後壹天 alert( date ); // 31 Dec 2015
當 Date
對象被轉化爲數字時,得到的是對應的時間戳,與使用 date.getTime()
的結果相同:
let date = new Date(); alert(+date); // 以毫秒爲單位的數值,與使用 date.getTime() 的結果相同
有壹個重要的副作用:日期可以相減,相減的結果是以毫秒爲單位時間差。
這個作用可以用于時間測量:
let start = new Date(); // 開始測量時間 // do the job for (let i = 0; i < 100000; i++) { let doSomething = i * i * i; } let end = new Date(); // 結束測量時間 alert( `The loop took ${end - start} ms` );
如果我們僅僅想要測量時間間隔,我們不需要 Date
對象。
有壹個特殊的方法 Date.now()
,它會返回當前的時間戳。
它相當于 new Date().getTime()
,但它不會創建中間的 Date
對象。因此它更快,而且不會對垃圾回收造成額外的壓力。
這種方法很多時候因爲方便,又或是因性能方面的考慮而被采用,例如使用 JavaScript 編寫遊戲或其他的特殊應用場景。
因此這樣做可能會更好:
let start = Date.now(); // 從 1 Jan 1970 至今的時間戳 // do the job for (let i = 0; i < 100000; i++) { let doSomething = i * i * i; } let end = Date.now(); // 完成 alert( `The loop took ${end - start} ms` ); // 相減的是時間戳,而不是日期
在對壹個很耗 CPU 性能的函數進行可靠的基准測試(Benchmarking)時,我們需要謹慎壹點。
例如,我們想判斷以下兩個計算日期差值的函數:哪個更快?
這種性能測量通常稱爲“基准測試(benchmark)”。
// 我們有 date1 和 date2,哪個函數會更快地返回兩者的時間差? 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; // 交替運行 bench(diffSubtract) 和 bench(diffGetTime) 各 10 次 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 引擎的先進優化策略只對執行很多次的 “hot code” 有效(對于執行很少次數的代碼沒有必要優化)。因此,在上面的例子中,第壹次執行的優化程度不高。我們可能需要增加壹個預熱步驟:
// 在主循環中增加預熱環節 bench(diffSubtract); bench(diffGetTime); // 開始度量 for (let i = 0; i < 10; i++) { time1 += bench(diffSubtract); time2 += bench(diffGetTime); }
進行微型基准測試時要小心
現代的 JavaScript 引擎執行了很多優化。與正常編寫的代碼相比,它們可能會改變“人爲編寫的專用于測試的代碼”的執行流程,特別是在我們對很小的代碼片段進行基准測試時,例如某個運算符或內建函數的工作方式。因此,爲了深入理解性能問題,請學習 JavaScript 引擎的工作原理。在那之後,妳或許再也不需要進行微型基准測試了。
http://mrale.ph 提供了很多 V8 引擎相關的文章。
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-DD
或 YYYY-MM
,甚至可以是 YYYY
。
Date.parse(str)
調用會解析給定格式的字符串,並返回時間戳(自 1970-01-01 00:00:00 起所經過的毫秒數)。如果給定字符串的格式不正確,則返回 NaN
。
舉個例子:
let ms = Date.parse('2012-01-26T13:51:50.417-07:00'); alert(ms); // 1327611110417 (時間戳)
我們可以通過時間戳來立即創建壹個 new Date
對象:
let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') ); alert(date);
在 JavaScript 中,日期和時間使用 Date 對象來表示。我們不能單獨創建日期或時間,Date
對象總是同時創建兩者。
月份從 0 開始計數(對,壹月是 0)。
壹周中的某壹天 getDay()
同樣從 0 開始計算(0 代表星期日)。
當設置了超出範圍的組件時,Date
會進行自動校准。這壹點對于日/月/小時的加減很有用。
日期可以相減,得到的是以毫秒表示的兩者的差值。因爲當 Date
被轉換爲數字時,Date
對象會被轉換爲時間戳。
使用 Date.now()
可以更快地獲取當前時間的時間戳。
和其他系統不同,JavaScript 中時間戳以毫秒爲單位,而不是秒。
有時我們需要更加精准的時間度量。JavaScript 自身並沒有測量微秒的方法(百萬分之壹秒),但大多數運行環境會提供。例如:浏覽器有 performance.now() 方法來給出從頁面加載開始的以毫秒爲單位的微秒數(精確到毫秒的小數點後三位):
alert(`Loading started ${performance.now()}ms ago`); // 類似于 "Loading started 34731.26000000001ms ago" // .26 表示的是微秒(260 微秒) // 小數點後超過 3 位的數字是精度錯誤,只有前三位數字是正確的
Node.js 可以通過 microtime
模塊或使用其他方法。從技術上講,幾乎所有的設備和環境都允許獲取更高精度的時間數值,只不過不是使用 Date
對象。
重要程度: 5
創建壹個 Date
對象,日期是:Feb 20, 2012, 3:12am。時區是當地時區。
使用 alert
顯示結果。
new Date
構造函數默認使用本地時區。所以唯壹需要牢記的就是月份從 0 開始計數。
所以二月對應的數值是 1。
這是壹個以數字作爲日期參數的示例:
// new Date(year, month, date, hour, minute, second, millisecond) let d1 = new Date(2012, 1, 20, 3, 12); alert( d1 );
我們還可以從字符串創建日期,像這樣:
// new Date(datestring) let d2 = new Date("2012-02-20T03:12"); alert( d2 );
重要程度: 5
編寫壹個函數 getWeekDay(date)
以短格式來顯示壹個日期的星期數:‘MO’,‘TU’,‘WE’,‘TH’,‘FR’,‘SA’,‘SU’。
例如:
let date = new Date(2012, 0, 3); // 3 Jan 2012 alert( getWeekDay(date) ); // 應該輸出 "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)
,並返回日期的歐洲式星期數。
let date = new Date(2012, 0, 3); // 3 Jan 2012 alert( getLocalDay(date) ); // 星期二,應該顯示 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)
,返回特定日期 date
往前 days
天是哪個月的哪壹天。
例如,假設今天是 20 號,那麽 getDateAgo(new Date(), 1)
的結果應該是 19 號,getDateAgo(new Date(), 2)
的結果應該是 18 號。
跨月、年也應該是正確輸出:
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)
P.S. 函數不應該修改給定的 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)
返回 month 月的最後壹天。有時候是 30,有時是 31,甚至在二月的時候會是 28/29。
參數:
year
—— 四位數的年份,比如 2012。
month
—— 月份,從 0 到 11。
舉個例子,getLastDayOfMonth(2012, 1) = 29
(閏年,二月)
打開帶有測試的沙箱。
讓我們使用下個月創建日期,但將零作爲天數(day)傳遞:
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 時,它的意思是“壹個月的第壹天的前壹天”,換句話說:“上個月的最後壹天”。
使用沙箱的測試功能打開解決方案。
重要程度: 5
寫壹個函數 getSecondsToday()
,返回今天已經過去了多少秒?
例如:如果現在是 10:00 am
,並且沒有夏令時轉換,那麽:
getSecondsToday() == 36000 // (3600 * 10)
該函數應該在任意壹天都能正確運行。那意味著,它不應具有“今天”的硬編碼值。
爲獲取秒數,我們可以使用今天的日期和 00:00:00 這個時間創建壹個日期,然後使用當前時間減去該時間。
不同之處在于,從今天之初開始算起的時間是以毫秒計算的,我們應該將其除以 1000,進而得到秒數:
function getSecondsToday() { let now = new Date(); // 使用當前的 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() );
另壹種解決方法是獲取 hours/minutes/seconds,然後把它們轉換爲秒數:
function getSecondsToday() { let d = new Date(); return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); }; alert( getSecondsToday() );
重要程度: 5
寫壹個函數 getSecondsToTomorrow()
,返回距離明天的秒數。
例如,現在是 23:00
,那麽:
getSecondsToTomorrow() == 3600
P.S. 該函數應該在任意壹天都能正確運行。那意味著,它不應具有“今天”的硬編碼值。
爲獲取距離明天的毫秒數,我們可以用“明天 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 秒,輸出 "right now"
。
否則,如果 date
距離現在不到 1 分鍾,輸出 "n sec. ago"
。
否則,如果不到 1 小時,輸出 "m min. ago"
。
否則,以 "DD.MM.YY HH:mm"
格式輸出完整日期。即:"day.month.year hours:minutes"
,全部以兩位數格式表示,例如: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" // 昨天的日期,例如 31.12.16 20:00 alert( formatDate(new Date(new Date - 86400 * 1000)) );
打開帶有測試的沙箱。
爲了獲取 date
距離當前時間的間隔 —— 我們將兩個日期相減。
function formatDate(date) { let diff = new Date() - date; // 以毫秒表示的差值 if (diff < 1000) { // 少于 1 秒 return 'right now'; } let sec = Math.floor(diff / 1000); // 將 diff 轉換爲秒 if (sec < 60) { return sec + ' sec. ago'; } let min = Math.floor(diff / 60000); // 將 diff 轉換爲分鍾 if (min < 60) { return min + ' min. ago'; } // 格式化 date // 將前置 0 加到壹位數 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)); // 得到每個組件的後兩位 // 將時間信息和日期組合在壹起 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" // 昨天的日期如: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; // 格式化 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}` } }
使用沙箱的測試功能打開解決方案。