JavaScript 日期差一天?用 Intl.DateTimeFormat 分開語系與時區
API 回傳 2026-10-19T16:30:00Z,台灣頁面卻顯示 10 月 19 日。若你用 toISOString().slice(0, 10) 取得日期,拿到的是 UTC 日期;同一個時間點在台北已經是 10 月 20 日。
zh-TW 選擇文字格式,Asia/Taipei 才選擇時區。 想讓伺服器與瀏覽器按照相同地區顯示時間,兩端都要明確指定時區,而不是依賴各自的系統預設。
先問輸入代表什麼
這三種資料不該走同一條轉換路徑:
| 輸入 | 資料意義 | 處理方向 |
|---|---|---|
2026-10-19T16:30:00Z | 已確定的時間點 | 轉成 Date,再依顯示時區格式化 |
2026-10-20 | 生日、帳單日等純日期 | 保留日期語意,不任意套用顯示時區 |
2026-10-20T09:00:00 | 未附時區的日期時間 | 先決定它屬於哪個地區,再轉成時間點 |
Date.parse 的規則區分只有日期與沒有 offset 的日期時間:標準日期字串按 UTC 解讀,沒有時區的日期時間則依執行環境的本地時區。伺服器和使用者電腦的設定不同,就可能在解析階段已經得到不同時間點。
因此,API 若要交換確定的事件時間,可約定傳帶 Z/offset 的標準格式或 timestamp;純日期欄位則留在日期型別的契約裡。不要為了統一顯示,替所有生日資料補上午夜 Z。
顯示給台灣讀者時明確指定 timeZone
Intl.DateTimeFormat會按照選項格式化既有時間點,不會改變 Date 內的 timestamp:
const instant = new Date('2026-10-19T16:30:00Z');const formatter = new Intl.DateTimeFormat('zh-TW', { timeZone: 'Asia/Taipei', dateStyle: 'medium', timeStyle: 'short',});console.log(formatter.format(instant));console.log(instant.toISOString()); // 仍是 2026-10-19T16:30:00.000ZSSR 也要用相同選項。如果伺服器用 UTC 預設、瀏覽器用使用者時區,兩端就可能顯示不同日。需要個人化時區時,先決定由伺服器取得使用者設定,或等瀏覽器接手後再顯示;不要靠隱藏 hydration 警告解決資料契約差異。
日期鍵不要拆本地化顯示字串
畫面可能使用斜線、空白或在地化數字。若需求是依「台灣當天」分組,可用 formatToParts() 按欄位取值,避免假設 format() 的字串永遠是固定順序。
下面只接受已建立且有效的 Date;上游字串格式、真實日曆日期的嚴格驗證要在輸入邊界另外處理。範例以西元四位數年份的業務資料為範圍。
function dateKeyAt(instant, timeZone) { if (!(instant instanceof Date) || !Number.isFinite(instant.getTime())) { throw new TypeError('需要有效的 Date 時間點'); } const formatter = new Intl.DateTimeFormat('en', { timeZone, calendar: 'gregory', numberingSystem: 'latn', year: 'numeric', month: '2-digit', day: '2-digit', }); const parts = Object.fromEntries( formatter.formatToParts(instant) .filter(({ type }) => ['year', 'month', 'day'].includes(type)) .map(({ type, value }) => [type, value]), ); return `${parts.year}-${parts.month}-${parts.day}`;}
const instant = new Date('2026-10-19T16:30:00Z');if (dateKeyAt(instant, 'Asia/Taipei') !== '2026-10-20') { throw new Error('台北日期錯誤');}if (dateKeyAt(instant, 'UTC') !== '2026-10-19') { throw new Error('UTC 日期錯誤');}calendar 與 numberingSystem 讓這個資料鍵明確使用西元與拉丁數字;它和中文畫面的顯示格式可以分開。若使用不同 iframe realm 的 Date 或 Temporal 型別,要先在邊界正規化,這個簡化 helper 沒有涵蓋那些型別。
夏令時間不能固定加減幾小時
台灣資料可能只顯示台北時間,但跨國使用者會遇到夏令時間。以下兩個 UTC 時間點不同,卻都落在紐約當地的凌晨 1<30>30>;要用時區名稱補充顯示,不能只保存「1<30>30>」當作唯一時間:
const formatter = new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', hour: '2-digit', minute: '2-digit', hourCycle: 'h23', timeZoneName: 'shortOffset',});const a = new Date('2026-11-01T05:30:00Z');const b = new Date('2026-11-01T06:30:00Z');console.log(formatter.format(a)); // 此環境:01:30 GMT-4console.log(formatter.format(b)); // 此環境:01:30 GMT-5if (a.getTime() === b.getTime()) throw new Error('應為不同時間點');這是針對時區資料庫規則的示例,並非宣稱未來已發生某個事件。未來排程若定義成「當地每天 9 點」,還要保存地區時區與排程規則;Intl 負責格式化,不會替你排程或把模糊的牆上時間反解成唯一 timestamp。
本文程式區塊於 2026-10-20 在 Node.js 24.13.0 執行,日期鍵另在 TZ=UTC、TZ=Asia/Taipei、TZ=America/Los_Angeles 驗證。這證明明確時區不依賴上述預設值,不代表所有瀏覽器的標點、縮寫與時區資料庫版本完全相同。測試宜比較 formatToParts() 的欄位與 timestamp,不把整段在地化輸出當成固定 API 格式。
需要產生行事曆事件時,可接著檢查日曆連結的開始/結束時間與 TZID;日期顯示正確,不代表匯出的事件時區也正確。
參考資料: