計算時間差(Date & Time)
簡介
在日常的 Web 開發或 Node.js 應用中,時間差的計算常常是必不可少的功能:從倒數計時、活動截止時間、到兩筆資料的更新間隔,都需要精確地算出兩個時間點之間的長度。
JavaScript 內建的 Date 物件提供了完整的時間資訊,但直接操作時容易踩到時區、毫秒與人類可讀格式之間的落差。因此,掌握 如何正確取得時間戳、轉換單位以及避免常見陷阱,是每位前端或後端開發者的基本功。
本篇文章將從概念說明出發,透過 5 個實用範例 示範如何在 JavaScript 中計算時間差,並提供最佳實踐與真實應用情境,幫助讀者從「會寫」升級到「寫得好、寫得安全」。
核心概念
1. Date 物件與時間戳(timestamp)
new Date()會產生一個代表 本機時間 的Date實例。date.getTime()或date.valueOf()會回傳 自 1970-01-01 00:00:00 UTC 起的毫秒數(Unix timestamp in milliseconds)。- 毫秒 是計算時間差最直接的單位,之後再根據需求換算成秒、分、時、天等。
const now = new Date(); // 2025-11-20T08:30:00.000Z(示意)
const timestamp = now.getTime(); // 1732045800000
2. 取得兩個時間點的毫秒差
只要把兩個 Date 物件的時間戳相減,即可得到 毫秒差。負值代表前者晚於後者。
const start = new Date('2025-11-20T08:00:00');
const end = new Date('2025-11-20T09:15:30');
const diffMs = end - start; // 4530000 ms
3. 單位換算
將毫秒差轉成更直觀的「天、時、分、秒」時,只需要除以對應的常數:
| 單位 | 常數(毫秒) |
|---|---|
| 秒 | 1 000 |
| 分 | 60 000 |
| 時 | 3 600 000 |
| 天 | 86 400 000 |
4. Intl.RelativeTimeFormat(國際化相對時間)
ES2020 起提供的 API,可直接產生 語系化的相對時間字串(如「3 天前」或「in 2 hours」),省去自行拼接的麻煩。
const rtf = new Intl.RelativeTimeFormat('zh-TW', { numeric: 'auto' });
rtf.format(-3, 'day'); // "3 天前"
rtf.format(2, 'hour'); // "2 小時後"
程式碼範例
下面提供 5 個從基礎到進階 的範例,每段都有詳細註解,方便初學者快速上手。
範例 1️⃣:最簡單的毫秒差
// 取得兩個時間點的毫秒差
const start = new Date('2025-11-01T12:00:00');
const end = new Date('2025-11-01T12:05:30');
const diffMs = end - start; // 330000 ms
console.log(`差距:${diffMs} 毫秒`);
重點:直接使用
-運算子即可得到毫秒差,Date物件在算術運算時會自動轉成時間戳。
範例 2️⃣:換算成「天、時、分、秒」的可讀格式
function formatDiff(ms) {
const sec = Math.floor(ms / 1000) % 60;
const min = Math.floor(ms / (1000 * 60)) % 60;
const hour = Math.floor(ms / (1000 * 60 * 60)) % 24;
const day = Math.floor(ms / (1000 * 60 * 60 * 24));
return `${day} 天 ${hour} 小時 ${min} 分 ${sec} 秒`;
}
const start = new Date('2025-10-31T08:00:00');
const end = new Date('2025-11-20T10:45:20');
const diffMs = end - start;
console.log('時間差:', formatDiff(diffMs));
// 輸出:時間差: 20 天 2 小時 45 分 20 秒
技巧:使用
%取餘數可以避免前面的單位「溢出」到下一層。
範例 3️⃣:產生「XX 天前 / XX 天後」的相對時間字串
function relativeTime(from, to = new Date()) {
const diff = to - from; // 正值表示 from 在過去
const rtf = new Intl.RelativeTimeFormat('zh-TW', { numeric: 'auto' });
const seconds = Math.round(diff / 1000);
if (Math.abs(seconds) < 60) return rtf.format(-seconds, 'second');
const minutes = Math.round(seconds / 60);
if (Math.abs(minutes) < 60) return rtf.format(-minutes, 'minute');
const hours = Math.round(minutes / 60);
if (Math.abs(hours) < 24) return rtf.format(-hours, 'hour');
const days = Math.round(hours / 24);
return rtf.format(-days, 'day');
}
// 範例:顯示「3 天前」或「5 小時後」
console.log(relativeTime(new Date('2025-11-17T12:00:00'))); // 3 天前
console.log(relativeTime(new Date('2025-11-20T15:00:00'))); // 5 小時後
說明:利用
Intl.RelativeTimeFormat可以一次解決 語系化 與 單位自動切換 的需求。
範例 4️⃣:跨時區的時間差(使用 UTC)
// 假設使用者在台北(+08:00),伺服器在 UTC
const taipei = new Date('2025-11-20T08:00:00+08:00'); // 2025-11-20T00:00:00Z
const utc = new Date('2025-11-20T02:30:00Z'); // 同一天的 UTC 時間
// 直接相減會得到正確的毫秒差,因為 Date 內部皆以 UTC 儲存
const diffMs = utc - taipei; // 9,000,000 ms = 2.5 小時
console.log(`跨時區差距:${diffMs / 3600000} 小時`);
關鍵:永遠以 UTC 為基礎計算,避免因本機時區或夏令時間(DST)造成誤差。
範例 5️⃣:倒數計時器(利用 setInterval)
function countdown(target) {
const timer = setInterval(() => {
const now = new Date();
const diff = target - now;
if (diff <= 0) {
clearInterval(timer);
console.log('時間到!');
return;
}
const { day, hour, minute, second } = (() => {
const sec = Math.floor(diff / 1000) % 60;
const min = Math.floor(diff / (1000 * 60)) % 60;
const hour = Math.floor(diff / (1000 * 60 * 60)) % 24;
const day = Math.floor(diff / (1000 * 60 * 60 * 24));
return { day, hour, minute: min, second: sec };
})();
console.log(`倒數:${day}d ${hour}h ${minute}m ${second}s`);
}, 1000);
}
// 設定 2025-12-01 00:00:00 為倒數目標
countdown(new Date('2025-12-01T00:00:00'));
實務提醒:在瀏覽器環境中,
setInterval的最小間隔受限於 最小 4ms(在背景分頁可能更長),若需高精度計時,請改用requestAnimationFrame或 Web Workers。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
直接使用 Date.parse() 解析不標準字串 |
Date.parse('2025/11/20') 在不同瀏覽器會有不同結果。 |
使用 ISO 8601 標準格式 YYYY-MM-DDTHH:mm:ss 或透過 new Date(year, month-1, day, ...)。 |
| 忽略時區差異 | 本機時間與伺服器時間不一致時,計算結果會偏差。 | 統一使用 UTC(Date.UTC()、.getTime())或在 API 中傳遞 ISO 8601 帶時區的字串。 |
| 毫秒除以 1000 後直接四捨五入 | 會產生「1 秒 999 毫秒」被算成 2 秒的錯誤。 | 先 取整 (Math.floor) 再進行單位換算,或使用 Math.round 依需求決定。 |
使用 setTimeout/setInterval 產生長時間倒數 |
瀏覽器在非活躍分頁會降頻,導致倒數不準。 | 使用 Web Workers 或 Server‑Side 計算,前端只負責渲染結果。 |
| 忘記處理負值(未來時間) | end - start 為負時,直接顯示負數會不易閱讀。 |
在顯示前先 取絕對值,並根據正負決定文字(如「後」或「前」)。 |
最佳實踐
- 統一時間基準:全專案盡量使用 UTC,前端顯示時再根據使用者時區轉換。
- 封裝工具函式:將時間差、換算、格式化等功能抽成獨立模組,避免重複程式碼。
- 避免硬編碼:把「毫秒/秒/分/時」等常數定義為常量(
const MS_IN_SEC = 1000),提升可讀性與維護性。 - 使用現代 API:
Intl.RelativeTimeFormat、Temporal(Stage 3)等新 API 能減少自行計算的錯誤。 - 單元測試:特別是跨時區或夏令時間的邏輯,務必寫測試確保正確。
實際應用場景
| 場景 | 需求 | 可能的實作 |
|---|---|---|
| 活動倒數 | 顯示活動開始前的天、時、分、秒 | 範例 5 的倒數計時器,配合 requestAnimationFrame 提升流暢度 |
| 訂單處理時間 | 計算訂單從下單到出貨的耗時,顯示在後台報表 | 使用 UTC 時間戳,換算成「X 天 Y 小時」後儲存於資料庫 |
| 聊天訊息時間 | 顯示「3 分鐘前」或「2 小時前」的相對時間 | Intl.RelativeTimeFormat 搭配自訂的 relativeTime 函式 |
| API 請求超時 | 判斷最後一次成功回應與現在的差距,決定是否重試 | 取得 Date.now(),與上一次成功的時間戳比較 |
| 日曆排程衝突檢測 | 判斷兩個會議是否重疊 | 先把開始與結束時間都轉成毫秒,檢查 startA < endB && startB < endA |
總結
- 時間差的核心是把 兩個
Date物件的時間戳相減,得到毫秒值。 - 透過 除以 1000、60、3600、86400 等常數,可輕鬆換算為秒、分、時、天。
- 為了 跨時區、語系化 與 易讀性,建議使用 UTC 作為計算基礎,並善用
Intl.RelativeTimeFormat或未來的TemporalAPI。 - 常見的坑包括 字串解析不一致、時區忽略、負值處理,只要遵守「統一基準、封裝工具、寫測試」的最佳實踐,就能寫出 可靠且易維護 的時間差程式。
掌握以上概念與範例後,你就能在 倒數計時、報表分析、即時聊天 等多種情境中,準確且優雅地處理時間差,為使用者提供更好的體驗。祝開發順利 🎉!