The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

前端工程研究:關於 JavaScript 中 Date 型別的常見地雷與建議作法

今天這篇文章,我想來談談 JavaScript 到底如何實作「日期字串」格式解析,日期時間有太多種表達方式,一般人在剛接觸日期格式的時候,都不會想太多跨瀏覽器的問題,通常都是遇到問題的時才來靠北瀏覽器有多爛多爛。有趣的是,有時候還會怪錯對象,例如 XXXX 框架好爛,我寫 JS 這麼久都沒遇過這問題,用了 XXXX 框架才遇到的。現在,我們就開始來對這個說常用不常用,用到時不熟悉的「日期」型別,話說從頭。

我這幾年遇到 JavaScript 許多靈異事件,除了 number 以外,就屬 Date 最詭異了,每過一段時間就會爆出驚人的意外之喜。XD

基本上我們在建立 Date 物件的時候,只有 4 種用法:

  1. new Date();
  2. new Date(value);
  3. new Date(dateString);
  4. new Date(year, month[, day[, hour[, minutes[, seconds[, milliseconds]]]]]);

這四種用法中,最安全的用法應該就屬第 1 種與第 2 種用法了:

第 1 種 new Date(); 會產生當下的本地時間

 

第二種 new Date(value); 需傳入一個整數值,其數值是從 1970-01-01 00:00:00 UTC ( UTC = GMT ) (格林威治標準時間) 到現在的毫秒數 (milliseconds)。

※ 請注意 ※

你在任何瀏覽器開發者工具的 Console 模式下,只要輸出 Date 物件的結果,一律都會使用「本地時間」來顯示該物件的日期時間。

 

第三種 new Date(dateString); 是我們這篇文章的討論重點,也是最雷的用法。

一般來說,我們比較少會直接用 new Date(dateString) 語法來產生日期物件,如果會直接這樣用,通常都是先從 AJAX 或其他地方讀入一個「日期格式的字串」值,然後才透過 new Date(dateString) 解析字串成為 Date 型別的物件。不過,大部分時候都是讀入資料後直接輸出字串文字,因此大家不常會遇到解析日期的問題。

但若牽涉到日期的相關運算,例如想要計算兩個日期之間的差異天數,或是想知道從特定時間點算起 60 天是幾月幾號,或是想顯示自訂的日期格式 (民國年),都很有可能會用到日期字串格式解析。

這部分的篇幅較長,我打算先介紹完第四種之後再回頭來介紹這種用法。

 

第四種算是非常淺顯易懂的,但這種「看似簡單」的 API 大家都該小心 XD

你直接從下圖看看我如何測試幾種日期用法,你有發現奇妙的地方嗎?

是的,沒錯,我們傳入的第二個參數為「月份」,但是你傳入 9 回傳的卻是 10 月,因為只有這個「月份」的參數是從 0 開始計算的,沒有人知道為什麼!#WTF

所以 0 代表「一月」,而 11 代表「十二月」。

你知道如果傳入 12 代表哪個月份嗎?很神奇的,是「十三月」,也就是今年的年份的 12 月自動加上 1 個月,如果你執行的是 new Date(2016, 12, 1); 的話,你得到的日期物件將會是 2017-01-01 這個日期,你說雷不雷! #WTF_Again

image

 

new Date(dateString);

現在就要進入 Date 型別最雷的用法,就是看他如何解析字串,還有更重要的,就是「你」通常會想傳入甚麼日期格式的字串?

我們先來看看不同瀏覽器如何解析不同的「日期格式字串」:

  • IE6-8

    先別說 JavaScript Date 有多雷了,瀏覽器本身就雷的話,其他都不用說了,請尊重生命,沒事不要用 IE8 上網。
    連用 ISO 標準格式的 2016-09-24 都不能用 #WTF
  • IE9-IE11

    這幾個末日 IE 版本真的稍微正常點,我覺得 2016-9-24 這種格式不接受還算有點道理,各位寫 Web API 的人要注意了,不要隨便產生這種格式的日期字串。
  • Edge

    Very Good. 這幾個格式都支援了,大家 Web API 終於可以亂寫了! (誤)
  • Chrome

    看到下圖的執行結果,嘴角總是會微微上揚,不枉費我們這麼愛用 Google Chrome 瀏覽器。
    image

    不過,請不要高興得太早,魔鬼總是出在細節哩!

    請看一下如下圖 第 1 個範例 ( 2016-09-24 ) 與 第 3 個範例 ( 2016/09/24 ),如果你仔細看會發現,回傳顯示的日期時間,兩者相差了 8 個小時喔!
    這是因為第 1 個範例被 Chrome 解析為 GMT 格林威治標準時間的日期,我們的「本地時間」是 GMT+8 因此顯示時間會自動加上八小時來顯示。
    我們再看第 3 個範例,他顯示的就是「本地時間」的 2016/09/24,這個小差異你必須特別注意,因為打開你網頁的人不見得位於台灣時區,因此顯示的日期有可能不是 2016/9/24 喔,如果你的電腦時區設定在其他地區,也有可能會顯示 2016/9/23 喔!
  • Safari 9.1.2 ( Mac OS X / El Capitan )

    人家說 Safari 就是另一個 IE 不是沒道理,連特性都跟 IE9-11 很像,可惜現在大部分 iOS 9 跑的也是 Safari 9 的版本!
  • Safari 10 ( macOS / Sierra )

 

接著我們再加上「時間」部分的字串,看看各瀏覽器之間如何解析「日期時間」字串格式的執行結果。

  • IE6-8

    非常好,沒有意外的,不支援的日期格式還是不支援。
  • IE9-11

    異相又出現了!甚麼? 2016-09-24 12:21:00 這種格式你也不接受,你在玩我嗎? >"<
  • Edge

    微軟 Edge 瀏覽器在很多方面的支援度,確實比 IE 好很多!
  • Chrome

    我們加上時間之後,原本第一種範例與第三種範例,不再會相差八小時了,而是通通解譯為「本地時間」。
  • Safari 9.1.2 ( Mac OS X / El Capitan )

    唉~ UCCU~ Safari 是不是真的跟 IE9-11 很像!是不是!>"<
  • Safari 10 ( macOS / Sierra )

    即便到了 Safari 10 最新版,第一種格式不支援就是不支援,脾氣很硬呢! XD

 

上述列了這幾種日期格式,應該算是在台灣比較常見的日期格式吧。

如果身在台灣,一般人都不習慣用歐美慣用的日期表示法 ( 9 September, 2016 12:21:00 ) 來產生日期時間的字串,因為從上述測試的狀況看來,這種格式反而是最沒問題的。不過,也不是真的那麼「沒問題」,請各位想想,如果上網的人剛好不在台灣,而你的日期時間代表的是「台灣時區」的時間,這時透過 JavaScript 解析出來的時間,就是錯誤的,因為瀏覽器預設會將這個日期格式的字串解析為「使用者目前瀏覽器的本地時間」,如果你又剛好將日期顯示出來,那麼時間就很有可能出現時差!( 除非你在畫面上很明確地說這個時間就是台灣時區的時間 )

我最常看到的日期格式應該是 2016-09-24 12:21:00 這種,但你可以發現到,從上述測試案例來看,在 IE6-8 以及他的兄弟 Safari 並不支援這種用法,這包含了 iPhone / iPad 內建的 Safari 版本,因此行動版網頁遇到這種日期格式,通通會完蛋!

 

真正萬無一失的日期字串格式

接著就要進入重點了,事實上在 ECMAScript Language Specification - ECMA-262 Edition 5.1 規格中,有對 Date Time String Format 做出了非常明確的定義,意思就是說,要使用 Date.parse() 或 new Date(dateString) 來解析日期格式字串,有一套標準的格式定義,規格中定義了要用簡化版的 ISO 8601 延伸格式。

這個 ISO 8601 Extended Format 格式大概長這樣:

YYYY-MM-DDTHH:mm:ss.sssZ

一個實際的日期時間範例如下 ( 以下是格林威治標準時區的時間 ):

2016-09-25T02:24:39.385Z

事實上,這種標準的日期格式,也可以允許只有日期的部分,而且還不一定要寫完整的日期格式,只有年份也可以當成合法有效的日期格式。

例如以下日期格式都是合法、有效且跨瀏覽器、跨平台的日期字串格式:

new Date('2016');
new Date('2016-09');
new Date('2016-09-25');

至於時間的部分,日期的部分不能省略,而日期與時間的部分會固定用一個 T 來分隔。時間的部分,從「秒」開始也是可以省略的,不一定要完整的時間格式,且時間格式最後的 Z 代表的是 Time zone 的意思,這個字元也是可以省略的。

例如以下時間格式也都是合法的:

THH:mm
THH:mm:ss
THH:mm:ss.sss

如果把日期與時間組合在一起,就有非常多種可能的變化了,以下我舉幾個例子,這些都是合法有效的日期格式字串:

new Date('2016T02:34');
new Date('2016-09T02:34:34');
new Date('2016-09-25T02:34:33');
new Date('2016-09T02:34:33.346');
new Date('2016-09T02:34:33Z');
new Date('2016-09T02:34:33+0800');
new Date('2016-09T02:34:33+08:00');

 

結論

〝JavaScript 啊 JavaScript,枉費我跟你相處這麼久,為什麼你總是讓人搞不懂啊!〞這句話不知道在多少 Web 開發人員心中出現過,我的「前端工程研究」系列,就是試圖解決大家長久以來最搞不懂的地雷。

請記得一件事,解析日期時間字串格式時,在 ES3 規格中根本沒有定義應該的日期時間字串格式,到了 ES5 才有了明確的 ISO-8601 格式規範。

在那個沒有明確定義日期時間字串格式的 ES3 年代 ( IE6-8 ),當時各瀏覽器都只能各自實作自己的日期時間格式解析程式,因此跨瀏覽器之間的相容問題自然會有。但當初畢竟瀏覽器都是美國人做的,所以用美國常見的日期時間格式來解析,肯定沒問題,例如:Sat, 24 Sep 2016 20:42:16Sat, 24 Sep 2016 20:42:16 GMTSat, 24 Sep 2016 20:42:16 GMT+0800 等等。

而到了 ES5 規格問世之後 ( IE9+ ),因為必須向前相容早期的實作,所以只要不是 ISO-8601 的日期格式出現,不同瀏覽器之間,還是會有可能出現不同的解析結果,像是 Safari 對於 2016-09-24 20:42:16 這種格式就一直是不支援的,你也不能說他錯,他們就是遵照 ES5 規範去實作而已,所以大家也不用抱怨了,多一點包容和理解,世界才有可能更加祥和! ^^

如果你的網站還是必須支援 IE8 或以下的瀏覽器的話,因為 IE8 以下的 JavaScript Runtime 並沒有實作 ES5 規格,所以當你的 Web API 回應 ISO-8601 日期格式,舊版 IE 瀏覽器還是無法解析,這個問題還是必須考量進去。

如果你要我給建議,我可以說相容性最高的應該就是 Sat, 24 Sep 2016 20:42:16 GMT 這種日期時間格式,但請注意這種格式的缺點是【如果你沒有加上 GMT 時區,瀏覽器預設就會將這種格式解析為使用者電腦的本地時間】,如果沒注意到時區部分,對於全球性或跨國瀏覽的使用者來說可能會出現錯誤的時間差。否則請一律使用 ISO-8601 標準日期時間格式 ( 2016-09T02:34:33.346Z ),這種格式的優點就是【瀏覽器只會將這種格式解析為 GMT/UTC 標準時區的時間】,這樣反而可以強迫你用具有時區的思維來開發程式,出現時差錯誤的問題也會減少。再者,你也可以考慮不要用 new Date() 方法解析為日期物件,直接用字串或 Regex 進行解析或許更方便、更不容易出錯!

下次大家在抱怨瀏覽器或框架之前,建議先查過 JavaScript 的權威文件,我心目中的 JavaScript 權威文件就是 JavaScript | MDN 網站,上面對 JavaScript 的介紹與說明一直都是最精準也最完整的,大家可以多多利用! ^_^

 

補充說明

以下補充在寫 .NET / C# 的 DateTime 格式時,方便轉換成 ISO-8601 日期時間格式的程式碼範例:

var dt = new DateTime(2016, 9, 25, 17, 57, 43);

Console.WriteLine(dt.ToUniversalTime().ToString("s"));

// 2016-09-25T09:57:43

 

相關連結

留言評論