﻿# 前端工程研究：關於 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();** 會產生當下的**本地時間**。

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/ebf2d5c15c2cad2f2a3f-image_2947.png)

第二種 **new Date(value);** 需傳入一個整數值，其數值是從 1970-01-01 00:00:00 UTC ( [UTC = GMT](http://pansci.asia/archives/84978) ) (格林威治標準時間) 到現在的毫秒數 (milliseconds)。

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/1cf455cfa7f7c23a5d4e-image_2948.png)

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

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

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

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

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

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

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

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/84a06fbef21b83da1e93-image_2949.png)

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

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

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

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/b59e02c7b868d2a6cc00-image_2950.png "image")

### new Date(dateString);

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

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

-   #### IE6-8
    
    先別說 JavaScript Date 有多雷了，瀏覽器本身就雷的話，其他都不用說了，請尊重生命，沒事不要用 IE8 上網。  
    連用 ISO 標準格式的 2016-09-24 都不能用 #WTF  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/6ce2fa383e99d551d019-image_2951.png)
-   #### IE9-IE11
    
    這幾個末日 IE 版本真的稍微正常點，我覺得 2016-9-24 這種格式不接受還算有點道理，各位寫 Web API 的人要注意了，不要隨便產生這種格式的日期字串。  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/57838e28ce5e6b916249-image_2952.png)
-   #### Edge
    
    Very Good. 這幾個格式都支援了，大家 Web API 終於可以亂寫了！ (誤)  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/341f2f2c3d81cadb137f-image_2953.png)
-   #### Chrome
    
    看到下圖的執行結果，嘴角總是會微微上揚，不枉費我們這麼愛用 Google Chrome 瀏覽器。  
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/901e84e38c317df21069-image_2954.png "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 的版本！  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/b851ad6bc232c66c75ca-image_2955.png)
-   #### Safari 10 ( macOS / Sierra )
    
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/e96fa15aadd8d20960e7-image_2956.png)

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

-   #### IE6-8
    
    非常好，沒有意外的，不支援的日期格式還是不支援。  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/0805f0336d094006c406-image_2957.png)
-   #### IE9-11
    
    異相又出現了！甚麼？ **2016-09-24 12:21:00** 這種格式你也不接受，你在玩我嗎？ >"<  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/ccafcad43df13c811972-image_2958.png)
-   #### Edge
    
    微軟 Edge 瀏覽器在很多方面的支援度，確實比 IE 好很多！  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/8b752f184f08483e24bb-image_2959.png)
-   #### Chrome
    
    我們加上時間之後，原本第一種範例與第三種範例，不再會相差八小時了，而是通通解譯為「本地時間」。  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/9cb52f824a3ab45dbae3-image_2960.png)
-   #### Safari 9.1.2 ( Mac OS X / El Capitan )
    
    唉~ UCCU~ Safari 是不是真的跟 IE9-11 很像！是不是！>"<  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/c0d934d105de378f70a4-image_2961.png)
-   #### Safari 10 ( macOS / Sierra )
    
    即便到了 Safari 10 最新版，第一種格式不支援就是不支援，脾氣很硬呢！ XD  
    ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/6f522aaa96da01407930-image_2962.png)

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

如果身在台灣，一般人都不習慣用歐美慣用的日期表示法 ( **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](http://www.ecma-international.org/ecma-262/5.1/) 規格中，有對 [Date Time String Format](http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15) 做出了非常明確的定義，意思就是說，要使用 Date.parse() 或 new Date(dateString) 來解析日期格式字串，有一套標準的格式定義，規格中定義了要用簡化版的 [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) 延伸格式。

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

> **YYYY-MM-DDTHH:mm:ss.sss*Z***

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

> **2016\-09\-25T02:24:39.385Z**

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

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

> new Date('2016');  
> new Date('2016-09');  
> new Date('2016-09-25');  
>   
> ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/98232d719f271e3b8862-image_2963.png)

至於時間的部分，日期的部分不能省略，而日期與時間的部分會固定用一個 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');  
>   
> ![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/284b85520785a2fff23d-image_2964.png)

### 結論

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

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

在那個沒有明確定義日期時間字串格式的 ES3 年代 ( IE6-8 )，當時各瀏覽器都只能各自實作自己的日期時間格式解析程式，因此跨瀏覽器之間的相容問題自然會有。但當初畢竟瀏覽器都是美國人做的，所以用美國常見的日期時間格式來解析，肯定沒問題，例如：**Sat, 24 Sep 2016 20:42:16** 、 **Sat, 24 Sep 2016 20:42:16 GMT** 或 **Sat, 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](https://developer.mozilla.org/en-US/docs/Web/JavaScript) 網站，上面對 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**

**相關連結**

-   [Date - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
-   [到底是 GMT+8 還是 UTC+8 ? - PanSci 泛科學](http://pansci.asia/archives/84978)
-   [ECMAScript Language Specification - ECMA-262 Edition 5.1 - 15.9.1.15 Date Time String Format](http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15)
-   [ECMAScript - Wikipedia, the free encyclopedia](https://en.wikipedia.org/wiki/ECMAScript)
-   [javascript - Safari JS cannot parse YYYY-MM-DD date format? - Stack Overflow](http://stackoverflow.com/questions/3085937/safari-js-cannot-parse-yyyy-mm-dd-date-format)
-   [Standard Date and Time Format Strings - MSDN](https://msdn.microsoft.com/en-us/library/az4se3k1.aspx)
