6 คะแนน โดย GN⁺ 2026-01-13 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ชี้ให้เห็นถึง ความไม่สมบูรณ์และความไม่สอดคล้องกันของอ็อบเจ็กต์ Date เดิมใน JavaScript พร้อมแนะนำ Temporal API ที่จะมาแทนที่
  • Date ทำงานเป็น อ็อบเจ็กต์แบบเปลี่ยนแปลงได้ (mutable object) จึงไม่สอดคล้องกับแนวคิดของวันที่จริง ๆ และยังมีปัญหาเชิงโครงสร้าง เช่น ข้อผิดพลาดในการ parsing และข้อจำกัดในการจัดการเขตเวลา
  • Temporal มอบ โมเดลการจัดการวันและเวลาแบบใหม่ที่ยึดตามความไม่เปลี่ยนแปลง (immutability) พร้อมคลาสที่แยกตามหน้าที่ เช่น PlainDate, ZonedDateTime, Duration
  • เมธอดของ Temporal จะ คืนค่าเป็นอ็อบเจ็กต์ใหม่ โดยไม่แก้ไขอ็อบเจ็กต์เดิม ทำให้ทำ chaining operation ได้อย่างชัดเจนและปลอดภัย
  • ขณะนี้ Temporal อยู่ใน ขั้นตอนมาตรฐาน Stage 3 และเริ่มมีการรองรับแบบทดลองในเบราว์เซอร์สมัยใหม่อย่าง Chrome และ Firefox

ปัญหาของอ็อบเจ็กต์ Date ใน JavaScript

  • คอนสตรัคเตอร์ Date ก่อให้เกิดความสับสนจาก กฎการ parsing ที่ไม่สอดคล้องกัน และ การนับดัชนีที่ไม่เป็นธรรมชาติ
    • ตัวอย่าง: เดือน (month) เริ่มนับจาก 0 แต่วัน (day) และปี (year) เริ่มจาก 1
    • สตริง "99" ถูกตีความเป็นปี 1999 แต่ "100" กลับถูกตีความเป็นปี 0100 แสดงถึงความไม่สม่ำเสมอ
  • Date ถูกออกแบบโดยยึด เวลา (time) เป็นศูนย์กลาง และจัดเก็บภายในเป็น Unix timestamp (หน่วยมิลลิวินาที)
  • การรองรับ เขตเวลา (time zone) มีข้อจำกัด และไม่เข้าใจ เวลาออมแสง (DST) หรือ ปฏิทินที่ไม่ใช่เกรกอเรียน
  • ด้วยข้อจำกัดเหล่านี้ จึงเป็นเรื่องปกติที่จะต้องพึ่งไลบรารี third-party ขนาดใหญ่อย่าง Moment.js, date-fns เป็นต้น ซึ่งนำไปสู่ ประสิทธิภาพที่ลดลง

ความขัดแย้งระหว่างความไม่เปลี่ยนแปลงกับแนวคิดเรื่อง reference

  • primitive ใน JavaScript เป็นค่าที่ไม่เปลี่ยนแปลงและถูกเก็บเป็นตัวค่าโดยตรง แต่ object ถูกเก็บแบบ reference จึงสามารถเปลี่ยนแปลงได้
  • Date เป็นอ็อบเจ็กต์ที่สร้างผ่าน constructor จึงมี ความเปลี่ยนแปลงได้
    • ตัวอย่าง: เมื่อเรียก setMonth() หรือ setDate() อ็อบเจ็กต์ต้นฉบับจะถูกแก้ไขโดยตรง
  • ส่งผลให้เกิด การเปลี่ยนค่าที่ไม่คาดคิด ระหว่างตัวแปรที่อ้างอิงอ็อบเจ็กต์เดียวกัน
    • ตัวอย่าง: หากฟังก์ชันรับ today เป็นอาร์กิวเมนต์แล้วแก้วันที่ภายใน ค่า today ต้นฉบับก็จะเปลี่ยนไปด้วย

Temporal: API ใหม่สำหรับวันและเวลา

  • Temporal เป็น namespace object ไม่ใช่ constructor และมีโครงสร้างคล้าย Math
    • องค์ประกอบหลัก: PlainDate, PlainDateTime, PlainTime, ZonedDateTime, Duration, Now เป็นต้น
  • Temporal.Now คืนค่าช่วงเวลาปัจจุบันได้หลายรูปแบบ
    • plainDateISO() → วันที่ในรูปแบบ ISO
    • zonedDateTimeISO() → เวลาพร้อมเขตเวลา
  • อ็อบเจ็กต์ Temporal มี ระบบเมธอดที่ชัดเจน
    • ใช้ add({ days: 1 }), subtract({ years: 2 }) เป็นต้น เพื่อทำ การคำนวณตามหน่วยอย่างชัดเจน
    • ไม่แก้ไขอ็อบเจ็กต์เดิม แต่ คืนค่าเป็นอ็อบเจ็กต์ใหม่ เพื่อรักษาความไม่เปลี่ยนแปลง

วิธีการทำงานและข้อดีของ Temporal

  • แม้อ็อบเจ็กต์ Temporal จะยังคงเป็นชนิด object แต่ก็ถูกออกแบบให้มี รูปแบบการใช้งานแบบไม่เปลี่ยนแปลงที่ตั้งใจไว้
    • ตัวอย่าง: today.add({ days: 1 }) จะคืนค่าอ็อบเจ็กต์วันที่ใหม่ โดย today เดิมจะไม่เปลี่ยนแปลง
  • ให้ ไวยากรณ์ที่กระชับและชัดเจนกว่า เมื่อเทียบกับ Date
    • ตัวอย่าง:
      const today = Temporal.Now.plainDateISO();  
      console.log(`Tomorrow will be ${ today.add({ days: 1 }) }. Today is ${ today }.`);  
      // 결과: Tomorrow will be 2026-01-01. Today is 2025-12-31.  
      
  • รองรับความต้องการสมัยใหม่ เช่น การระบุเขตเวลา, การคำนวณช่วงเวลา, การคงรูปแบบ ISO
  • สามารถใช้เมธอด chaining เช่น add, subtract, since, until เพื่อเขียน การคำนวณวันที่ซับซ้อน ให้กระชับได้

สถานะการทำให้เป็นมาตรฐานและแนวโน้มต่อจากนี้

  • Temporal ได้เข้าสู่ ขั้นข้อเสนอ ECMAScript ระดับ 3 (Stage 3) แล้ว ซึ่งเป็นสถานะที่แนะนำให้เบราว์เซอร์เริ่มนำไปใช้งาน
  • Chrome และ Firefox เริ่มรองรับแบบทดลองแล้ว และเบราว์เซอร์อื่น ๆ ก็มีแผนนำมาใช้ตามมา
  • นักพัฒนาสามารถเริ่ม ทดสอบและส่ง feedback ได้ตั้งแต่ตอนนี้ เพื่อมีส่วนร่วมในการปรับปรุงสเปก
  • แม้ Date จะยังคงอยู่ แต่ในอนาคต Temporal มีแนวโน้มจะกลายเป็นวิธีหลักในการจัดการวันที่
  • บทความปิดท้ายว่า “มันควรถูกแทนที่ตั้งแต่ปี 1995 แต่ถึงตอนนี้ Temporal.Now ก็ยังเป็นจังหวะที่เหมาะที่สุด”

1 ความคิดเห็น

 
GN⁺ 2026-01-13
ความคิดเห็นจาก Hacker News
  • บทความนี้พูดถึงพฤติกรรมชวนงงหลายอย่างของ ตัวสร้าง Date ใน JavaScript
    โดยเฉพาะการที่รูปแบบ 'YYYY-MM-DD' ถูกตีความเป็น เที่ยงคืนแบบ UTC ทำให้ในเขตเวลาท้องถิ่นวันที่คลาดไปหนึ่งวัน
    เดิมทีตาม ISO 8601 ถ้าไม่ได้ระบุเขตเวลา ควรถูกมองเป็นเวลาท้องถิ่น แต่ระหว่างการเขียนสเปก ES5 กลับพลาดจนถูกจัดการเป็น “Z” (UTC)
    ภายหลังพยายามแก้ใน ES2015 แต่เนื่องจากมีเว็บไซต์จำนวนมากพึ่งพาพฤติกรรมที่ผิดเดิมอยู่ จึงต้องย้อนกลับด้วยเหตุผลด้าน ความเข้ากันได้ของเว็บ
    ดูรายละเอียดเพิ่มเติมได้ในส่วน Broken Parser

    • น่าจะดีถ้ามี directive แบบ 'strict datetime' เหมือน 'use strict'
      แบบนี้จะสามารถเลือกใช้พฤติกรรมที่ถูกต้องได้โดยไม่เกิดปัญหา ความไม่เข้ากันกับโค้ดเดิม
      หรืออาจใช้วิธีนำเข้า global object ที่แก้ไขแล้ว ผ่านโมดูลภายใน เช่น import Date from 'browser:date'
    • เมื่อก่อนฉันก็เคยแยกสตริงเองเพื่อสร้าง ออบเจ็กต์วันที่ที่ไม่มีเขตเวลา
      ค่าประเภทวันเกิดที่ต้องการสื่อแค่วันที่อย่างเดียว ไม่ควรเปลี่ยนเพราะเขตเวลา
      จำได้ว่า Outlook เคยเก็บวันเกิดพร้อมเขตเวลา ทำให้พอย้ายประเทศ วันเกิดก็เลื่อนไปทีละหนึ่งวัน
    • วลีที่ว่า “ถูกบูชายัญบนแท่นแห่งความเข้ากันได้ของเว็บ” ฟังดูติดหูมาก
      แต่จะมีทางเลือกอื่นจริงหรือ? ถ้าต้องบังคับให้เช็กตามเวอร์ชันเบราว์เซอร์เหมือนสมัย IE5 ก็คงแย่กว่าเดิม
    • เว็บไซต์ jsdate.wtf ทำให้ลองสัมผัส พฤติกรรมสุดประหลาด ของ JS Date ได้โดยตรง
    • พอคิดว่าปัญหาแบบนี้เป็นต้นตอของ บั๊กเล็ก ๆ มากมายนับไม่ถ้วน ก็ทั้งขำทั้งเศร้า
  • อิจฉา วิธีจัดการเวลา ของ Rails และ Ruby มาก
    API อย่าง Time.current.in_time_zone('America/Los_Angeles') + 3.days - 4.months + 1.hour ทั้งเข้าใจง่ายและทรงพลัง
    Ruby โอเวอร์โหลด Time ให้เป็น ออบเจ็กต์ที่สอดคล้องกันเพียงหนึ่งเดียว จึงแทบไม่ต้องกังวลเรื่องการแปลงหรือการแคสต์
    เลยคิดว่าถ้าใน JS เขียนแบบ new Date().add({ days: 1 }) ได้ง่าย ๆ ก็คงดีมาก

    • แต่ก็มีข้อสงสัยว่าไวยากรณ์อย่าง “3.days - 4.months + 1.hour” ดีจริงหรือไม่
      อีกทั้งการโอเวอร์โหลด core library เป็นแนวทางที่ดีจริงไหมก็ยังเป็นประเด็นถกเถียงได้
  • น่าเสียดายที่ Safari ยังไม่รองรับ Temporal API
    หวังว่าปีหน้าจะรองรับ

  • แม้ JavaScript Date จะมีปัญหามาก แต่ก็คิดว่าการเป็น ออบเจ็กต์ เองไม่ใช่ปัญหาใหญ่
    ถ้าเป็น immutable object ก็คงดีกว่า แต่จะตกใจที่ mutable object ถูกเปลี่ยนค่าก็คงไม่ใช่เรื่อง

    • แต่ปัญหาคือโค้ดอื่นสามารถ เปลี่ยน Date object ที่ฉันถืออยู่แบบเงียบ ๆ ได้
      อันตรายที่แท้จริงของความเปลี่ยนแปลงได้อยู่ที่ การเปลี่ยนแปลงจากภายนอกบริบท มากกว่า
  • รู้สึกไม่สะดวกที่ Temporal API ไม่จัดการข้อมูล leap second เลย
    อยากทำเครื่องมือ JS สำหรับการคำนวณทางดาราศาสตร์ แต่การแปลง UTC ต้องใช้ข้อมูล leap second
    มีทางอ้อมอย่าง temporal-tai ก็จริง แต่ก็ยุ่งยากเพราะต้อง ดูแลไฟล์ leap second ฝั่งไคลเอนต์
    และเพราะ SOP (นโยบาย CORS) จึงไม่สามารถดึงไฟล์จากเว็บภายนอกได้ตรง ๆ
    เบราว์เซอร์ก็อัปเดตเป็นระยะอยู่แล้ว จึงสงสัยว่าทำไมไม่ฝังข้อมูล leap second มาให้

    • ความเข้าใจที่ว่า SOP ขัดขวางไฟล์ leap second นั้นไม่ถูกต้อง
      ถ้าเซิร์ฟเวอร์ตั้งค่า header Access-Control-Allow-Origin หรือให้มาในรูปไฟล์ JS ก็ทำได้
      เพียงแต่การที่เบราว์เซอร์จะรวมและดูแลข้อมูล leap second เองอาจเป็นงานที่ มีต้นทุนสูง
    • คำว่า “รองรับแค่ UTC” ก็ไม่แม่นนัก
      UTC โดยตัวมันเองรวม leap second อยู่แล้ว ดังนั้นจริง ๆ ควรมองว่ามันรองรับเพียง เวลาแบบ POSIX
  • ในตัวอย่างโค้ดควรบอกว่าเริ่มตีความเป็นยุค 1900 ตั้งแต่ "50" ไม่ใช่ "33" — เป็นเพียงการ ชี้ว่าพิมพ์ผิด

  • กำลังใช้ Temporal polyfill อยู่ และจนถึงตอนนี้พอใจมาก

    • แต่ขนาด 51KB ก็ไม่ได้เบานัก (ลิงก์ bundlephobia)
      สำหรับเซิร์ฟเวอร์หรือแอปขนาดใหญ่ถือว่าโอเค แต่กับแอปเล็กอาจเป็นภาระ
    • มีคนถามด้วยว่าเมื่อเทียบกับ moment หรือ luxon แล้วเป็นอย่างไร
  • น่าแปลกที่บทความไม่พูดถึง Date.now() เลย
    ถ้าจะเทียบกับ Temporal ก็ควรอธิบายโดยยึด Date.now() เป็นฐาน
    ฟังก์ชันนี้คืนค่า เวลาที่ผ่านไปเป็นมิลลิวินาทีนับจาก 1 มกราคม 1970
    แม้ Temporal จะมี API ที่เป็นมิตรกว่า แต่แก่นแท้ก็ยังมีเป้าหมายเพื่อแสดง ระยะห่างสัมพัทธ์ของเวลา

    • แต่ก็ยังมี บั๊กเกี่ยวกับ timestamp ที่ถูกกล่าวถึงในบทความอยู่
      ถ้าอย่างนั้นจะมีวิธีแปลงเป็นรูปแบบที่ต้องการโดยไม่ต้องผ่าน Date หรือไม่ก็น่าสงสัย
  • มีการทักท้วงเล็กน้อยแต่สำคัญว่า ควรใช้ “daylight saving time” ไม่ใช่ “daylight savings time”

  • ไม่เคยรู้มาก่อนเลยว่า JS Date จะเละเทะขนาดนี้

    • ที่น่าเสียดายยิ่งกว่าคือ ปัญหาเหล่านี้ เห็นกันชัดเจนมาตั้งแต่ปี 1995 แล้ว
      ถ้าตอนนั้นรอบคอบกว่านี้อีกนิด นักพัฒนาจำนวนมากก็คงไม่ต้องมาตกหลุมพรางแบบนี้