- ชี้ให้เห็นถึง ความไม่สมบูรณ์และความไม่สอดคล้องกันของอ็อบเจ็กต์ 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
- รองรับความต้องการสมัยใหม่ เช่น การระบุเขตเวลา, การคำนวณช่วงเวลา, การคงรูปแบบ ISO
- สามารถใช้เมธอด chaining เช่น
add, subtract, since, until เพื่อเขียน การคำนวณวันที่ซับซ้อน ให้กระชับได้
สถานะการทำให้เป็นมาตรฐานและแนวโน้มต่อจากนี้
Temporal ได้เข้าสู่ ขั้นข้อเสนอ ECMAScript ระดับ 3 (Stage 3) แล้ว ซึ่งเป็นสถานะที่แนะนำให้เบราว์เซอร์เริ่มนำไปใช้งาน
- Chrome และ Firefox เริ่มรองรับแบบทดลองแล้ว และเบราว์เซอร์อื่น ๆ ก็มีแผนนำมาใช้ตามมา
- นักพัฒนาสามารถเริ่ม ทดสอบและส่ง feedback ได้ตั้งแต่ตอนนี้ เพื่อมีส่วนร่วมในการปรับปรุงสเปก
- แม้
Date จะยังคงอยู่ แต่ในอนาคต Temporal มีแนวโน้มจะกลายเป็นวิธีหลักในการจัดการวันที่
- บทความปิดท้ายว่า “มันควรถูกแทนที่ตั้งแต่ปี 1995 แต่ถึงตอนนี้ Temporal.Now ก็ยังเป็นจังหวะที่เหมาะที่สุด”
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
บทความนี้พูดถึงพฤติกรรมชวนงงหลายอย่างของ ตัวสร้าง Date ใน JavaScript
โดยเฉพาะการที่รูปแบบ
'YYYY-MM-DD'ถูกตีความเป็น เที่ยงคืนแบบ UTC ทำให้ในเขตเวลาท้องถิ่นวันที่คลาดไปหนึ่งวันเดิมทีตาม ISO 8601 ถ้าไม่ได้ระบุเขตเวลา ควรถูกมองเป็นเวลาท้องถิ่น แต่ระหว่างการเขียนสเปก ES5 กลับพลาดจนถูกจัดการเป็น “Z” (UTC)
ภายหลังพยายามแก้ใน ES2015 แต่เนื่องจากมีเว็บไซต์จำนวนมากพึ่งพาพฤติกรรมที่ผิดเดิมอยู่ จึงต้องย้อนกลับด้วยเหตุผลด้าน ความเข้ากันได้ของเว็บ
ดูรายละเอียดเพิ่มเติมได้ในส่วน Broken Parser
'strict datetime'เหมือน'use strict'แบบนี้จะสามารถเลือกใช้พฤติกรรมที่ถูกต้องได้โดยไม่เกิดปัญหา ความไม่เข้ากันกับโค้ดเดิม
หรืออาจใช้วิธีนำเข้า global object ที่แก้ไขแล้ว ผ่านโมดูลภายใน เช่น
import Date from 'browser:date'ค่าประเภทวันเกิดที่ต้องการสื่อแค่วันที่อย่างเดียว ไม่ควรเปลี่ยนเพราะเขตเวลา
จำได้ว่า Outlook เคยเก็บวันเกิดพร้อมเขตเวลา ทำให้พอย้ายประเทศ วันเกิดก็เลื่อนไปทีละหนึ่งวัน
แต่จะมีทางเลือกอื่นจริงหรือ? ถ้าต้องบังคับให้เช็กตามเวอร์ชันเบราว์เซอร์เหมือนสมัย IE5 ก็คงแย่กว่าเดิม
อิจฉา วิธีจัดการเวลา ของ 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 })ได้ง่าย ๆ ก็คงดีมากอีกทั้งการโอเวอร์โหลด core library เป็นแนวทางที่ดีจริงไหมก็ยังเป็นประเด็นถกเถียงได้
น่าเสียดายที่ Safari ยังไม่รองรับ Temporal API
หวังว่าปีหน้าจะรองรับ
แม้ JavaScript Date จะมีปัญหามาก แต่ก็คิดว่าการเป็น ออบเจ็กต์ เองไม่ใช่ปัญหาใหญ่
ถ้าเป็น immutable object ก็คงดีกว่า แต่จะตกใจที่ mutable object ถูกเปลี่ยนค่าก็คงไม่ใช่เรื่อง
อันตรายที่แท้จริงของความเปลี่ยนแปลงได้อยู่ที่ การเปลี่ยนแปลงจากภายนอกบริบท มากกว่า
รู้สึกไม่สะดวกที่ Temporal API ไม่จัดการข้อมูล leap second เลย
อยากทำเครื่องมือ JS สำหรับการคำนวณทางดาราศาสตร์ แต่การแปลง UTC ต้องใช้ข้อมูล leap second
มีทางอ้อมอย่าง
temporal-taiก็จริง แต่ก็ยุ่งยากเพราะต้อง ดูแลไฟล์ leap second ฝั่งไคลเอนต์และเพราะ SOP (นโยบาย CORS) จึงไม่สามารถดึงไฟล์จากเว็บภายนอกได้ตรง ๆ
เบราว์เซอร์ก็อัปเดตเป็นระยะอยู่แล้ว จึงสงสัยว่าทำไมไม่ฝังข้อมูล leap second มาให้
ถ้าเซิร์ฟเวอร์ตั้งค่า header
Access-Control-Allow-Originหรือให้มาในรูปไฟล์ JS ก็ทำได้เพียงแต่การที่เบราว์เซอร์จะรวมและดูแลข้อมูล leap second เองอาจเป็นงานที่ มีต้นทุนสูง
UTC โดยตัวมันเองรวม leap second อยู่แล้ว ดังนั้นจริง ๆ ควรมองว่ามันรองรับเพียง เวลาแบบ POSIX
ในตัวอย่างโค้ดควรบอกว่าเริ่มตีความเป็นยุค 1900 ตั้งแต่
"50"ไม่ใช่"33"— เป็นเพียงการ ชี้ว่าพิมพ์ผิดกำลังใช้ Temporal polyfill อยู่ และจนถึงตอนนี้พอใจมาก
สำหรับเซิร์ฟเวอร์หรือแอปขนาดใหญ่ถือว่าโอเค แต่กับแอปเล็กอาจเป็นภาระ
น่าแปลกที่บทความไม่พูดถึง
Date.now()เลยถ้าจะเทียบกับ Temporal ก็ควรอธิบายโดยยึด
Date.now()เป็นฐานฟังก์ชันนี้คืนค่า เวลาที่ผ่านไปเป็นมิลลิวินาทีนับจาก 1 มกราคม 1970
แม้ Temporal จะมี API ที่เป็นมิตรกว่า แต่แก่นแท้ก็ยังมีเป้าหมายเพื่อแสดง ระยะห่างสัมพัทธ์ของเวลา
ถ้าอย่างนั้นจะมีวิธีแปลงเป็นรูปแบบที่ต้องการโดยไม่ต้องผ่าน Date หรือไม่ก็น่าสงสัย
มีการทักท้วงเล็กน้อยแต่สำคัญว่า ควรใช้ “daylight saving time” ไม่ใช่ “daylight savings time”
ไม่เคยรู้มาก่อนเลยว่า JS Date จะเละเทะขนาดนี้
ถ้าตอนนั้นรอบคอบกว่านี้อีกนิด นักพัฒนาจำนวนมากก็คงไม่ต้องมาตกหลุมพรางแบบนี้