JS 新特性:Temporal
前言
近日(2020.9.16), Moment
团队发布声明,宣布 Moment
将进入维护状态,除非必要,将不再更新:
- 未来不会新增任何新特性
- 不会改成
immutable
- 不会做
tree shakeing
,也不会解决包体积问题 - 不会更新 v3 版本
- 不会修复 bug 或非预期行为,特别是长期以来的已知问题
同时,声明中着重强调了新项目不要再使用 Moment
,并且给出了充分的理由,以及替代方案, 其中提到了一个正处于 stage2 状态的 js 新特性 Temporal
,虽然离生产还有很长的路,但是不妨提前学习下这个未来可以取代所有第三方时间库的新特性。
学习 Temporal
之前,先来看下从 JS 诞生之初就存在的 Date
有什么问题,为何重头设计一个全新的事件对象:
不支持时区:
Date
对象无法修改时区,永远以当前系统的时区为准,如果需要计算不同时区的时间,只能手动根据时区之间的时间差手动计算时间格式化不可靠:这个很好理解,应该没有一个 JSER 会觉得原生的
Date
对象格式化成YYYY-MM-DD
是个简单操作Date
对象是mutable
的:与String
对象的所有方法都返回一个新对象,不会修改原始字符串对象不同,对Date
对象的操作都会修改原始对象,导致结果难以保证夏令时行为诡异:中国有一段时间(1986-1991)实行过夏令时,所以这个时间段的时间在不同的浏览器上得到的时间戳不一致,并且没有直接判断是夏令时的 API
时间计算 API 难用至极:需要计算两个时间的间隔或者时间增减时,API 及其难用
等等
所以 TC39 工作组提出了遵循 ISO_8601 标准 的全新时间对象 Temporal
来解决这些问题,全新的对象将着眼于以下几个方面:
- 提供简单易用的 API
- 提供
immutable
对象 - 严格的时间字符串格式
- 支持非公历、支持时区
详细的 API 可以查看 TC39文档,接下来我们用实际的案例感受下 Temporal
的强大(在 Temporal
文档页面,已集成 polyfill,可在控制台直接调用)。
实践
获取/构造时间
相较于 Date
,使用 Temporal
获取/构造时间有以下优点:
- 最大精度为纳秒,可以对时间进行更精准的计算(为了配合纳秒,
Temporal
以bigInit
为基础参数) - 标准的时间构造方法
- 完整的时间静态属性
bigInit
是 JS 新特性之一,简单来讲,在number
后跟上n
即为一个bigInit
变量,比如0n
12345678900987654321n
,此处不具体展开。
// 当前系统时间
const now = Temporal.now.dateTime().toString(); // -> 2020-09-18T11:49:34.076618285
// 忽略时区的绝对时间
const now = Temporal.now.absolute().toString(); // -> 2020-09-18T03:49:34.871974076Z
// 构造距 1970-1-1 xx 纳秒的 Temporal 对象
const now = new Temporal.Absolute(0n).toString(); // -> 1970-01-01T00:00Z
// 根据 年 月 日 时 分 秒 毫秒 微妙 纳秒 构造时间 !!!
const now = new Temporal.DateTime(2020, 9, 20, 12, 15, 32, 666, 233, 999).toString(); // -> 2020-09-20T12:15:32.666233999
// 更全的时间静态属性
const now = Temporal.now.dateTime();
/* ⬇️⬇️⬇️
{
year: 2020, // 年
month: 9, // 月
day: 21, // 日
hour: 17, // 时
minute: 18, // 分
second: 7, // 秒
millisecond: 771, // 毫秒
microsecond: 785, // 微妙
nanosecond: 972, // 纳秒
dayOfWeek: 1, // 全周的第几天(从1开始)
dayOfYear: 265, // 全年的第几天
weekOfYear: 39, // 全年的第几周
daysInWeek: 7, // 当前时间所在周的天数
daysInMonth: 30, // 当前时间所在月的天数
daysInYear: 366, // 当前时间所在年的天数
monthsInYear: 12, // 当前时间所在年的月数
isLeapYear: true, // 是否是闰年
}
*/
时间计算
原生时间对象 Date
缺少时间计算相关 API,如果需要在当前时间基础上新增或减少天数得到另一个时间,只能通过先获取天数,再进行加减,再合并成完整时间,更要考虑跨天、跨月、跨年,甚至闰年、夏令时的影响,极其难用。
相比较而言, moment.js
day.js
等第三方库提供了便捷的时间计算 API,我们来做个简单比较
// day.js / moment.js
const futureTime = dayjs().add(7, 'day').toString(); // -> Mon, 28 Sep 2020 09:33:31 GMT
const futureTime = dayjs().subtract(7, 'd').toString(); // -> Mon, 14 Sep 2020 09:39:45 GMT
// Temporal
const futureTime = Temporal.now.dateTime().plus({ days: 7 }).toString(); // -> Mon, 28 Sep 2020 09:33:31 GMT
const futureTime = Temporal.now.dateTime().minus(new Temporal.Duration('P7D')).toString(); // -> 2020-09-21T17:40:17.539198751
// P7D 为 Temporal 中构造段时间的语法,简介如下
// P1Y1M1DT1H1M1.1S -> 一年一月一天一小时一分一百毫秒
// P1M -> 一个月
// PT1M -> 一分钟
时间展示
对大部分场景来说,展示时间是采用各种第三方库的首要原因,这样非常痛点的能力在 Temporal
是怎样使用的呢?
时间格式化
很遗憾,目前 Temporal
未提供与 day.js
类似的占位符格式化 API。不过根据上文提到的时间静态属性,结合模板字符串,与原生 Date
相比,格式化的工作量也将大大降低。根据这些静态属性,实现一个类似三方库的 API,工作量也不会太大。
时间偏差
计算两个时间之间的差值,展示时间差也是一个经常遇到的场景, Temporal
提供的 API 与 day.js
相比更具灵活性,差值单位甚至可以多单位组合。
// day.js / moment.js
dayjs('2019-01-25').diff('2017-06-05', 'month'); // -> 19
dayjs('2019-01-25').diff('2017-06-05', 'm'); // -> 19
// Temporal
Temporal.DateTime.from('2019-01-25').difference(Temporal.DateTime.from('2017-06-05')).toString(); // -> P599D = 599天
Temporal.DateTime.from('2019-01-25').difference(Temporal.DateTime.from('2017-06-05'), {
largestUnit: 'years',
smallestUnit: 'seconds'
}).toString(); // -> P1Y7M20D = 一年七个月二十天
时区支持
与 Date
不同, Temporal
将天然支持时区。 Temporal
的时间默认无时区信息,所以需要在不同时区转换时间信息时,将用到时区相关 API。
// 构造中国东八区时区
const tz = new Temporal.TimeZone('Asia/Shanghai'); // 时区参数支持 IANA 名称,支持 UTC等 https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
// 获取绝对时间
const absoluteNow = Temporal.now.absolute(); // -> 2020-09-21T12:02:50.041733410
// 时间转换
const now = tz.getDateTimeFor(Temporal.now.absolute()); // -> 2020-09-21T20:02:50.041733410
历法支持
在上文提到的时间静态属性中,细心的朋友可能已经发现有是否是闰年这个属性,其实这就是历法在起作用。
时间定义中,历法决定了了一年中有多少天,一年有多少月,一个月有多少天,所以能够灵活而准确的采用不同历法在时间计算中相当重要。
目前世界上最常用的历法是 Gregorian calendar,在此基础上演变出来的时间标准 ISO_8601, Temporal
就是采用此种标准。
以 Hebrew calendar(犹太历)为例,犹太历根据大小月,每个月有29或30天,八月九月是润月,根据常年缺年满年的不同,有不同的天数,以犹太历为例:
// 获取犹太历 5756年3月14号 的绝对时间
const dateWithCalendar = Temporal.DateTime.from({
year: 5756,
month: 3,
day: 14,
hour: 3,
minute: 24,
second: 30,
calendar: new Temporal.Calendar('hebrew'),
}); // -> 1995-12-07T03:24:30[c=hebrew]
其他
其他方面, Temporal
相较于 Date
也有非常多的新特性,比如本地化、验证、比较等,期待 Temporal
尽早进入 stage3 甚至 stage4 阶段,彻底替代所有第三方时间库。