การบันทึกข้อมูลตามลำดับเวลาที่ควรจะเหมือนกันไม่ว่าผู้ดูจะอยู่ที่ใด เช่น วันเกิด การปลุกประจำวัน วันหมดอายุของบัตรเครดิต หรือรอบการเรียกเก็บเงินรายเดือน มักจะเกิดข้อผิดพลาดได้ง่ายเมื่อใช้ออบเจ็กต์ Date เดิม เนื่องจาก Date แสดงถึงช่วงเวลาที่เฉพาะเจาะจงเสมอและแสดงโดยใช้เขตเวลาท้องถิ่นของผู้ชม การบันทึกวันที่ เช่น "1990-01-01" อาจส่งผลให้ผู้ใช้ที่อยู่ทางตะวันตกของ UTC เห็น "1989-12-31" ในขณะที่การจำลองวันที่บางส่วน (เช่น วันหมดอายุของบัตรที่ระบุเฉพาะเดือน) จะบังคับให้ใช้วันที่ที่กำหนดเองเป็นตัวยึดตำแหน่ง ซึ่งทำให้เกิดข้อบกพร่องด้านโอเวอร์โฟลว์
Temporal API มีประเภท "ธรรมดา" ที่ไม่ขึ้นอยู่กับสถานที่ตั้ง ซึ่งไม่มีการชดเชยเขตเวลาและแสดงวันที่ในปฏิทินและเวลาตามนาฬิกาติดผนังตามที่อ่านจากปฏิทินหรือนาฬิกา
| ประเภท | สิ่งที่โมเดลจำลอง | กรณีการใช้งานทั่วไป |
|---|---|---|
Temporal.PlainDate |
วันที่แบบเต็มในปฏิทิน (year, month, day) |
วันเกิด วันที่จัดกิจกรรม วันที่เรียกเก็บเงินสำหรับการสมัครใช้บริการ |
Temporal.PlainTime |
เวลาตามนาฬิกา (hour, minute, second, ...) |
การปลุกทุกวัน (9:00 น. ไม่ว่าผู้ใช้จะอยู่ที่ไหน) เวลาทำการ |
Temporal.PlainDateTime |
วันที่ในปฏิทิน + เวลาตามนาฬิกาโดยไม่มีเขตเวลา | กำหนดการในพื้นที่แบบลอยตัว โดยรวม PlainDate และ PlainTime ก่อนแนบโซน |
Temporal.PlainMonthDay |
เดือนและวันโดยไม่มีปี | วันเกิด วันครบรอบ วันหยุดที่กำหนดไว้ซึ่งเกิดขึ้นซ้ำทุกปี |
Temporal.PlainYearMonth |
ปีและเดือนโดยไม่มีวัน | วันหมดอายุของบัตรเครดิต (2027-12), ช่วงเวลาที่เรียกเก็บเงินรายเดือน |
วิธีการนำไปใช้งาน
- เลือกประเภท
Temporal.Plain*ที่แคบที่สุดซึ่งตรงกับข้อมูลของคุณ:- ใช้
Temporal.PlainDate.from()สำหรับวันที่แบบเต็มในปฏิทิน และTemporal.PlainTime.from()สำหรับเวลาตามนาฬิกา - ใช้
Temporal.PlainDateTime.from()(หรือplainDate.toPlainDateTime(plainTime)) เมื่อรวมวันที่ในปฏิทินและเวลาบนนาฬิกาติดผนังโดยไม่ผูกกับเขตเวลา - ใช้
Temporal.PlainMonthDay.from()สำหรับวันที่ประจำปีที่ไม่มีปี และแปลงเป็นPlainDateด้วย.toPlainDate({ year })ก่อนดำเนินการคำนวณวันที่ - ใช้
Temporal.PlainYearMonth.from()สำหรับค่าปีและเดือน เพื่อไม่ให้ตัวเลขวันที่ที่กำหนดเองรั่วไหลไปในการคำนวณ
- ใช้
- โปรดสังเกตพารามิเตอร์
calendarเมื่อสร้างประเภท Plain: ISO 8601 ('iso8601') เป็นปฏิทินเริ่มต้นเมื่อแยกวิเคราะห์สตริง ISO หรือสร้างด้วยPlainDate.from({ year, month, day })เมื่อสร้างPlainMonthDayหรือPlainYearMonthจาก Property Bag หรือเมื่อทำงานกับปฏิทินที่ไม่ใช่ ISO ให้ส่งcalendarอย่างชัดเจน (เช่น{ month: 10, day: 31, calendar: 'iso8601' }) - ใช้
.add()กับตัวเลือกoverflowสำหรับช่วงเวลาในปฏิทินที่เกิดซ้ำ: การเรียกใช้plainDate.add({ months: 1 })จะมีค่าเริ่มต้นเป็น{ overflow: 'constrain' }ซึ่งจะจำกัดวันที่สิ้นเดือน เช่น วันที่ 31 ม.ค. เป็นวันที่ 28/29 ก.พ. ส่ง{ overflow: 'reject' }เมื่อวันที่เป้าหมายที่ไม่ถูกต้องควรแสดงRangeErrorแทนการจำกัด
ตัวอย่าง: วันที่แบบเต็ม เวลาตามนาฬิกา และ PlainDateTime
// 1. Parse a calendar date and a wall-clock time from input strings
const birthdate = Temporal.PlainDate.from('1990-01-01');
const reminderTime = Temporal.PlainTime.from('09:00');
// Combine into a location-agnostic PlainDateTime without string interpolation
const localAppointment = birthdate.toPlainDateTime(reminderTime);
// 2. Display without time zone conversion; outputs the same date in any time zone
console.log(birthdate.toLocaleString('en-GB'));
console.log(localAppointment.toString()); // "1990-01-01T09:00:00"
// 3. Read calendar fields directly from the Plain type. A PlainDate already *is*
// a calendar date, so there is no need for the formatToParts() workaround that
// legacy Date code uses to recover year/month/day from an instant.
const { year, month, day } = birthdate;
ตัวอย่าง: วันที่บางส่วน (PlainMonthDay และ PlainYearMonth)
// Annual recurring event (no year component)
// Include explicit calendar when constructing from an object
const annualHoliday = Temporal.PlainMonthDay.from({
month: 10,
day: 31,
calendar: 'iso8601',
});
const today = Temporal.Now.plainDateISO();
const isToday = annualHoliday.equals(today.toPlainMonthDay());
// Convert PlainMonthDay to a PlainDate with a specific year before doing arithmetic,
// because month lengths (such as February) depend on whether the year is a leap year.
const occurrenceThisYear = annualHoliday.toPlainDate({ year: today.year });
// Monthly expiration (no day component, preventing day-of-month overflow bugs)
const cardExpiry = Temporal.PlainYearMonth.from({
year: 2027,
month: 12,
calendar: 'iso8601',
});
const currentMonth = today.toPlainYearMonth();
const isExpired = Temporal.PlainYearMonth.compare(cardExpiry, currentMonth) < 0;
ตัวอย่าง: ช่วงเวลาที่เกิดซ้ำรายเดือน (overflow: 'constrain' เทียบกับ 'reject')
const billingStart = Temporal.PlainDate.from('2024-01-31');
// Default overflow: 'constrain' clamps Jan 31 + 1 month to the last valid day of February
const nextBilling = billingStart.add({ months: 1 }, { overflow: 'constrain' });
console.log(nextBilling.toString()); // "2024-02-29" (2024 is a leap year)
// Use overflow: 'reject' to throw a RangeError when the target day does not exist
try {
billingStart.add({ months: 1 }, { overflow: 'reject' });
} catch (err) {
// RangeError: day 31 does not exist in February
}
ตัวอย่าง: การทำงานร่วมกับ Date เดิม
Date เดิมคือการประทับเวลา (ทันที) ไม่ใช่วันที่ในปฏิทิน การจัดเก็บ "1990-01-01" เป็น Date จะบังคับให้คุณเลือกเวลาบางช่วง (โดยปกติคือเที่ยงคืนตามเวลา UTC) และการอ่านค่ากลับในเขตเวลาทางตะวันตกของ UTC จะได้ผลลัพธ์เป็น "31/12/1989" หากไลบรารีหรือ API ยังคงต้องใช้ Date ให้แปลงอย่างชัดเจนผ่าน Temporal.Instant แทนที่จะสร้างสตริง ISO ด้วยตนเอง ดังนี้
// PlainDate -> Date: attach a time zone (time of day defaults to the start of
// that day) to get an Instant, then hand the epoch milliseconds to Date.
// 'UTC' is a deliberate choice and must be documented; any zone would do,
// but the stored instant depends on it.
const instant = birthdate.toZonedDateTime('UTC').toInstant();
const legacyDate = new Date(instant.epochMilliseconds);
// Date -> PlainDate: go back through Instant (legacyDate.toTemporalInstant()
// is equivalent), then project into an explicit time zone. The result depends
// on the zone, which is exactly the drift risk.
const viewedDate = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime())
.toZonedDateTimeISO('America/New_York')
.toPlainDate();
// Compare Plain types with .equals() (or Temporal.PlainDate.compare), not strings.
console.log(viewedDate.equals(birthdate)); // false: the calendar date drifted to 1989-12-31
การนำไปใช้เชิงกลยุทธ์และแนวทางปฏิบัติแนะนำ
- ควรใช้
Temporal.PlainDateสำหรับวันที่ในปฏิทินแบบเต็ม,Temporal.PlainTimeสำหรับเวลาบนนาฬิกา,Temporal.PlainDateTimeสำหรับคู่วันที่และเวลาในท้องถิ่นที่ไม่มีโซน,Temporal.PlainMonthDayสำหรับวันที่ที่เกิดซ้ำทุกปี และTemporal.PlainYearMonthสำหรับวันหมดอายุหรือช่วงเวลาที่เรียกเก็บเงินแบบปี-เดือน - อย่าดำเนินการทางคณิตศาสตร์กับ
Temporal.PlainMonthDayโดยตรง ให้แปลงเป็นTemporal.PlainDateก่อนผ่าน.toPlainDate({ year })เนื่องจากความยาวของเดือนจะแตกต่างกันไปตามปี - ควรใช้
.add()ในTemporal.PlainDateสำหรับช่วงปฏิทินที่เกิดซ้ำ โดยอาศัยoverflow: 'constrain'(ค่าเริ่มต้น) เพื่อจำกัดวันที่สิ้นเดือน หรือoverflow: 'reject'เพื่อแสดงRangeErrorในวันที่ที่ไม่มีอยู่จริง - ควรระบุ
calendar(เช่นcalendar: 'iso8601') เมื่อสร้างPlainMonthDayหรือประเภท Plain ที่ไม่ใช่ ISO จากออบเจ็กต์ - อย่าใช้ประเภท Plain หากต้องการแสดงช่วงเวลาที่เจาะจงในเวลาจริง (ทันที) ใช้
Temporal.InstantหรือTemporal.ZonedDateTimeสำหรับบันทึก การประทับเวลาของเหตุการณ์ หรือการกำหนดเวลาข้ามเขตเวลา - อย่าสร้างออบเจ็กต์
Dateจากสตริง ISO ที่สร้างด้วยมือ (เช่น`${date}T00:00:00Z`) สร้างDateจากepochMillisecondsของTemporal.InstantหรือTemporal.ZonedDateTimeและอ่านกลับด้วยTemporal.Instant.fromEpochMilliseconds(date.getTime())หรือdate.toTemporalInstant()เพื่อให้สมมติฐานเขตเวลามีความชัดเจน - อย่าใช้
Intl.DateTimeFormat.prototype.formatToParts()เพื่อดึงหรือเปรียบเทียบค่าปี เดือน หรือวัน อ่าน.year,.monthและ.dayโดยตรงจากประเภทธรรมดา และเปรียบเทียบกับ.equals() - ควรระบุเขตเวลาตามชื่อ IANA (เช่น
America/New_York) และไม่ควรระบุตามการชดเชยเวลา UTC แบบคงที่ ค่าชดเชยจะเปลี่ยนไปตามเวลาออมแสงและกฎหมาย ดังนั้นป้ายกำกับอย่าง"UTC-5"จึงไม่ถูกต้องในบางช่วงของปี - ควรจำไว้ว่าออบเจ็กต์
Temporalเปลี่ยนแปลงไม่ได้ เมธอดอย่าง.add(),.subtract()หรือ.with()จะแสดงผลอินสแตนซ์ใหม่ที่มีการอ้างอิงใหม่เสมอ แทนที่จะเปลี่ยนแปลงออบเจ็กต์เดิมในตำแหน่งเดิม ทำให้จัดเก็บในสถานะ UI แบบรีแอ็กทีฟได้โดยตรงอย่างปลอดภัย
กลยุทธ์สำรอง
เบราว์เซอร์ที่รองรับและทางเลือกสำรองสำหรับ Temporal
สำหรับเบราว์เซอร์ที่ยังไม่รองรับ Temporal API แบบเนทีฟ ให้ใช้การตรวจหาฟีเจอร์และโหลด temporal-polyfill แบบไดนามิก (~15 KB min+gz)
โปรดทราบว่าการนำเข้า temporal-polyfill จะไม่กำหนดออบเจ็กต์ Temporal ให้กับขอบเขตส่วนกลางโดยอัตโนมัติ กำหนดอย่างชัดเจนหากโค้ดของคุณใช้ globalThis.Temporal
// Check if Temporal is supported natively
(async () => {
if (typeof Temporal === 'undefined') {
// Load the polyfill conditionally
const module = await import('https://esm.sh/temporal-polyfill');
globalThis.Temporal = module.Temporal;
// Extend Date.prototype if needed
Date.prototype.toTemporalInstant = module.toTemporalInstant;
}
initializeApp();
})();