JS 的新一代日期/时间 API Temporal和 Moment.js的继承者

背景

在前端Moment.js可以说是家喻户晓,目前在日期和时间的处理上仍然是使用最广泛的库。从2011年到现在Moment已经完成了它的使命,在技术发展到今天,JavaScript生态已经足够健全,有很多优秀的库可以替代moment.js。所以除非有重大问题会维护一下,小毛病就不管了,也不再发布大迭代版本创新了。不建议大家使用moment.js作为新项目的时间处理包,可以选择Luxon,Day.js,date-fns,js-Joda作为替代品。

Moment的继承者

  • day.js官网
  • Day.js 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样
对比 moment dayjs
体积 67.8k左右 2KB左右
沙箱 Moment 对象是可变对象(mutable) 所有更改Day.js对象的API操作都将返回一个新的实例。
多语言支持
插件拓展 ×
  • 插件可以给 Day.js 增加新功能和扩展已有功能:
    • AdvancedFormat 扩展了 dayjs().format API 以支持更多模版
    • RelativeTime 增加了 .from .to .fromNow .toNow 4个 API 来展示相对的时间 (e.g. 3 小时以前).
    • IsLeapYear 增加了 .isLeapYear API 返回一个 boolean 来展示一个 Dayjs’s 的年份是不是闰年.
    • WeekOfYear 增加了 .week() API 返回一个 number 来表示 Dayjs 的日期是年中第几周.
    • IsSameOrAfter 增加了 .isSameOrAfter() API 返回一个 boolean 来展示一个时间是否和一个时间相同或在一个时间之后.
    • IsSameOrBefore 增加了 .isSameOrBefore() API 返回一个 boolean 来展示一个时间是否和一个时间相同或在一个时间之前.

小结

简单来说day.js 是Moment的完美继承者,不仅解决了mutable的问题,同样解决了包体积过大的问题,同时保持了与moment相同的api,几乎不需要任何代价的进行切换使用。Luxon,date-fns,js-Joda感兴趣的同学可以自行了解。

JS 的新一代日期/时间 API Temporal

  • Temporal 博客
    众所周知,JS的Date是出了名的难用,一直以来我们都在使用momentjs,dayjs等第三方库来处理日期和时间格式,于是 TC39 组织开始了对 Date 的升级改造,他们找到了 moment.js 库的作者,Maggie ,由她来担任新特性 Temporal的主力设计。
  • 特性
  • 所有的对象都是不可改变的。改变它们会产生新的值,类似于JavaScript中字符串的工作方式。
  • 它支持时区和非格雷戈尔式的日历。
  • 有几个专门针对时间值的类(带时区的日期时间值,不带时区的日期时间值,不带时区的日期值,等等)。这有几个好处。
    • 一个值的上下文(是否有时区,等等)更容易理解。
    • 如何实现一个给定的任务往往更加明显。
    • .toString() ,使用时可以少考虑很多。
  • 1月是第1个月。
  • Temporal 支持的日历基于标准的Unicode Unicode Common Locale Data Repository (CLDR) 也就是说支持农历、民国历等等
npm install @js-temporal/polyfill

import { Temporal} from '@js-temporal/polyfill';

Temporal是一个全局对象,像 Math 、Promise 一样位于顶级命名空间中,为 Javascript 语言带来了现代化的日期、时间接口。

对比Date和Temporal的使用

  • Temporal被设计为三部分

    • ISO 8601 格式的日期和时间;具体时间

    • 时区(中国北京);哪个时区的

    • 日历(中国农历);使用什么历法

  • 对比 Date

    new Date()
    //Mon May 16 2022 10:11:27 GMT+0800 (中国标准时间)
    

Date 采用 GMT格式(旧的时间表示格式) 的时间,使用方面不如 ISO 8601 通用,同时不包含 时区和历法。

使用方法(更多API)

  • 如何获取本地时区的当前日期和时间?
    请注意,如果您只需要日期而不是时间,则应使用Temporal.PlainDate. 如果两者都需要,请使用Temporal.PlainDateTime.

    const date = Temporal.Now.plainDateISO();
    date.toString();
    
    Temporal.Now.plainDateTimeISO().toString();
    
  • 如何获得 Unix 时间戳?

    const timeStamp = Temporal.Now.instant();
    
    // Timestamp in Milliseconds
    timeStamp.epochMilliseconds;
    // Timestamp in Seconds
    timeStamp.epochSeconds;
    
  • Temporal在类型和遗留之间转换Date

    • 旧版Date=>Temporal.Instant和/或Temporal.ZonedDateTime
    const legacyDate = new Date('1970-01-01T00:00:01Z');
    const instant = legacyDate.toTemporalInstant();  // 转换为 temporal 格式
    
    assert.equal(instant.epochMilliseconds, legacyDate.getTime()); // 和 Date 对比
    assert.equal(instant.toString(), '1970-01-01T00:00:01Z');
    
    const zoned = instant.toZonedDateTimeISO(Temporal.Now.timeZone());
    
    assert.equal(zoned.epochMilliseconds, legacyDate.getTime());
    
    const zoned2 = instant.toZonedDateTimeISO('Asia/Shanghai');
    
    assert.equal(zoned2.epochMilliseconds, legacyDate.getTime());
    assert.equal(zoned2.timeZone.id, 'Asia/Shanghai');
    
    • 仅日期值:legacy Date=>Temporal.PlainDate
      要正确地将 date-only 转换Date为 aTemporal.PlainDate而不会受到偏离一天错误的影响,您必须确定使用哪个时区的午夜来构造Date,然后在从 转换为 时使用相同的时Temporal.Instant区Temporal.PlainDate。
    	let date = new Date(2000, 0, 1);
    	let plainDate = date
    	  .toTemporalInstant()                         // => 2000-01-01T08:00:00Z
    	  .toZonedDateTimeISO(Temporal.Now.timeZone()) // => 2000-01-01T00:00:00-08:00[America/Los_Angeles]
    	  .toPlainDate();                              // => 2000-01-01
    	  
    	assert.equal(plainDate.toString(), '2000-01-01');
    	
    	date = new Date(Date.UTC(2000, 0, 1)); // => Fri Dec 31 1999 16:00:00 GMT-0800 (Pacific Standard Time)
    	date = new Date('2000-01-01T00:00Z');  // => Fri Dec 31 1999 16:00:00 GMT-0800 (Pacific Standard Time)
    	plainDate = date
    	  .toTemporalInstant()       // => 2000-01-01T00:00:00Z
    	  .toZonedDateTimeISO('UTC') // => 2000-01-01T00:00:00+00:00[UTC]
    	  .toPlainDate();            // => 2000-01-01
    	
    	assert.equal(plainDate.toString(), '2000-01-01');
    
    • Temporal类型 => 遗留Date
    // To convert Instant to legacy Date, use the epochMilliseconds property.
    
    const instant = Temporal.Instant.from('2020-01-01T00:00:01.000999Z');
    const result = new Date(instant.epochMilliseconds);
    
    assert.equal(result.getTime(), 1577836801000); // ms since Unix epoch
    assert.equal(result.toISOString(), '2020-01-01T00:00:01.000Z');
    
    // Same thing for ZonedDateTime.
    // Note that legacy Date will not preserve the ZonedDateTime's time zone.
    
    const zoned = Temporal.ZonedDateTime.from('2020-01-01T00:00:01.001[Asia/Tokyo]');
    const result2 = new Date(zoned.epochMilliseconds);
    
    assert.equal(result2.getTime(), 1577804401001); // note, different time
    assert.equal(result2.toISOString(), '2019-12-31T15:00:01.001Z');
    
    // For most use cases, new Date(x.epochMilliseconds) is fine.
    // You may need to add an extra round() step if you want other
    // rounding behaviour than truncation. For example, here the 999
    // microseconds is rounded to 1 millisecond.
    
    const result3 = new Date(instant.round({ smallestUnit: 'millisecond' }).epochMilliseconds);
    
    assert.equal(result3.getTime(), 1577836801001);
    assert.equal(result3.toISOString(), '2020-01-01T00:00:01.001Z');
    
  • 类型之间的转换

    • 将日历日期 ( Temporal.PlainDate) 和挂钟时间 ( Temporal.PlainTime) 组合成Temporal.PlainDateTime. 就是将日期加上时间组合起来
    const date = Temporal.PlainDate.from('2020-05-14');
    
    const noonOnDate = date.toPlainDateTime(Temporal.PlainTime.from({ hour: 12 }));
    
    assert(noonOnDate instanceof Temporal.PlainDateTime);
    
    assert.equal(noonOnDate.toString(), '2020-05-14T12:00:00');
    
    
    • 将日历上的一天 ( Temporal.PlainMonthDay) 和一年组合成Temporal.PlainDate 就是将天和年组合起来
    const birthday = Temporal.PlainMonthDay.from('12-15');
    
    const birthdayIn2030 = birthday.toPlainDate({ year: 2030 });
    
    birthdayIn2030.dayOfWeek; // => 7
    
    assert(birthdayIn2030 instanceof Temporal.PlainDate);
    
    assert.equal(birthdayIn2030.toString(), '2030-12-15');
    
  • 序列化

    • 要将精确时间序列Temporal.Instant化为字符串,请使用toString(). 没有任何参数,这会给你一个 UTC 时间的字符串。

    • 如果您需要您的字符串包含 UTC 偏移量,则使用该timeZone选项Temporal.Instant.prototype.toString()将返回该时区中与确切时间相对应的挂钟时间的字符串序列化。

    • 这会丢失有关字符串所在时区的信息,因为它只保留该特定确切时间与时区的 UTC 偏移量。如果您需要您的字符串包含时区名称,请改用Temporal.ZonedDateTime它保留此信息。

    const instant = Temporal.Instant.from('2022-05-16T10:41:51Z');
    // 使用 toString 获得字符串
    const result = instant.toString(); // '2022-05-16T10:41:51Z'
    // 使用 toString 获得某时区字符串
    const result2 = instant.toString({ timeZone: 'America/Yellowknife' });
    
    // 使用 toZonedDateTimeISO 转换时区
    const zoned = instant.toZonedDateTimeISO('Asia/Seoul');
    const result3 = zoned.toString();
    
    // ZonedDateTime
    assert(zoned.equals(Temporal.ZonedDateTime.from(result3)));
    
  • 同时Temporal 支持排序, 四舍五入, 时区转换, 时间的计算等等功能

    • 具体API查看上面链接的Doc文档

Temporal 小结

1.Date不支持除用户本地时间以外的时区。Temparal 支持开发人员通过 TimeZone 来设置本地时间以外的时区。
2.计算 API 缺失。除了时区和日历类型外,其他类型都可以进行 算术运算,即时间的比较,增加,减少等。
3.不支持非公历,Calendar 类型支持 Temparal 选择日历。
4.解析器行为不可靠以至于无法使用,在 Temporal 里,new 构造函数() 或者From 方法,对参数的要求都更加规范,同时From 方法支持 日期溢出 后的逻辑处理,可以防止系统崩溃。

JS 的新一代日期/时间 API Temporal和 Moment.js的继承者_第1张图片

左侧绿色区域的 Instant 类型,用来表达某个瞬间的时间,不包含时区和日历的信息。右侧黄色区域的 PlainXX系列(5个),用来表达日历日期或者钟表时间,包含日历信息,而中间的 ZonedDateTime 则横跨左右两个区域,包含时区和日历信息,可以作为一个通道,连接左侧的 Instant 和右侧的 Plain系列,负责类型之间转换的桥梁,同时中间的 Timezone 时区类型 Calendar 日历类型,不单独使用,配合上方的 ZonedDateTime 类型来辅助转换。最下面的 Duration 与所有类型没有直接关系,不参与类型转换,表示一段持续时间,并且这段时间可以用来进行算术。

参考文章

Day.js中文网

tc39 提案博客

Moment.js 宣布停止开发,现在该用什么?

360导航前端团队 - JS历史最头疼的对象Date即将淘汰,替代物Temporal真的好用

你可能感兴趣的:(javascript,前端,moment,dayjs)