การจัดรูปแบบและแปลงเวลาใน JavaScript: กับดักที่พบบ่อย
เรียนรู้วิธีจัดรูปแบบและแปลงเวลาใน JavaScript อย่างมั่นใจ: timestamps, สตริง ISO, โซนเวลา, DST, กฎการแยกวิเคราะห์ และรูปแบบที่เชื่อถือได้

ปัญหาที่มักเกิดกับการจัดการเวลาใน JavaScript
บั๊กเกี่ยวกับเวลาใน JavaScript มักจะไม่ได้มาเป็น “นาฬิกาผิดไป” ชัดเจน แต่เป็นการเลื่อนเล็กๆ ที่สับสน: วันที่ถูกต้องบนเครื่องของคุณแต่ผิดบนเครื่องเพื่อนร่วมงาน, การตอบกลับจาก API ที่ดูถูกต้องจนกระทั่งแสดงผลในโซนเวลาอื่น, หรือรายงานที่ “คลาดไปหนึ่ง” รอบการเปลี่ยนแปลงฤดู.
อาการที่พบบ่อยที่สุด
โดยทั่วไปคุณจะสังเกตเห็นหนึ่งในสิ่งเหล่านี้:
- เลื่อนหนึ่งชั่วโมง: โดยเฉพาะรอบ Daylight Saving Time หรือเมื่อค่าถูกแปลงระหว่างเวลาในเครื่องกับ UTC โดยไม่ได้ตั้งใจ
- เลื่อนหนึ่งวัน: ค่าที่เป็นเฉพาะวันที่ (เช่น “2025-12-23”) ปรากฏเป็นวันก่อน/ถัดไปขึ้นอยู่กับโซนเวลา
- โซนเวลาไม่ตรง: เวลาเองดูถูกต้อง แต่ offset (เช่น
+02:00) แตกต่างจากที่คาดหวัง - การจัดรูปแบบไม่สอดคล้อง: “ทำงานใน Chrome” แต่ดูต่างใน Safari หรือ server กับ browser แตกต่างกันในการแยกวิเคราะห์สตริง
ทำไมเรื่องนี้ถึงเกิด: “เวลา” อาจหมายถึงหลายอย่าง
ปัญหาใหญ่คือคำว่า เวลา อาจหมายถึงแนวคิดที่ต่างกันหลายอย่าง:
- ช่วงเวลา (instant): โมเมนต์เฉพาะระดับโลก (เช่น “2025-12-23T10:00:00Z”) ซึ่งมักต้องการสำหรับการล็อก เหตุการณ์ และการเก็บข้อมูล API
- วันที่ตามปฏิทิน: วันบนปฏิทินโดยไม่มีโซนเวลา (เช่น วันเกิด วันกำหนดชำระ) การปฏิบัติต่อมันเหมือนช่วงเวลาอาจทำให้วันที่เลื่อนไปข้ามวัน
- เวลาตามนาฬิกา (wall-clock time): “9:00 AM ในเบอร์ลิน” ซึ่งขึ้นกับกฎโซนเวลาและการเปลี่ยนแปลง DST
Date ใน JavaScript พยายามรองรับทั้งหมดนี้ แต่โดยหลักแล้วมันแทนค่า instant ขณะที่มักจะย้ายคุณไปสู่การแสดงผลแบบ local ทำให้การแปลงโดยไม่ตั้งใจเกิดขึ้นได้ง่าย
บทความนี้เน้นอะไร
แนวทางนี้เน้นการใช้งานจริง: วิธีได้การแปลงที่ คาดการณ์ได้ข้ามเบราว์เซอร์และเซิร์ฟเวอร์, วิธีเลือกฟอร์แมตที่ปลอดภัยกว่า (เช่น ISO 8601), และวิธีสังเกตกับดักคลาสสิก (วินาที vs มิลลิวินาที, UTC vs local, และความแตกต่างในการแยกวิเคราะห์) เป้าหมายไม่ใช่ทฤษฎีเพิ่มเติม—แต่เพื่อลดความประหลาดใจแบบ “ทำไมมันเลื่อน?”
ประเภทข้อมูลเวลา: Timestamp, Date และสตริง
บั๊กเกี่ยวกับเวลามักเริ่มจากการผสมการแทนค่าที่ดูเหมือนจะเทียบเท่ากันแต่ไม่ได้
การแทนค่าที่คุณจะเจอบ่อยที่สุดสามแบบ
1) Epoch milliseconds (ตัวเลข)
ตัวเลขอย่าง 1735689600000 โดยปกติคือ “มิลลิวินาทีตั้งแต่ 1970-01-01T00:00:00Z” มันแทน instant โดยไม่มีการจัดรูปแบบหรือโซนเวลาแนบ
2) วัตถุ Date (ห่อรอบ instant)
Date เก็บ instant เหมือนกับ timestamp ส่วนที่สับสนคือเมื่อคุณ พิมพ์ Date JavaScript จะจัดรูปแบบโดยใช้กฎท้องถิ่นของสภาพแวดล้อมของคุณ เว้นแต่คุณจะร้องขออย่างอื่น
3) สตริงจัดรูปแบบ (สำหรับคนอ่าน)
สตริงเช่น "2025-01-01", "01/01/2025 10:00", หรือ "2025-01-01T00:00:00Z" ไม่ใช่สิ่งเดียวกันทั้งหมด บางแบบไม่กำกวม (ISO 8601 ที่มี Z), บางแบบขึ้นกับท้องถิ่น, และบางแบบไม่มีโซนเวลาเลย
“Instant” กับ “การแสดงสำหรับคนอ่าน”
- Instant: “โมเมนต์นี้ระดับโลก” (ควรเก็บเป็น epoch ms หรือ ISO string ใน UTC)
- การแสดงสำหรับคนอ่าน: “สิ่งที่ผู้ใช้ควรเห็น” (ขึ้นกับท้องถิ่นและโซนเวลา)
Instant เดียวกันสามารถแสดงต่างกันตามโซนเวลา:
const instant = new Date("2025-01-01T00:00:00Z");
instant.toLocaleString("en-US", { timeZone: "UTC" });
// "1/1/2025, 12:00:00 AM"
instant.toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
// "12/31/2024, 4:00:00 PM" (วันก่อนหน้า)
เลือก “แหล่งความจริง” เดียว
เลือกการแทนค่าภายในเดียว (โดยทั่วไป epoch milliseconds หรือ UTC ISO 8601) และยึดตามมันทั่วทั้งแอปและ API ของคุณ แปลงไป/กลับ Date และสตริงจัดรูปแบบเฉพาะที่ขอบเขต: การแยกวิเคราะห์เมื่อรับค่าเข้า และการแสดงใน UI
Timestamps: วินาที vs มิลลิวินาที (สับกันง่าย)
“timestamp” มักหมายถึง epoch time (หรือ Unix time): จำนวนเวลาตั้งแต่ 1970-01-01 00:00:00 UTC ปัญหาคือระบบต่างกันนับหน่วยต่างกัน
Date ของ JavaScript เป็นแหล่งความสับสนเพราะมันใช้ มิลลิวินาที ในขณะที่ API ฐานข้อมูล และโลกรายงานบางอย่างใช้ วินาที
กฎคร่าวๆ
- Unix timestamp (วินาที):
1704067200 - JavaScript timestamp (มิลลิวินาที):
1704067200000
เป็นโมเมนต์เดียวกัน แต่แบบมิลลิวินาทีมีเลขศูนย์เพิ่มสามหลัก
การแปลงที่ปลอดภัย (วินาที ↔ มิลลิวินาที)
ให้ใช้การคูณ/หารอย่างชัดเจนเพื่อให้หน่วยชัดเจน:
// seconds -> Date
const seconds = 1704067200;
const d1 = new Date(seconds * 1000);
// milliseconds -> Date
const ms = 1704067200000;
const d2 = new Date(ms);
// Date -> seconds
const secondsOut = Math.floor(d2.getTime() / 1000);
// Date -> milliseconds
const msOut = d2.getTime();
บั๊กคลาสสิก: ส่งวินาทีเข้า Date() โดยไม่แปลง
โค้ดนี้ดูสมเหตุสมผล แต่ผิดเมื่อ ts อยู่ในหน่วยวินาที:
const ts = 1704067200; // seconds
const d = new Date(ts); // WRONG: treated as milliseconds
ผลลัพธ์จะเป็นวันที่ในปี 1970 เพราะ 1,704,067,200 มิลลิวินาทีเป็นเพียงประมาณ 19 วันหลัง epoch
การตรวจสอบอย่างรวดเร็วขณะดีบัก
เมื่อไม่แน่ใจว่าคุณมีหน่วยไหน ให้เพิ่มการตรวจสอบง่ายๆ:
function asDateFromUnknownEpoch(x) {
// crude heuristic: seconds are ~1e9-1e10, milliseconds are ~1e12-1e13
if (x < 1e11) return new Date(x * 1000); // assume seconds
return new Date(x); // assume milliseconds
}
const input = Number(valueFromApi);
console.log({ input, digits: String(Math.trunc(input)).length });
console.log('as ISO:', asDateFromUnknownEpoch(input).toISOString());
ถ้าจำนวน "หลัก" ประมาณ 10 น่าจะเป็นวินาที ถ้าประมาณ 13 น่าจะเป็นมิลลิวินาที นอกจากนี้พิมพ์ toISOString() ในการดีบัก: มันไม่กำกวมและช่วยให้เห็นข้อผิดพลาดหน่วยทันที
เวลาในเครื่อง (Local) vs UTC: ทำไมการแสดงผลเลื่อนได้
Date ใน JavaScript อาจสับสนเพราะมัน เก็บ instant เดียว แต่สามารถ นำเสนอ instant นั้นในโซนเวลาต่างๆ
ภายใน Date จริงๆ คือ “มิลลิวินาทีนับตั้งแต่ Unix epoch (1970-01-01T00:00:00Z)” ตัวเลขนั้นแทนโมเมนต์ใน UTC การ "เลื่อน" เกิดขึ้นเมื่อคุณขอให้ JavaScript ฟอร์แมตโมเมนต์นั้นเป็น เวลาในเครื่อง (ตามการตั้งค่าคอมพิวเตอร์/เซิร์ฟเวอร์) เทียบกับ UTC
getter แบบ local vs getter แบบ UTC
หลาย API ของ Date มีแบบ local และแบบ UTC ให้ค่าแตกต่างกันสำหรับ instant เดียวกัน:
const d = new Date('2025-01-01T00:30:00Z');
d.getHours(); // hour in *local* time zone
d.getUTCHours(); // hour in UTC
d.toString(); // local time string
d.toISOString(); // UTC (always ends with Z)
ถ้าเครื่องของคุณอยู่ที่นิวยอร์ก (UTC-5) เวลานั้นอาจปรากฏเป็น “19:30” ในวันก่อนหน้าท้องถิ่น บนเซิร์ฟเวอร์ตั้งค่าเป็น UTC มันจะปรากฏเป็น “00:30” โมเมนต์เดียวกัน แต่อยู่การแสดงต่างกัน
ทำไมล็อกดู “ผิด”
ล็อกมักใช้ Date#toString() หรือแทรก Date โดยปริยาย ซึ่งใช้โซนเวลาในเครื่องของสภาพแวดล้อม นั่นหมายความว่า โค้ดเดียวกัน สามารถพิมพ์ timestamp ต่างกันบนเครื่องของคุณ ใน CI และใน production
คำแนะนำเชิงปฏิบัติ
เก็บและส่งเวลาเป็น UTC (เช่น epoch milliseconds หรือ ISO 8601 ที่มี Z) แปลงเป็น โซนเวลาผู้ใช้ เมื่อจะแสดงเท่านั้น:
- สำหรับ API: เลือก
toISOString()หรือส่ง epoch milliseconds - สำหรับ UI: ฟอร์แมตในโซนเวลาผู้ใช้โดยใช้
Intl.DateTimeFormat
ถ้าคุณกำลังสร้างแอปอย่างรวดเร็ว (เช่น ด้วย workflow แบบ vibe-coding ใน Koder.ai) ควรกำหนดสิ่งนี้ในสัญญา API ตั้งแต่ต้น: ตั้งชื่อตัวฟิลด์ชัดเจน (createdAtMs, createdAtIso) และให้ server (Go + PostgreSQL) กับ client (React) สอดคล้องกันว่าแต่ละฟิลด์แทนอะไร
สตริง ISO 8601: ฟอร์แมตที่ปลอดภัยที่สุดสำหรับ API
ถ้าต้องส่งวันที่/เวลา ระหว่างเบราว์เซอร์ เซิร์ฟเวอร์ และฐานข้อมูล สตริง ISO 8601 เป็นค่าพื้นฐานที่ปลอดภัย พวกมันชัดเจน รองรับอย่างกว้าง และที่สำคัญคือมีข้อมูลโซนเวลา
ใช้ UTC ชัดเจนหรือ offset ชัดเจน
สองฟอร์แมตที่ดีสำหรับการแลกเปลี่ยน:
- เวลา UTC (แนะนำเมื่อไม่สนใจโซนท้องถิ่น):
2025-03-04T12:30:00Z - เวลาท้องถิ่นพร้อม offset (แนะนำเมื่อ "เวลาบนหน้าปัดท้องถิ่น" สำคัญ):
2025-03-04T12:30:00+02:00
Z หมายความว่าอะไร?
Z ย่อมาจาก Zulu time ซึ่งเป็นอีกชื่อของ UTC ดังนั้น 2025-03-04T12:30:00Z คือ “12:30 ตาม UTC”
เมื่อใดที่ offset อย่าง +02:00 สำคัญ?
offset สำคัญเมื่อเหตุการณ์ผูกกับบริบทโซนเวลาท้องถิ่น (การนัดหมาย การจอง เวลาเปิดร้าน) 2025-03-04T12:30:00+02:00 บอกเวลาโมเมนต์ที่ เร็วกว่า UTC สองชั่วโมง และไม่เท่ากับ 2025-03-04T12:30:00Z
หลีกเลี่ยงสตริงวันที่กำกวม
สตริงเช่น 03/04/2025 เป็นกับดัก: เป็นวันที่ 4 มีนาคม หรือ 3 เมษายน? ผู้ใช้และสภาพแวดล้อมตีความต่างกัน ให้ใช้ 2025-03-04 (ISO date) หรือ datetime แบบเต็มของ ISO
กลับ-ออกอย่างปลอดภัย (string → Date → string)
const iso = "2025-03-04T12:30:00Z";
const d = new Date(iso);
const back = d.toISOString();
console.log(iso); // 2025-03-04T12:30:00Z
console.log(back); // 2025-03-04T12:30:00.000Z
พฤติกรรม “round-trip” แบบนี้คือสิ่งที่คุณต้องการสำหรับ API: สม่ำเสมอ คาดเดาได้ และรับรู้โซนเวลา
กับดักการแยกวิเคราะห์: Date.parse และความแตกต่างของเบราว์เซอร์
Date.parse() รู้สึกสะดวก: ให้สตริงแล้วได้ timestamp กลับ ปัญหาคือสำหรับอะไรก็ตามที่ไม่ชัดเจนเป็น ISO 8601 การแยกวิเคราะห์สามารถพึ่ง heuristics ของเบราว์เซอร์ ซึ่งต่างกันในแต่ละเอนจินและเวอร์ชัน นั่นหมายความว่าอินพุตเดียวกันอาจแยกวิเคราะห์ต่างกัน (หรือไม่ถูกเลย) ขึ้นกับที่โค้ดรัน
ทำไม Date.parse() ถึงต่างกันได้
JavaScript มาตรฐานการแยกวิเคราะห์ที่เชื่อถือได้เฉพาะสำหรับสตริงสไตล์ ISO 8601 (และแม้แต่รายละเอียดอย่างโซนเวลาก็มีข้อควรระวัง) สำหรับฟอร์แมตที่เป็นมิตร—เช่น "03/04/2025", "March 4, 2025", หรือ "2025-3-4"—เบราว์เซอร์อาจตีความ:
- เดือน/วัน เทียบกับ วัน/เดือน ตามสมมติฐานโลคอล
- สตริงที่ไม่มีโซนเวลาเป็น local time ในเอนจินหนึ่ง แต่ถูกปฏิเสธในอีกเอนจิน
- สตริงผิดรูปเล็กน้อยเป็น "ใกล้เคียงพอ" … จนกระทั่งไม่ใช่อีกต่อไป
ถ้าคุณไม่สามารถคาดเดารูปร่างสตริงได้แน่นอน คุณก็ไม่สามารถคาดเดาผลลัพธ์ได้
กรณีที่น่าตกใจ: YYYY-MM-DD
กับดักทั่วไปคือฟอร์แมตเฉพาะวันที่ "YYYY-MM-DD" (เช่น "2025-01-15") นักพัฒนาหลายคนคาดหวังว่าแปลเป็น เที่ยงคืนท้องถิ่น แต่ในทางปฏิบัติบางสภาพแวดล้อมตีความฟอร์แมตนี้เป็น เที่ยงคืน UTC
ความต่างนี้สำคัญ: เที่ยงคืน UTC แปลงเป็นเวลาในเครื่องอาจเป็น วันก่อนหน้า ในโซนเวลาที่มี offset เป็นลบ (เช่น อเมริกา) หรือเลื่อนชั่วโมงอย่างไม่คาดคิด เป็นวิธีง่ายๆ ที่จะเกิดบั๊ก "ทำไมวันที่ฉันมีคลาดไปหนึ่งวัน?"
เช็คลิสต์การแยกวิเคราะห์: อินพุตจากผู้ใช้ vs อินพุตจากเซิร์ฟเวอร์
สำหรับ อินพุตจากเซิร์ฟเวอร์/API:
- ควรใช้ ISO 8601 แบบเต็มพร้อมโซนเวลาชัดเจน เช่น
2025-01-15T13:45:00Zหรือ2025-01-15T13:45:00+02:00 - ถ้าเป็นค่าเฉพาะวันที่ ให้ปฏิบัติมันเป็น ข้อมูล ไม่ใช่โมเมนต์ในเวลา หากเป็นวันเกิดหรือวันกำหนดชำระ ให้เก็บเป็นสตริงธรรมดา (
"YYYY-MM-DD") และหลีกเลี่ยงการแปลงเป็นDateเว้นแต่จะกำหนดโซนเวลาที่ต้องการด้วย
สำหรับ อินพุตจากผู้ใช้:
- อย่ารับฟอร์แมตกำกวมเช่น
03/04/2025ยกเว้น UI ของคุณระบุความหมายแน่นอน - ควรใช้ input ที่ควบคุมได้ (date pickers) ที่สร้างฟอร์แมตที่รู้จัก
- ถ้าต้องแยกวิเคราะห์ข้อความอิสระ ให้กำหนดและบังคับฟอร์แมตที่ยอมรับล่วงหน้า
ใช้กฎชัดเจน (ไม่ใช่ heuristics)
แทนที่จะพึ่ง Date.parse() ให้ “เดา” เลือกหนึ่งในแพตเทิร์นเหล่านี้:
- รับเฉพาะ ISO 8601 จากเซิร์ฟเวอร์; ปฏิเสธอย่างอื่น
- แยกวิเคราะห์ด้วยตนเองสำหรับฟอร์แมตที่รู้จัก (แบ่งสตริงและใช้
new Date(year, monthIndex, day)สำหรับวันที่ท้องถิ่น) - เก็บและส่ง timestamps (epoch milliseconds) สำหรับ instant ที่ชัดเจน แล้วฟอร์แมตเมื่อจะแสดง
เมื่อข้อมูลเวลาเป็นเรื่องสำคัญ "มันแยกวิเคราะห์ในเครื่องฉัน" ไม่เพียงพอ—ทำให้กฎการแยกวิเคราะห์ชัดเจนและสม่ำเสมอ
ฟอร์แมตสำหรับคนอ่านด้วย Intl.DateTimeFormat
ถ้าจุดประสงค์คือ “แสดงวันที่/เวลาในแบบที่คนคาดหวัง” เครื่องมือที่ดีที่สุดใน JavaScript คือ Intl.DateTimeFormat มันใช้กฎโลคอลของผู้ใช้ (ลำดับ ตัวคั่น ชื่อเดือน) และหลีกเลี่ยงวิธีเปราะบางในการต่อสตริงด้วยมืออย่าง month + '/' + day
ทำไมมันชนะการต่อสตริงด้วยมือ
การฟอร์แมตด้วยมือมักกำหนดรูปแบบแบบ US ลืมเลขศูนย์นำหน้า หรือให้ผลลัพธ์ 24/12 ชั่วโมงที่สับสน Intl.DateTimeFormat ยังทำให้เห็นชัดว่ากำลังแสดงในโซนเวลาใด—สำคัญเมื่อข้อมูลเก็บเป็น UTC แต่ UI ควรแสดงโซนเวลาผู้ใช้
ตัวเลือกที่ใช้จริงได้บ่อย
สำหรับ “แค่อยากฟอร์แมตให้สวย” dateStyle และ timeStyle คือวิธีที่ง่ายที่สุด:
const d = new Date('2025-01-05T16:30:00Z');
// User’s locale + user’s local time zone
console.log(new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(d));
// Force a specific time zone (great for event times)
console.log(new Intl.DateTimeFormat('en-GB', {
dateStyle: 'full',
timeStyle: 'short',
timeZone: 'UTC'
}).format(d));
ถ้าต้องการควบคุม cycle ของชั่วโมง (เช่น สลับ 12/24 ชั่วโมง), ให้ใช้ hour12:
console.log(new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(d));
รูปแบบ UI เชิงปฏิบัติ
เลือกฟังก์ชันการฟอร์แมตหนึ่งอันต่อ “ประเภท” ของ timestamp ใน UI ของคุณ (เวลาในข้อความ, รายการล็อก, เริ่มเหตุการณ์) และตัดสินใจ timeZone อย่างตั้งใจ:
- ใช้เวลาในเครื่องสำหรับ “เกิดขึ้นเมื่อไหร่สำหรับฉัน”
- ใช้โซนคงที่ (มักเป็น UTC) สำหรับล็อก audit, เหตุการณ์เซิร์ฟเวอร์, หรือการประสานงานข้ามทีม
วิธีนี้ให้ผลลัพธ์ที่สอดคล้องและเป็นมิตรกับโลคอลโดยไม่ต้องดูแลชุดสตริงฟอร์แมตที่เปราะบาง
Daylight Saving Time: บั๊กซ่อนหนึ่งชั่วโมง
Daylight Saving Time (DST) คือช่วงที่โซนเวลาปรับ offset กับ UTC (โดยทั่วไปหนึ่งชั่วโมง) ในวันที่กำหนด ส่วนที่ยุ่งยากคือ DST ไม่ได้เพียงเปลี่ยน offset—แต่มันเปลี่ยน การมีอยู่ ของเวลาท้องถิ่นบางช่วง
เวลาตามนาฬิกาที่หายไปและซ้ำกัน
เมื่อเข็มนาฬิกา เดินหน้า ช่วงเวลาท้องถิ่นบางช่วงจะไม่เกิดขึ้น เช่น ในหลายภูมิภาค นาฬิกาจะข้ามจาก 01:59 เป็น 03:00 ดังนั้น 02:30 เวลาท้องถิ่นจะ “ไม่มี”
เมื่อเข็มนาฬิกา ถอยหลัง ช่วงเวลาท้องถิ่นบางช่วงเกิดขึ้นสองครั้ง ตัวอย่างเช่น 01:30 อาจเกิดขึ้นครั้งหนึ่งก่อนการเลื่อนและอีกครั้งหลังการเลื่อน ซึ่งหมายความว่าเวลาเดียวกันอาจอ้างถึงสอง instant ที่ต่างกัน
การบวก 24 ชั่วโมง vs “เวลาเดียวกันในวันถัดไป”
สองอย่างนี้ไม่เท่ากันรอบขอบเขต DST:
- บวก 24 ชั่วโมง: “ตรง 24 * 60 * 60 วินาทีต่อมา”
- วันถัดไปในเวลาเดียวกันตามท้องถิ่น: “พรุ่งนี้เวลา 9:00 น. ในโซนเวลานี้”
ถ้า DST เริ่มคืนนี้ “พรุ่งนี้เวลา 9:00” อาจห่างแค่ 23 ชั่วโมง ถ้า DST จบคืนนี้ อาจห่าง 25 ชั่วโมง
// Scenario: schedule “same local time tomorrow”
const d = new Date(2025, 2, 8, 9, 0); // Mar 8, 9:00 local
const plus24h = new Date(d.getTime() + 24 * 60 * 60 * 1000);
const nextDaySameLocal = new Date(d);
nextDaySameLocal.setDate(d.getDate() + 1);
// Around DST, plus24h and nextDaySameLocal can differ by 1 hour.
ทำไม setHours ถึงทำให้ประหลาดใจ
ถ้าคุณทำอย่าง date.setHours(2, 30, 0, 0) ในวันที่เกิด "spring forward" JavaScript อาจทำให้มันเป็นเวลาอื่นที่ถูกต้องแทน (มักเป็น 03:30) เพราะ 02:30 ไม่มีในเวลาท้องถิ่น
แนวทางที่ปลอดภัยกว่า
- ทำการคำนวณใน UTC สำหรับเวลาใช้หมด (durations): ใช้มิลลิวินาที epoch และเมธอด UTC
- สำหรับ การตั้งเวลาท้องถิ่น ให้ชัดเจนว่าต้องการอะไร: “วันถัดไปเวลา 9:00 ท้องถิ่น” ควรใช้การดำเนินการตามปฏิทิน (
setDate) แทนการบวกมิลลิวินาที - เมื่อแลกเปลี่ยนเวลาผ่าน API ให้ใช้ ISO 8601 พร้อม offset หรือ
Zเพื่อให้ instant ชัดเจน
Durations vs Dates: อย่าใช้ Date เป็นตัวจับเวลา
ข้อผิดพลาดทั่วไปคือการใช้ Date เพื่อแทนสิ่งที่ไม่ใช่โมเมนต์บนปฏิทิน
timestamp ตอบคำถามว่า “เรื่องนี้เกิดขึ้นเมื่อไหร่?” (instant เฉพาะ เช่น 2025-12-23T10:00:00Z) ส่วน duration ตอบคำถามว่า “นานเท่าไหร่?” (เช่น “3 นาที 12 วินาที”) ทั้งสองต่างกัน และการผสมกันนำไปสู่คณิตศาสตร์ที่สับสนและผลกระทบจากโซนเวลา/DST
ทำไม Date ไม่ใช่เครื่องมือที่เหมาะสำหรับ durations
Date แทนจุดบนเส้นเวลาสัมพันธ์กับ epoch หากเก็บ “90 seconds” เป็น Date จริงๆ แล้วคุณเก็บ “1970-01-01 บวก 90 วินาที” ในโซนเวลาหนึ่ง การฟอร์แมตอาจแสดง 01:01:30, เลื่อนไปชั่วโมง หรือแสดงวันที่ที่คุณไม่ตั้งใจ
สำหรับ durations ให้เก็บเป็น ตัวเลขล้วน:
- เก็บ duration เป็น วินาที หรือ มิลลิวินาที (เลือกหน่วยหนึ่งและยึดตามมัน)
- คำนวณด้วยตัวเลข
- แปลงเป็นสตริงแสดงผลเมื่อถึงเวลาเท่านั้น
แปลงวินาทีเป็น HH:mm:ss
ตัวอย่างฟอร์แมตเตอร์ง่ายๆ สำหรับตัวนับถอยหลังหรือความยาวสื่อ:
function formatHMS(totalSeconds) {
const s = Math.max(0, Math.floor(totalSeconds));
const hh = String(Math.floor(s / 3600)).padStart(2, "0");
const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
const ss = String(s % 60).padStart(2, "0");
return `${hh}:${mm}:${ss}`;
}
formatHMS(75); // "00:01:15" (countdown timer)
formatHMS(5423); // "01:30:23" (media duration)
ถ้ากำลังแปลงจากนาที ให้คูณก่อน (minutes * 60) และเก็บค่าเป็นตัวเลขจนกว่าจะต้องเรนเดอร์
เปรียบเทียบ เรียงลำดับ และช่วงเวลาโดยไม่ประหลาดใจ
เมื่อคุณเปรียบเทียบเวลาใน JavaScript วิธีที่ปลอดภัยคือเปรียบเทียบตัวเลข ไม่ใช่ข้อความฟอร์แมต Date คือห่อรอบ timestamp ตัวเลข (มิลลิวินาที epoch) ดังนั้นการเปรียบเทียบควรจบลงด้วย “ตัวเลขเทียบตัวเลข”
การเปรียบเทียบที่ปลอดภัย (timestamp ชนะ)
ใช้ getTime() (หรือ Date.valueOf() ซึ่งคืนค่าเดียวกัน) เพื่อเปรียบเทียบอย่างเชื่อถือได้:
const a = new Date('2025-01-10T12:00:00Z');
const b = new Date('2025-01-10T12:00:01Z');
if (a.getTime() < b.getTime()) {
// a is earlier
}
// Also works:
if (+a < +b) {
// unary + calls valueOf()
}
หลีกเลี่ยงการเปรียบเทียบสตริงที่ฟอร์แมตแล้วเช่น "1/10/2025, 12:00 PM"—พวกนั้นขึ้นกับโลคอลและจะไม่เรียงลำดับถูกต้อง ข้อยกเว้นหลักคือสตริง ISO 8601 ในฟอร์แมตเดียวกันและโซนเวลาเดียวกัน (เช่น ทั้งหมดเป็น ...Z) ซึ่งเรียงตามลำดับตัวอักษรได้
การเรียงลำดับและกรองตามช่วง
การเรียงลำดับโดยเวลาเรียบง่ายถ้าคุณเรียงตามมิลลิวินาที epoch:
items.sort((x, y) => new Date(x.createdAt).getTime() - new Date(y.createdAt).getTime());
การกรองไอเท็มในช่วงก็ใช้แนวคิดเดียวกัน:
const start = new Date('2025-01-01T00:00:00Z').getTime();
const end = new Date('2025-02-01T00:00:00Z').getTime();
const inRange = items.filter(i => {
const t = new Date(i.createdAt).getTime();
return t >= start && t < end;
});
“จุดเริ่ม/จุดสิ้นของวัน” (local vs UTC)
“จุดเริ่มของวัน” ขึ้นกับว่าคุณหมายถึงเวลาในเครื่องหรือ UTC:
// Local start/end of day
const d = new Date(2025, 0, 10); // Jan 10 in local time
const localStart = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
const localEnd = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
// UTC start/end of day
const utcStart = new Date(Date.UTC(2025, 0, 10, 0, 0, 0, 0));
const utcEnd = new Date(Date.UTC(2025, 0, 10, 23, 59, 59, 999));
เลือกคำนิยามหนึ่งแต่ต้นและยึดตามมันตลอดการเปรียบเทียบและตรรกะช่วงของคุณ
เช็คลิสต์ดีบัก: วิธีวินิจฉัยบั๊กการแปลงเวลา
บั๊กเกี่ยวกับเวลาดูเหมือนสุ่มจนกว่าคุณจะระบุได้ว่า คุณมีอะไร (timestamp? สตริง? Date?) และ จุดไหน ที่เกิดการเลื่อน (การแยกวิเคราะห์ การแปลงโซนเวลา การฟอร์แมต)
1) แคปเจอร์ “สามมุมมอง” ของโมเมนต์เดียวกัน
เริ่มด้วยการล็อกค่านั้นในสามรูปแบบต่างกัน วิธีนี้เผยเร็วว่าปัญหาเป็นเรื่องวินาที vs มิลลิวินาที, local vs UTC, หรือการแยกวิเคราะห์สตริง
console.log('raw input:', input);
const d = new Date(input);
console.log('toISOString (UTC):', d.toISOString());
console.log('toString (local):', d.toString());
console.log('timezone offset (min):', d.getTimezoneOffset());
ควรมองหาอะไร:
- ถ้า
toISOString()ผิดมาก (เช่น ปี 1970 หรือไกลออกไป) ให้สงสัย วินาที vs มิลลิวินาที - ถ้า
toISOString()ดูถูกต้องแต่toString()"เลื่อน" แสดงว่าคุณกำลังเห็นปัญหา การแสดงเวลาในเครื่อง - ถ้า
getTimezoneOffset()เปลี่ยนตามวันที่ แสดงว่าคุณข้าม Daylight Saving Time
2) ยืนยันสภาพแวดล้อม: โซนเวลาและโลคอล
หลายรายงาน "มันทำงานในเครื่องฉัน" เป็นเพราะค่าเริ่มต้นสภาพแวดล้อมต่างกัน
- เบราว์เซอร์: ตรวจสอบการตั้งค่าโซนเวลาของ OS และภาษาของเบราว์เซอร์ แล้วล็อก:
console.log(Intl.DateTimeFormat().resolvedOptions());
- Node.js / เซิร์ฟเวอร์: ยืนยันโซนเวลาของโปรเซส:
console.log('TZ:', process.env.TZ);
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
ถ้าเซิร์ฟเวอร์รันใน UTC แต่เครื่องคุณรันในโซนท้องถิ่น การแสดงผลที่จัดรูปแบบจะแตกต่างเว้นแต่คุณระบุ timeZone อย่างชัดเจน
3) เพิ่มเทสต์ในจุดที่เวลาแตกบ่อยที่สุด
สร้าง unit tests รอบขอบเขต DST และเวลา “ขอบ” :
- หนึ่งชั่วโมงก่อนและหลังการสลับ DST ในโซนที่เกี่ยวข้อง
- สิ้นเดือน/ปี และการข้าม
23:30→00:30 - หลายโซนเวลาถ้าผลิตภัณฑ์ของคุณรองรับ
ถ้าคุณ iterate เร็ว ให้ทำให้เทสต์พวกนี้เป็นส่วนหนึ่งของโครงสร้าง เช่น เมื่อสร้าง React + Go app ใน Koder.ai คุณอาจเพิ่มชุดทดสอบ "time contract" เล็กๆ ตั้งแต่ต้น (ตัวอย่าง payload + assert การแยกวิเคราะห์/ฟอร์แมต) เพื่อจับการ regress ก่อน deploy
เช็คลิสต์ก่อนปล่อย
- อินพุตมีสัญญาชัดเจน: epoch milliseconds หรือ ISO 8601 พร้อม offset
- ไม่มีสตริงกำกวมเช่น
"2025-03-02 10:00" - การฟอร์แมตกำหนด
localeและ (เมื่อจำเป็น)timeZone - เทสต์ครอบคลุมขอบเขต DST สำหรับภูมิภาคเป้าหมายของคุณ
แบบแผนที่แนะนำเพื่อการจัดการเวลาที่เชื่อถือได้
การจัดการเวลาใน JavaScript ที่เชื่อถือได้คือการเลือก “แหล่งความจริง” และสม่ำเสมอตั้งแต่การเก็บจนถึงการแสดง
แนวปฏิบัติพื้นฐานง่ายๆ
เก็บและคำนวณใน UTC ถือเวลาท้องถิ่นสำหรับการนำเสนอเท่านั้น ส่งวันที่ระหว่างระบบเป็นสตริง ISO 8601 พร้อม offset ชัดเจน (หรือ Z) ถ้าต้องส่ง epoch เป็นตัวเลข ให้ระบุหน่วยและยึดตามมัน (มิลลิวินาทีเป็นดีฟอลต์ทั่วไปใน JS)
ฟอร์แมตสำหรับคนอ่านด้วย Intl.DateTimeFormat (หรือ toLocaleString) และส่ง timeZone อย่างชัดเจนเมื่อคุณต้องการผลลัพธ์ที่แน่นอน (เช่น แสดงเวลาเป็น UTC เสมอ หรือโซนธุรกิจเฉพาะ)
แนวทางการตัดสินใจ: DB vs API vs UI
- ฐานข้อมูล: เก็บ instant เป็น UTC (epoch milliseconds หรือชนิด datetime แบบ UTC) หลีกเลี่ยง datetime แบบ local เว้นแต่โดเมนของคุณเก็บเวลาแบบ wall-clock จริงๆ (เช่น "ร้านเปิด 09:00")
- ขอบเขต API: ชอบ ISO 8601 พร้อม
Z(เช่น2025-12-23T10:15:00Z) ถ้าใช้ epochs ให้ตั้งชื่อลักษณะฟิลด์เช่นcreatedAtMsเพื่อให้หน่วยชัดเจน - UI: เอา instant ที่เก็บเป็น UTC แล้วฟอร์แมตให้ตรงกับโลคอลของผู้ใช้ สำหรับ UI การตั้งเวลาควรระบุโซนเวลาและการแปลงอย่างชัดเจน
เมื่อไลบรารีคุ้มค่า
พิจารณาไลบรารีจัดการเวลาเฉพาะถ้าคุณต้องการเหตุการณ์ซ้ำซ้อน กฎโซนเวลาซับซ้อน การคำนวณที่ปลอดภัยรอบ DST ("เวลาเดียวกันพรุ่งนี้" แบบปลอดภัย) หรือต้องแยกวิเคราะห์จากอินพุตที่ไม่สม่ำเสมอ ค่าที่ได้คือ API ที่ชัดเจนและบั๊กมุมขอบที่น้อยลง
ถ้าต้องการอ่านเพิ่มเติมเกี่ยวกับเวลา ลองดูแหล่งข้อมูลเพิ่มเติมที่ /blog หากคุณกำลังประเมินเครื่องมือหรือทางเลือกการสนับสนุน ให้ดูที่ /pricing.
คำถามที่พบบ่อย
ควรใช้รูปแบบใดสำหรับการประทับเวลาใน JavaScript?
ใช้มิลลิวินาทีแบบ epoch หรือสตริง UTC ISO 8601 เป็นรูปแบบภายในของคุณ แปลงเป็นรูปแบบท้องถิ่นเมื่อแสดงค่าให้ผู้ใช้ดูเท่านั้น
เหตุใดการประทับเวลา Unix ของฉันจึงกลายเป็นวันที่ในปี 1970?
Date ของ JavaScript ต้องการหน่วยเป็นมิลลิวินาที คูณการประทับเวลา Unix ที่เป็นวินาทีด้วย 1000 ก่อนส่งให้ new Date()
เหตุใดวันที่เดียวกันจึงแสดงเวลาแตกต่างกันบนคอมพิวเตอร์แต่ละเครื่อง?
Date เก็บจุดเวลาเดียว แต่ JavaScript มักแสดงผลตามเขตเวลาท้องถิ่นของคอมพิวเตอร์ เปรียบเทียบ toISOString() กับ toString() เพื่อดูว่าข้อมูลเปลี่ยนไป หรือเปลี่ยนแค่การแสดงผล
จะป้องกันไม่ให้วันที่เลื่อนไปหนึ่งวันได้อย่างไร?
เก็บค่าที่มีเฉพาะวันที่ เช่น วันเกิดหรือวันที่ในใบแจ้งหนี้ ไว้เป็น YYYY-MM-DD อย่าแปลงเป็น Date เว้นแต่คุณจะกำหนดเขตเวลาที่ต้องการไว้ด้วย
รูปแบบวันที่ใดปลอดภัยที่สุดสำหรับ API?
ส่งค่า ISO 8601 แบบเต็มพร้อม Z สำหรับ UTC เช่น 2025-03-04T12:30:00Z หรือระบุออฟเซ็ตอย่างชัดเจน หลีกเลี่ยงสตริงอย่าง 03/04/2025 เพราะผู้ใช้และเบราว์เซอร์อาจตีความต่างกัน
ควรจัดรูปแบบวันที่สำหรับผู้ใช้อย่างไร?
ใช้ Intl.DateTimeFormat และระบุ timeZone เมื่อหน้าจอต้องแสดงเขตเวลาแบบตายตัว ระบบจะจัดรูปแบบวันที่ตามโลแคลของผู้ใช้โดยไม่ต้องประกอบสตริงเอง
ควรใช้ Date.parse กับวันที่ที่ผู้ใช้ป้อนหรือไม่?
อย่าพึ่งพาเมธอดนี้กับสตริงที่ไม่เป็นทางการหรือกำกวม รับรูปแบบ ISO ที่แน่นอนจาก API หรือแยกวิเคราะห์ข้อมูลที่ผู้ใช้ป้อนภายใต้การควบคุมด้วยกฎที่คุณกำหนดเอง
เหตุใดจึงควรหลีกเลี่ยงการใช้ Date สำหรับตัวนับถอยหลังหรือระยะเวลา?
Date ระบุช่วงเวลาหนึ่งบนปฏิทิน ส่วนระยะเวลาเป็นเพียงปริมาณเวลา เก็บระยะเวลาเป็นวินาทีหรือมิลลิวินาที แล้วจัดรูปแบบตัวเลขตอนนำไปแสดงผล
เวลาออมแสงส่งผลต่อวันที่ใน JavaScript อย่างไร?
ใช้มิลลิวินาทีแบบ epoch สำหรับการคำนวณเวลาที่ผ่านไป สำหรับตารางเวลา เช่น «พรุ่งนี้เวลา 9:00 น.» ในเขตเวลาที่ระบุชื่อ ให้ใช้การดำเนินการตามปฏิทินและทดสอบวันที่เปลี่ยนผ่าน DST
วิธีที่เร็วที่สุดในการดีบักบั๊กเกี่ยวกับเวลาใน JavaScript คืออะไร?
บันทึกอินพุตดิบ, toISOString(), toString() และ getTimezoneOffset() ค่า ISO ที่ผิดมักหมายถึงปัญหาการแยกวิเคราะห์หรือหน่วย ส่วนสตริงท้องถิ่นที่ต่างกันมักหมายถึงความต่างในการแสดงผลตามเขตเวลา