引言:告别 Date 的痛点
JavaScript 中的日期和时间处理一直是开发者的痛点。自 1995 年 JavaScript 诞生以来,Date 对象就伴随着各种设计缺陷,给开发者带来了无尽的困扰。为了解决这些问题,TC39 委员会提出了 Temporal API 提案,旨在为 JavaScript 提供一个更现代、更强大、更直观的日期和时间处理方案。
本文将深入探讨 Temporal API 的设计理念、核心特性、使用方法,并与现有的 Date 对象以及流行的第三方库进行对比,帮助你全面了解这一即将到来的重要 JavaScript 特性。
Temporal API 的核心概念与设计理念
Temporal API 是 ECMAScript 的一个 Stage 3 提案,由 TC39 委员会推动,目前正在等待最终的标准化和浏览器实现。它的设计目标是提供一个全面的、现代化的日期和时间 API,以替代现有的 Date 对象。
Temporal 的设计原则
-
所有 Temporal 对象都是不可变的:这意味着所有操作都会返回新的对象,而不是修改原始对象,避免了意外的副作用。
-
日期值可以在不同的日历系统中表示:Temporal 支持多种日历系统,但它们都可以与公历(Proleptic Gregorian Calendar)相互转换。
-
所有时间值都基于标准的 24 小时制:提供统一的时间表示方式。
-
不表示闰秒:简化了时间计算,避免了闰秒带来的复杂性。
Temporal 的核心类型
Temporal API 提供了多种专门的类型,每种类型都针对特定的日期和时间处理场景:
-
Temporal.Instant:表示时间线上的一个精确时刻,不考虑日历或位置,例如 1969 年 7 月 20 日 20:17 UTC。
-
Temporal.ZonedDateTime:带有时区的日期时间对象,表示在地球上特定区域发生的真实事件,例如美国太平洋时间 1995 年 12 月 7 日凌晨 3:24(格里高利历)。
-
Temporal.PlainDate:表示不与特定时间或时区关联的日历日期,例如 2006 年 8 月 24 日。
-
Temporal.PlainTime:表示不与特定日期或时区关联的挂钟时间,例如下午 7:39。
-
Temporal.PlainDateTime:表示不与特定时区关联的日期和时间组合。
-
Temporal.PlainYearMonth:表示特定年份和月份,不包含日期信息。
-
Temporal.PlainMonthDay:表示特定月份和日期,不包含年份信息。
-
Temporal.Duration:表示两个时间点之间的持续时间。
-
Temporal.Now:提供获取当前时间的各种方法。
Temporal API 与 Date 对象的深度对比
Date 对象的主要问题
- 可变性导致的副作用:Date 对象是可变的,这意味着方法调用会修改原始对象,容易导致意外的副作用。
// Date 对象的可变性问题
function addOneWeek(myDate) {
myDate.setDate(myDate.getDate() + 7);
return myDate;
}
const today = new Date();
const oneWeekFromNow = addOneWeek(today);
console.log(`today is ${today.toLocaleString()}, and one week from today will be ${oneWeekFromNow.toLocaleString()}`);
// today 和 oneWeekFromNow 是相同的,因为 today 被修改了
- 混乱的月份编号:月份从 0 开始(0-11),而日期从 1 开始(1-31),这种不一致性容易导致错误。
// 混乱的月份编号
const date = new Date(2024, 0, 1); // 2024年1月1日
console.log(date.getMonth()); // 0(表示1月)
-
有限的时区支持:Date 对象主要依赖系统的本地时区,对多时区处理支持有限。
-
解析行为不可靠:Date 对象的解析行为在不同浏览器中可能不一致。
-
不支持非格里高利历:无法处理其他日历系统。
Temporal API 的解决方案
- 不可变性:所有操作都返回新对象,避免副作用。
// Temporal 的不可变性
const date = Temporal.PlainDate.from('2024-01-01');
const nextMonth = date.add({ months: 1 });
console.log(date.toString()); // '2024-01-01' - 原始对象不变
console.log(nextMonth.toString()); // '2024-02-01' - 新对象
- 一致的索引:所有单位都使用基于 1 的编号,更符合直觉。
// 一致的索引
const date = Temporal.PlainDate.from({ year: 2024, month: 1, day: 1 }); // 2024年1月1日
console.log(date.month); // 1(表示1月)
- 强大的时区支持:明确的时区处理,支持所有 IANA 时区。
// 明确的时区处理
const nyDateTime = Temporal.ZonedDateTime.from({
timeZone: 'America/New_York',
year: 2024,
month: 1,
day: 1,
hour: 9
});
const tokyoDateTime = nyDateTime.withTimeZone('Asia/Tokyo');
console.log(tokyoDateTime.toString()); // 显示东京时间,自动处理时区转换
-
可靠的解析:严格指定的字符串格式,确保一致的解析行为。
-
支持非格里高利历:支持多种日历系统。
Temporal API 与主流时间处理库的对比
与 Moment.js 的对比
Moment.js 曾是 JavaScript 中最流行的日期处理库,但现在已经进入维护模式,不再积极开发。
相似点:
- 都提供丰富的日期和时间操作方法
- 都支持时区处理
- 都支持格式化和解析
区别:
- Temporal 是不可变的,而 Moment.js 是可变的
- Temporal 将成为语言内置特性,不需要额外引入库
- Temporal 支持更精确的时间(纳秒级别)
- Temporal 提供更清晰的类型区分(PlainDate、PlainTime 等)
// Moment.js
moment().add(7, 'days');
// Temporal
Temporal.Now.plainDateTimeISO().add({ days: 7 });
与 date-fns 的对比
date-fns 是一个函数式编程风格的日期库,支持 tree-shaking。
相似点:
- 都采用不可变的设计理念
- 都提供丰富的日期和时间操作方法
区别:
- date-fns 使用函数式 API,而 Temporal 使用面向对象的 API
- Temporal 将成为语言内置特性,不需要额外引入库
- Temporal 提供更强大的时区支持
- Temporal 支持更多的日历系统
// date-fns
import addDays from 'date-fns/addDays';
addDays(new Date(), 7);
// Temporal
Temporal.Now.plainDateTimeISO().add({ days: 7 });
与 Luxon 的对比
Luxon 是由 Moment.js 的一些维护者开发的,旨在提供更现代的 API。
相似点:
- 都是不可变的
- 都提供丰富的日期和时间操作方法
- 都有强大的时区支持
区别:
- Temporal 将成为语言内置特性,不需要额外引入库
- Temporal 提供更清晰的类型区分
- Temporal 支持更多的日历系统
// Luxon
DateTime.local().plus({ days: 7 });
// Temporal
Temporal.Now.plainDateTimeISO().add({ days: 7 });
与 Day.js 的对比
Day.js 是一个轻量级的日期库,API 与 Moment.js 类似,但体积更小。
相似点:
- 都提供丰富的日期和时间操作方法
- 都支持链式调用
区别:
- Temporal 是不可变的,而 Day.js 的某些操作是可变的
- Temporal 将成为语言内置特性,不需要额外引入库
- Temporal 提供更强大的时区支持
- Temporal 支持更多的日历系统
// Day.js
dayjs().add(7, 'day');
// Temporal
Temporal.Now.plainDateTimeISO().add({ days: 7 });
特性对比表
下表汇总了 Temporal API 与 Date 对象以及主流时间处理库的主要差异:
| 特性 | Date | Temporal | Moment.js | date-fns | Luxon | Day.js |
|---|---|---|---|---|---|---|
| 不可变性 | ❌ | ✅ | ❌ | ✅ | ✅ | 部分支持 |
| 时区支持 | 有限 | 完整 | 完整 | 有限 | 完整 | 插件支持 |
| 国际化支持 | 有限 | 完整 | 完整 | 有限 | 完整 | 插件支持 |
| 日历系统 | 仅格里高利历 | 多种日历 | 仅格里高利历 | 仅格里高利历 | 仅格里高利历 | 仅格里高利历 |
| 精确度 | 毫秒 | 纳秒 | 毫秒 | 毫秒 | 毫秒 | 毫秒 |
| 类型区分 | 单一类型 | 多种类型 | 单一类型 | 函数式 | 少量类型 | 单一类型 |
| 语言内置 | ✅ | 即将支持 | ❌ | ❌ | ❌ | ❌ |
| 包大小 | 0 | 0 | 大 | 小(可tree-shaking) | 中 | 小 |
| API风格 | 面向对象 | 面向对象 | 面向对象 | 函数式 | 面向对象 | 面向对象 |
| 维护状态 | 活跃 | 活跃(提案) | 维护模式 | 活跃 | 活跃 | 活跃 |
| 解析可靠性 | 低 | 高 | 中 | 高 | 高 | 中 |
这个对比表清晰地展示了 Temporal API 相比于 Date 对象和其他库的优势,尤其是在不可变性、时区支持、日历系统和类型区分方面。作为未来的语言内置特性,Temporal API 将为 JavaScript 开发者提供更强大、更可靠的日期和时间处理能力。
Temporal API 的常用场景和使用示例
创建日期和时间对象
// 创建日期对象
const date = Temporal.PlainDate.from('2024-01-01');
console.log(date.toString()); // '2024-01-01'
// 创建时间对象
const time = Temporal.PlainTime.from('09:00:00');
console.log(time.toString()); // '09:00:00'
// 创建日期时间对象
const dateTime = Temporal.PlainDateTime.from('2024-01-01T09:00:00');
console.log(dateTime.toString()); // '2024-01-01T09:00:00'
// 创建带时区的日期时间对象
const zonedDateTime = Temporal.ZonedDateTime.from('2024-01-01T09:00:00[America/New_York]');
console.log(zonedDateTime.toString()); // '2024-01-01T09:00:00-05:00[America/New_York]'
// 创建持续时间对象
const duration = Temporal.Duration.from({ hours: 1, minutes: 30 });
console.log(duration.toString()); // 'PT1H30M'
获取当前日期和时间
// 获取当前时刻(UTC)
const now = Temporal.Now.instant();
console.log(now.toString()); // 例如 '2024-01-01T12:00:00Z'
// 获取当前日期(本地)
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // 例如 '2024-01-01'
// 获取当前时间(本地)
const currentTime = Temporal.Now.plainTimeISO();
console.log(currentTime.toString()); // 例如 '12:00:00'
// 获取当前日期和时间(本地)
const currentDateTime = Temporal.Now.plainDateTimeISO();
console.log(currentDateTime.toString()); // 例如 '2024-01-01T12:00:00'
// 获取当前带时区的日期和时间
const currentZonedDateTime = Temporal.Now.zonedDateTimeISO();
console.log(currentZonedDateTime.toString()); // 例如 '2024-01-01T12:00:00+08:00[Asia/Shanghai]'
日期和时间运算
// 日期加法
const date = Temporal.PlainDate.from('2024-01-01');
const futureDate = date.add({ days: 10 });
console.log(futureDate.toString()); // '2024-01-11'
// 日期减法
const pastDate = date.subtract({ months: 1 });
console.log(pastDate.toString()); // '2023-12-01'
// 使用持续时间进行加法
const duration = Temporal.Duration.from({ days: 5 });
const anotherFutureDate = date.add(duration);
console.log(anotherFutureDate.toString()); // '2024-01-06'
// 计算两个日期之间的差异
const date1 = Temporal.PlainDate.from('2024-01-01');
const date2 = Temporal.PlainDate.from('2024-01-10');
const difference = date1.until(date2);
console.log(difference.toString()); // 'P9D'(9天)
console.log(difference.days); // 9
时区处理
// 创建带时区的日期时间对象
const londonTime = Temporal.ZonedDateTime.from({
timeZone: 'Europe/London',
year: 2024,
month: 1,
day: 1,
hour: 9
});
console.log(londonTime.toString()); // '2024-01-01T09:00:00+00:00[Europe/London]'
// 转换到另一个时区
const tokyoTime = londonTime.withTimeZone('Asia/Tokyo');
console.log(tokyoTime.toString()); // '2024-01-01T18:00:00+09:00[Asia/Tokyo]'
// 获取当前时区
const currentTimeZone = Temporal.Now.timeZoneId();
console.log(currentTimeZone); // 例如 'Asia/Shanghai'
格式化日期和时间
// 基本格式化
const date = Temporal.PlainDate.from('2024-01-01');
console.log(date.toString()); // '2024-01-01'
// 使用 toLocaleString 进行本地化格式化
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
console.log(date.toLocaleString('zh-CN', options)); // '2024年1月1日星期一'
// 自定义格式化
const dateTime = Temporal.PlainDateTime.from('2024-01-01T09:00:00');
console.log(dateTime.toLocaleString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})); // '2024年1月1日 09:00'
日历系统
// 创建使用特定日历系统的日期
const chineseDate = Temporal.PlainDate.from({
year: 2024,
month: 1,
day: 1,
calendar: 'chinese'
});
console.log(chineseDate.toString()); // '2024-01-01[u-ca=chinese]'
// 转换到另一个日历系统
const gregorianDate = chineseDate.withCalendar('iso8601');
console.log(gregorianDate.toString()); // '2024-01-01'
Temporal API 的兼容性和使用方法
当前兼容性状态
Temporal API 目前处于 TC39 提案的 Stage 3 阶段,这意味着它已经被推荐实现,但尚未成为 ECMAScript 标准的一部分。目前,主流浏览器尚未原生支持 Temporal API,但 Firefox Nightly 版本已经添加了对 Temporal 提案的实验性支持。
使用 Polyfill
在 Temporal API 被广泛支持之前,你可以使用 polyfill 来在你的项目中使用它。目前有两个主要的 polyfill 可供选择:
- @js-temporal/polyfill:由 Temporal 提案的一些拥护者开发的 polyfill。
npm install @js-temporal/polyfill
// 导入 polyfill
import { Temporal } from '@js-temporal/polyfill';
// 使用 Temporal API
const now = Temporal.Now.instant();
console.log(now.toString());
- temporal-polyfill:一个更轻量级的 polyfill,体积约为 20KB。
npm install temporal-polyfill
// 导入 polyfill
import { Temporal } from 'temporal-polyfill';
// 使用 Temporal API
const now = Temporal.Now.instant();
console.log(now.toString());
你也可以通过 CDN 在浏览器中直接使用 polyfill:
<script src="https://cdn.jsdelivr.net/npm/temporal-polyfill@0.3.0/global.min.js"></script>
<script>
const now = Temporal.Now.instant();
console.log(now.toString());
</script>
浏览器支持
使用 temporal-polyfill,最低需要的浏览器版本为:
- Chrome 60(2017年7月)
- Firefox 55(2017年8月)
- Safari 11.1(2018年3月)
- Safari iOS 11.3(2018年3月)
- Edge 79(2020年1月)
- Node.js 14(2020年4月)
如果你使用转译工具,可以支持更早的浏览器版本。
Temporal API 的未来发展
标准化进程
Temporal API 目前处于 TC39 提案的 Stage 3 阶段,这意味着它已经被推荐实现,但尚未成为 ECMAScript 标准的一部分。根据 TC39 的流程,Stage 3 之后是 Stage 4,即最终阶段,此时提案将被纳入 ECMAScript 标准。
Temporal API 的规范文本已经完成,并且正在进行实现和测试。一旦通过足够的测试和实现验证,它将进入 Stage 4,并在未来的 ECMAScript 版本中正式发布。
浏览器实现计划
目前,Firefox Nightly 版本已经添加了对 Temporal 提案的实验性支持,这是浏览器原生支持 Temporal API 的第一步。其他主流浏览器(如 Chrome、Safari 和 Edge)也在关注这一提案,并可能在未来实现它。
一旦 Temporal API 进入 Stage 4,我们可以期待主流浏览器在未来的版本中逐步实现它。然而,考虑到浏览器的发布周期和向后兼容性的考虑,可能需要一段时间才能在所有主流浏览器中看到完全的原生支持。
对 JavaScript 生态系统的影响
Temporal API 的引入将对 JavaScript 生态系统产生深远的影响:
-
减少对第三方库的依赖:随着 Temporal API 成为语言的一部分,开发者将不再需要依赖 Moment.js、date-fns、Luxon 等第三方库来处理日期和时间,这将减少项目的依赖和体积。
-
统一的日期和时间处理方式:Temporal API 提供了一套统一的、现代化的日期和时间处理方式,这将减少开发者之间的学习成本和代码差异。
-
更好的国际化支持:Temporal API 的设计考虑了国际化需求,支持多种日历系统和时区,这将使开发国际化应用程序变得更加容易。
-
更可靠的日期和时间处理:Temporal API 的不可变设计和明确的类型区分将减少日期和时间处理中的常见错误,提高代码的可靠性。
结论
Temporal API 代表了 JavaScript 日期和时间处理的未来。它解决了 Date 对象的许多痛点,提供了更现代、更强大、更直观的 API,使日期和时间处理变得更加简单和可靠。
虽然 Temporal API 尚未成为 ECMAScript 标准的一部分,但你可以通过 polyfill 在你的项目中开始使用它,为未来的标准化做好准备。随着浏览器对 Temporal API 的原生支持逐步增加,我们可以期待在不久的将来看到它成为 JavaScript 开发的标准工具。
无论你是正在开发新项目,还是维护现有代码库,了解 Temporal API 都将帮助你为未来的 JavaScript 开发做好准备,并在日期和时间处理方面做出更明智的选择。