作者:JavaScript时间专家
技术难度:中高级
引言:告别Date对象的时代
在JavaScript开发中,日期时间处理一直是开发者的痛点。传统的Date对象存在诸多问题:月份从0开始、时区处理混乱、API设计不合理等。经过多年的标准化工作,Temporal API终于进入Stage 3,成为2024年最值得关注的JavaScript新特性之一。
本文将带你全面掌握Temporal API,通过实际案例展示如何构建现代化、可靠的时间处理系统。
传统Date对象的核心问题
1. 反直觉的月份索引
// 传统Date的困惑
const date = new Date(2024, 0, 1); // 月份从0开始!
console.log(date.getMonth()); // 0 代表一月
// 更糟糕的是
const feb = new Date(2024, 1, 29); // 这到底是2月29日还是3月1日?
2. 可变性带来的隐患
const original = new Date('2024-01-15');
const modified = original;
modified.setMonth(modified.getMonth() + 1);
console.log(original.getMonth()); // 也被修改了!
console.log(modified.getMonth()); // 1
3. 时区处理的混乱
const date = new Date('2024-01-15T10:30:00');
console.log(date.toString()); // 本地时间
console.log(date.toISOString()); // UTC时间
console.log(date.getHours()); // 本地小时
console.log(date.getUTCHours()); // UTC小时
// 同一时间在不同时区显示不同结果
Temporal API基础概念
1. 核心类型介绍
- Temporal.PlainDate – 仅日期(无时间、无时区)
- Temporal.PlainTime – 仅时间(无日期、无时区)
- Temporal.PlainDateTime – 日期+时间(无时区)
- Temporal.ZonedDateTime – 完整的日期时间+时区
- Temporal.Instant – 时间点(类似时间戳)
- Temporal.Duration – 时间段
- Temporal.Calendar – 日历系统
- Temporal.TimeZone – 时区信息
2. 不可变性设计
// Temporal所有对象都是不可变的
const date1 = Temporal.PlainDate.from('2024-01-15');
const date2 = date1.add({ months: 1 });
console.log(date1.toString()); // 2024-01-15
console.log(date2.toString()); // 2024-02-15
// date1保持不变!
3. 人性化的月份处理
// 月份从1开始!
const date = Temporal.PlainDate.from({
year: 2024,
month: 1, // 一月!
day: 15
});
console.log(date.month); // 1
console.log(date.monthCode); // 'M01'
实战教程:构建国际化会议调度系统
案例目标
创建一个支持多时区的会议调度系统,包含:
- 会议时间创建与验证
- 自动时区转换
- 重复会议规则
- 时间冲突检测
- 人性化时间显示
步骤1:环境准备与Polyfill
// 安装Temporal polyfill
// npm install @js-temporal/polyfill
import { Temporal } from '@js-temporal/polyfill';
// 或者使用CDN
// <script src="https://cdn.jsdelivr.net/npm/@js-temporal/polyfill/dist/index.umd.js"></script>
步骤2:会议时间模型
class MeetingScheduler {
constructor() {
this.meetings = new Map();
this.timeZone = Temporal.TimeZone.from('Asia/Shanghai');
}
// 创建会议
createMeeting(title, startTime, duration, timeZone = 'Asia/Shanghai') {
const meetingId = crypto.randomUUID();
// 解析输入时间
const zonedStart = Temporal.ZonedDateTime.from({
timeZone,
year: startTime.year,
month: startTime.month,
day: startTime.day,
hour: startTime.hour,
minute: startTime.minute,
second: 0
});
// 计算结束时间
const durationObj = Temporal.Duration.from(duration);
const zonedEnd = zonedStart.add(durationObj);
const meeting = {
id: meetingId,
title,
start: zonedStart,
end: zonedEnd,
duration: durationObj,
timeZone,
participants: new Set(),
recurrence: null
};
this.meetings.set(meetingId, meeting);
return meeting;
}
// 检查时间冲突
checkConflict(newMeeting) {
for (const [, existingMeeting] of this.meetings) {
if (this.meetingsOverlap(newMeeting, existingMeeting)) {
return {
conflict: true,
with: existingMeeting,
message: `与会议"${existingMeeting.title}"时间冲突`
};
}
}
return { conflict: false };
}
// 判断会议是否重叠
meetingsOverlap(meetingA, meetingB) {
return (
meetingA.start.epochMilliseconds meetingB.start.epochMilliseconds
);
}
}
步骤3:时区转换与显示
class TimeZoneConverter {
// 转换到目标时区
static convertToTimeZone(zonedDateTime, targetTimeZone) {
const targetTZ = Temporal.TimeZone.from(targetTimeZone);
return zonedDateTime.withTimeZone(targetTZ);
}
// 获取所有参与者的本地时间
static getLocalTimesForParticipants(meeting, participants) {
const localTimes = new Map();
for (const participant of participants) {
const localStart = this.convertToTimeZone(
meeting.start,
participant.timeZone
);
const localEnd = this.convertToTimeZone(
meeting.end,
participant.timeZone
);
localTimes.set(participant.id, {
start: localStart,
end: localEnd,
formatted: this.formatForDisplay(localStart, localEnd)
});
}
return localTimes;
}
// 人性化时间格式化
static formatForDisplay(start, end) {
const formatter = new Intl.DateTimeFormat('zh-CN', {
dateStyle: 'full',
timeStyle: 'short',
timeZone: start.timeZone.id
});
const startFormatted = formatter.format(start.toInstant());
const endFormatted = formatter.format(end.toInstant());
return `${startFormatted} - ${endFormatted}`;
}
// 计算最佳会议时间(考虑所有参与者的工作时间)
static findOptimalTime(participants, duration, workingHours = { start: 9, end: 17 }) {
// 获取所有参与者的共同可用时间
const commonSlots = this.findCommonAvailability(participants);
// 转换为持续时间
const meetingDuration = Temporal.Duration.from(duration);
// 寻找合适的时段
for (const slot of commonSlots) {
const slotDuration = slot.end.since(slot.start);
if (slotDuration.total('minutes') >= meetingDuration.total('minutes')) {
// 在工作时间内
const slotStartHour = slot.start.hour;
if (slotStartHour >= workingHours.start &&
slotStartHour <= workingHours.end - meetingDuration.hours) {
return slot.start;
}
}
}
return null;
}
}
步骤4:重复会议规则
class RecurrenceRule {
constructor(pattern) {
this.pattern = pattern; // daily, weekly, monthly, yearly
this.interval = pattern.interval || 1;
this.daysOfWeek = pattern.daysOfWeek; // [1, 3, 5] 周一、三、五
this.endDate = pattern.endDate ?
Temporal.PlainDate.from(pattern.endDate) : null;
this.occurrenceCount = pattern.occurrenceCount || null;
}
// 生成重复会议时间
generateOccurrences(startDate, count = 10) {
const occurrences = [];
let current = Temporal.PlainDate.from(startDate);
let generated = 0;
while (generated this.endDate.epochMilliseconds) {
break;
}
if (this.occurrenceCount && generated >= this.occurrenceCount) {
break;
}
if (this.isValidOccurrence(current)) {
occurrences.push(current);
generated++;
}
current = this.getNextDate(current);
}
return occurrences;
}
// 检查日期是否符合规则
isValidOccurrence(date) {
if (this.daysOfWeek) {
const dayOfWeek = date.dayOfWeek; // 1-7,1=周一
return this.daysOfWeek.includes(dayOfWeek);
}
return true;
}
// 获取下一个日期
getNextDate(date) {
switch (this.pattern.frequency) {
case 'daily':
return date.add({ days: this.interval });
case 'weekly':
return date.add({ weeks: this.interval });
case 'monthly':
return date.add({ months: this.interval });
case 'yearly':
return date.add({ years: this.interval });
default:
return date.add({ days: 1 });
}
}
// 计算会议系列的总持续时间
calculateTotalDuration(startTime, duration, endDate) {
const occurrences = this.generateOccurrences(
startTime.toPlainDate(),
1000 // 生成足够多的次数
).filter(date => {
return !endDate || date.epochMilliseconds <= endDate.epochMilliseconds;
});
const meetingDuration = Temporal.Duration.from(duration);
return meetingDuration.multiply(occurrences.length);
}
}
步骤5:完整系统集成
class CompleteMeetingSystem {
constructor() {
this.scheduler = new MeetingScheduler();
this.converter = new TimeZoneConverter();
this.recurrenceRules = new Map();
}
// 创建重复会议
createRecurringMeeting(title, startTime, duration, recurrencePattern, participants) {
const rule = new RecurrenceRule(recurrencePattern);
const occurrences = rule.generateOccurrences(startTime.toPlainDate());
const meetings = [];
for (const occurrenceDate of occurrences) {
// 将日期转换为完整的日期时间
const occurrenceDateTime = occurrenceDate.toZonedDateTime({
timeZone: startTime.timeZone,
plainTime: startTime.toPlainTime()
});
const meeting = this.scheduler.createMeeting(
title,
{
year: occurrenceDateTime.year,
month: occurrenceDateTime.month,
day: occurrenceDateTime.day,
hour: occurrenceDateTime.hour,
minute: occurrenceDateTime.minute
},
duration,
startTime.timeZone.id
);
// 添加参与者
for (const participant of participants) {
meeting.participants.add(participant);
}
meetings.push(meeting);
}
this.recurrenceRules.set(meetings[0].id, rule);
return meetings;
}
// 获取参与者的日程视图
getParticipantSchedule(participantId, startDate, endDate) {
const schedule = [];
const participantTZ = this.getParticipantTimeZone(participantId);
for (const [, meeting] of this.scheduler.meetings) {
if (meeting.participants.has(participantId)) {
// 转换到参与者时区
const localStart = this.converter.convertToTimeZone(
meeting.start,
participantTZ
);
const localEnd = this.converter.convertToTimeZone(
meeting.end,
participantTZ
);
// 检查是否在查询时间范围内
if (localStart.epochMilliseconds >= startDate.epochMilliseconds &&
localEnd.epochMilliseconds
a.localStart.epochMilliseconds - b.localStart.epochMilliseconds
);
return schedule;
}
// 检测时间重叠(考虑所有参与者的时区)
detectCrossTimeZoneOverlaps() {
const overlaps = [];
const meetings = Array.from(this.scheduler.meetings.values());
for (let i = 0; i < meetings.length; i++) {
for (let j = i + 1; j 0) {
// 对每个共同参与者检查时间冲突
for (const participant of commonParticipants) {
const participantTZ = participant.timeZone;
const aLocal = this.converter.convertToTimeZone(
meetingA.start,
participantTZ
);
const bLocal = this.converter.convertToTimeZone(
meetingB.start,
participantTZ
);
const aEndLocal = aLocal.add(meetingA.duration);
const bEndLocal = bLocal.add(meetingB.duration);
if (this.scheduler.meetingsOverlap(
{ start: aLocal, end: aEndLocal },
{ start: bLocal, end: bEndLocal }
)) {
overlaps.push({
participant,
meetings: [meetingA, meetingB],
localTimes: {
a: { start: aLocal, end: aEndLocal },
b: { start: bLocal, end: bEndLocal }
}
});
}
}
}
}
}
return overlaps;
}
}
高级应用:时间计算与业务逻辑
1. 精确的时间差计算
class TimeCalculations {
// 计算两个日期之间的工作日天数
static calculateBusinessDays(startDate, endDate, holidays = []) {
let businessDays = 0;
let current = Temporal.PlainDate.from(startDate);
const end = Temporal.PlainDate.from(endDate);
while (current.epochMilliseconds <= end.epochMilliseconds) {
// 跳过周末
if (current.dayOfWeek
holiday.equals(current)
);
if (!isHoliday) {
businessDays++;
}
}
current = current.add({ days: 1 });
}
return businessDays;
}
// 计算年龄(精确到天)
static calculateAge(birthDate, referenceDate = Temporal.Now.plainDateISO()) {
const birth = Temporal.PlainDate.from(birthDate);
const reference = Temporal.PlainDate.from(referenceDate);
let years = reference.year - birth.year;
let months = reference.month - birth.month;
let days = reference.day - birth.day;
// 处理负值
if (days < 0) {
months--;
// 获取上个月的天数
const lastMonth = reference.subtract({ months: 1 });
days += lastMonth.daysInMonth;
}
if (months < 0) {
years--;
months += 12;
}
return { years, months, days };
}
// 生成时间序列
static generateTimeSeries(start, end, interval) {
const series = [];
let current = Temporal.Instant.from(start);
const endInstant = Temporal.Instant.from(end);
const intervalDuration = Temporal.Duration.from(interval);
while (current.epochMilliseconds <= endInstant.epochMilliseconds) {
series.push(current);
current = current.add(intervalDuration);
}
return series;
}
}
2. 性能优化与内存管理
class TemporalPerformance {
// 批量处理时间数据
static processInBatches(dates, batchSize, processor) {
const results = [];
for (let i = 0; i
processor(Temporal.PlainDate.from(date))
);
results.push(...batchResults);
}
return results;
}
// 使用缓存优化重复计算
static createCachedConverter() {
const cache = new Map();
return function convertWithCache(zonedDateTime, targetTimeZone) {
const cacheKey = `${zonedDateTime.toString()}|${targetTimeZone}`;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = TimeZoneConverter.convertToTimeZone(
zonedDateTime,
targetTimeZone
);
cache.set(cacheKey, result);
// 限制缓存大小
if (cache.size > 1000) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
return result;
};
}
}
从Date迁移到Temporal的实用指南
1. 渐进式迁移策略
// 包装函数,逐步替换
class DateMigration {
// 将Date转换为Temporal
static dateToTemporal(date) {
if (date instanceof Date) {
return Temporal.Instant.from(date.toISOString());
}
return date;
}
// 将Temporal转换为Date(向后兼容)
static temporalToDate(temporal) {
if (temporal instanceof Temporal.Instant) {
return new Date(temporal.epochMilliseconds);
}
if (temporal instanceof Temporal.ZonedDateTime) {
return new Date(temporal.epochMilliseconds);
}
return temporal;
}
// 混合使用期间的适配器
static createAdapter(oldDateFunction) {
return function(...args) {
// 将Temporal参数转换为Date
const dateArgs = args.map(arg => {
if (arg instanceof Temporal.Instant ||
arg instanceof Temporal.ZonedDateTime) {
return new Date(arg.epochMilliseconds);
}
return arg;
});
const result = oldDateFunction.apply(this, dateArgs);
// 将Date结果转换回Temporal
if (result instanceof Date) {
return Temporal.Instant.from(result.toISOString());
}
return result;
};
}
}
2. 常见模式转换
// 1. 获取当前时间
// 旧的:
const now = new Date();
// 新的:
const nowInstant = Temporal.Now.instant();
const nowZoned = Temporal.Now.zonedDateTimeISO();
const nowPlain = Temporal.Now.plainDateISO();
// 2. 格式化日期
// 旧的:
const formatted = date.toLocaleDateString('zh-CN', options);
// 新的:
const formatter = new Intl.DateTimeFormat('zh-CN', options);
const formatted = formatter.format(temporal.toInstant());
// 3. 日期计算
// 旧的:
date.setMonth(date.getMonth() + 1); // 会修改原对象!
// 新的:
const newDate = date.add({ months: 1 }); // 返回新对象
// 4. 比较日期
// 旧的:
const isAfter = date1 > date2;
// 新的:
const isAfter = Temporal.Instant.compare(date1, date2) > 0;
// 或者
const isAfter = date1.epochMilliseconds > date2.epochMilliseconds;
3. 测试策略
// 创建可预测的时间测试
class TemporalTestUtils {
static createMockNow(fixedTime) {
const originalNow = Temporal.Now;
Temporal.Now = {
instant() {
return Temporal.Instant.from(fixedTime);
},
zonedDateTimeISO(timeZone) {
return Temporal.ZonedDateTime.from({
timeZone: timeZone || 'UTC',
...Temporal.PlainDateTime.from(fixedTime)
});
},
plainDateISO() {
return Temporal.PlainDate.from(fixedTime);
}
};
return () => {
Temporal.Now = originalNow;
};
}
// 测试时间相关的业务逻辑
static testBusinessLogic() {
const restoreNow = this.createMockNow('2024-01-15T10:30:00Z');
try {
// 执行测试
const result = someTimeDependentFunction();
// 断言结果
assert(result === expectedValue);
} finally {
restoreNow();
}
}
}
// 添加代码复制功能
document.addEventListener(‘DOMContentLoaded’, () => {
const codeBlocks = document.querySelectorAll(‘pre’);
codeBlocks.forEach(block => {
// 添加复制按钮
const copyButton = document.createElement(‘button’);
copyButton.textContent = ‘复制’;
copyButton.style.cssText = `
position: absolute;
top: 8px;
right: 8px;
padding: 4px 8px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
opacity: 0.8;
transition: opacity 0.2s;
`;
block.style.position = ‘relative’;
block.style.paddingTop = ’32px’;
block.appendChild(copyButton);
copyButton.addEventListener(‘click’, async () => {
const code = block.textContent.replace(‘复制’, ”).trim();
try {
await navigator.clipboard.writeText(code);
copyButton.textContent = ‘已复制!’;
copyButton.style.background = ‘#45a049’;
setTimeout(() => {
copyButton.textContent = ‘复制’;
copyButton.style.background = ‘#4CAF50’;
}, 2000);
} catch (err) {
console.error(‘复制失败:’, err);
copyButton.textContent = ‘复制失败’;
copyButton.style.background = ‘#f44336’;
}
});
// 悬停效果
block.addEventListener(‘mouseenter’, () => {
copyButton.style.opacity = ‘1’;
});
block.addEventListener(‘mouseleave’, () => {
copyButton.style.opacity = ‘0.8’;
});
});
// 平滑滚动
document.querySelectorAll(‘nav a[href^=”#”]’).forEach(anchor => {
anchor.addEventListener(‘click’, function(e) {
e.preventDefault();
const targetId = this.getAttribute(‘href’);
if (targetId === ‘#’) return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: ‘smooth’,
block: ‘start’
});
// 更新URL
history.pushState(null, ”, targetId);
}
});
});
// 添加阅读进度指示器
const progressBar = document.createElement(‘div’);
progressBar.style.cssText = `
position: fixed;
top: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, #2196F3, #4CAF50);
width: 0%;
z-index: 9999;
transition: width 0.1s ease;
`;
document.body.appendChild(progressBar);
// 更新阅读进度
window.addEventListener(‘scroll’, () => {
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
const height = document.documentElement.scrollHeight – document.documentElement.clientHeight;
const scrolled = (winScroll / height) * 100;
progressBar.style.width = scrolled + ‘%’;
});
// 高亮当前阅读部分
const observerOptions = {
root: null,
rootMargin: ‘-20% 0px -70% 0px’,
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 移除所有高亮
document.querySelectorAll(‘section’).forEach(section => {
section.style.boxShadow = ‘none’;
});
// 高亮当前部分
entry.target.style.boxShadow = ‘inset 4px 0 0 #2196F3’;
}
});
}, observerOptions);
// 观察所有章节
document.querySelectorAll(‘section’).forEach(section => {
observer.observe(section);
});
});

