JavaScript – Temporal API & Date

前言

Temporal API 是 JS 的新东西, 用来取代 Date. 虽然现在 (15-01-2023) 没有任何游览器支持. 但它已经是 stage 3 了. 而且有完整的 polyfill, 所以还是非常推荐大家积极的去使用它.

如果你对日期不熟悉, 可以先看看这篇 Time Zone, Leap Year, Date Format, Epoch Time 时区, 闰年, 日期格式

 

参考

YouTube – Learn Temporal API In 17 Minutes

Temporal Date API Ultimate Guide

tc39 – Temporal

 

吐槽 Date

Date 非常的烂, 它是 1995年 模仿 Java 设计的. 后来 1997年 Java 丢弃了这个设计, 但 JavaScript 却一直沿用至今. 从这点你就知道它有多烂了.

month starts with zero

const date = new Date(2023, 1, 1);
console.log(date.toDateString()); // Wed Feb 01 2023

你以为创建的是 1月1号 ? 不, JS 的 Date 月份是从 0 开始的. 1月 = 0, 2月 = 1....

parse string 倒是正确的, 但 getMonth 依旧是返回 0 哦.

const date2 = new Date('2023-01-01');
console.log(date2.toDateString()); // Sun Jan 01 2023
console.log(date2.getMonth()); // 0

alwasy time zone & only locale time zone

JS 的 Date 一定包含 time zone, 而且这个 time zone 是无法用 JS 控制的, 它总是依据游览器的设定.

创建一个早上八点的 Date

const date = new Date(2023, 0, 15, 8);
console.log(date.toLocaleString()); // 1/15/2023, 8:00:00 AM

查看 time zone offset, 马来西亚的 offset 是 +08.00

console.log(date.getTimezoneOffset()); // -480

480 是 minutes, 换成 hours 就是 8 小时. 前面的(-)减符号让人匪夷所思. +08:00 不应该是 positive 吗?!

查看 UTC 时间

console.log(date.toUTCString()); // "Sun, 15 Jan 2023 00:00:00 GMT" 

果然, 8 小时没了. 所以 Date 的 time zone 就是 +08:00.

Force UTC ?

有没有办法让它变成 UTC time zone +00:00 呢? 没有

const date = new Date(Date.UTC(2023, 0, 15, 8));
console.log(date.toUTCString()); // Sun, 15 Jan 2023 08:00:00 GMT

用 Date.UTC 看上去好像是 UTC time zone, 但其实不是.

Date.UTC 只是把日期变成了 number 然后交给 Date. 最终出来的 Date 依然是 locale time zone

不信, 查看 offset 和 locale date

console.log(date.getTimezoneOffset()); // -480
console.log(date.toLocaleString()); // 1/15/2023, 4:00:00 PM 

locale date 从早上 8 点变成了下午 4 点, 多了 8 小时.

总结: Date 总是有 time zone 概念的, 而且 time zone 是依据游览器 locale 设定的.

不直观的 Date edit function

想添加 15天 到一个 datetime 是这样的

const now = new Date(2023, 0, 29);
console.log(now.toDateString()); // Sun Jan 29 2023
now.setDate(30); // mutable
console.log(now.toDateString()); // Mon Jan 30 2023
now.setDate(now.getDate() + 15); // add 15 days
console.log(now.toDateString()); // Tue Feb 14 2023

通过 setDate 而且利用了它自动进位的功能来完成 add 15 days 操作.

直观的操作应该是这样的

const now = new Date();
const newDate = now.addDays(15); // immutable

sort date

我在 JavaScript – Sort 里提过 sort date 的方式. 利用 date.getTime 得到 epoch time 然后对减.

这个方法很间接. 应该要提供一个类似 String.prototype.localeCompare 的方法用于 Date 排序.

总结

总之 Date 有一万个不好, 不要用它, 宁愿用 momentjs.

但随着 momentjs 退役, 我们也需要找一个替代方案, 而 Temporal API 似乎是一个不错的选择.

 

Temporal Polyfill

目前没有任何游览器支持. 所以一定要安装 temporal-polyfill

yarn add @js-temporal/polyfill

它自带 TypeScript 哦

import { Temporal, Intl, toTemporalInstant } from '@js-temporal/polyfill';
Date.prototype.toTemporalInstant = toTemporalInstant;

有三个主要 exports. Temporal 就是 Temporal API 的接口.

Intl 是为了让 Intl.DateTimeFormat 支持 Temporal 实例. 所以也需要 polyfill.

最后是 toTemporalInstant, 它是一个方便把 Date 转换成 Temporal 实例的小扩展.

 

Date, Time, DateTime, Duration, TimeZone 概念

传统的 Date 对象包含了 Date (日期), Time (时间), Time Zone (时区). 这种 always all in 的对象并不友好.

很多时候我们只想表达日期. 不需要时间也不需要 Time Zone. 有时候只想表达几点几分, 不需要日期. 这些时候用 Date 都很变扭.

Temporal 允许我们只定义日期 Date, 或者只定义时间 TIme, 或者两者 DateTime. 而 DateTIme 也可以选择要不要有 Time Zone 概念.

所以我们会有许多不同的对象, 对象的方法也都不一样. 比如 Temporal 的 Date 对象没有操控时间的 API, Time 对象没有操控日期的 API.

另外 Temporal 还引入了 Duration 概念. 它表示一个时间单位. 比如 10 分钟, 两天, 一星期. 这个在日常生活很常见. 

总之, Temporal 把时间分的很细, 而不像传统的 Date 对象, 完全不符合我们直观的理解.

 

创建 Temporal 

Temporal.now 可以依据此时此刻创建一个 Temporal 对象

如同上面提到的, 它有很多种对象可以选择. Date, Time, DateTime, zonedDateTIme. 我们依据需求范围决定使用哪个对象就可以了.

Temporal.Now.plainDateISO().toString();     // 2023-01-15
Temporal.Now.plainTimeISO().toString();     // 21:16:35.06659506
Temporal.Now.plainDateTimeISO().toString(); // 2023-01-15T21:16:35.069595066
Temporal.Now.zonedDateTimeISO().toString(); // 2023-01-15T21:16:35.072595069+08:00[Asia/Kuala_Lumpur]
Temporal.Now.instant().toString();          // 2023-01-15T13:16:35.075595072Z

这里有一个知识点要知道.

date, time, datetime 都缺少了 time zone, 它们是算不出 epoch time 的. 只有拥有 time zone 的 zonedDateTime 和 instant (它是 UTC) 才可以算出 epoch time 哦.

因为 epoch time 是 1970年1月1号 (UTC +00:00) 到某时间的纳秒数. 而这个某时间一定要明确表明 Time Zone 不然秒数肯定不准.

Temporal.Now.zonedDateTimeISO().epochMilliseconds; // 1673789585487
Temporal.Now.instant().epochMilliseconds;          // 1673789585487

from

想创建任意时间/时区, 可以用 from 方法, 可以用 string format 或者传 config 对象

const date = Temporal.ZonedDateTime.from('2012-01-01[Asia/Kuala_Lumpur]'); // 2012-01-01T00:00:00+08:00[Asia/Kuala_Lumpur]

const date = Temporal.ZonedDateTime.from({
  year: 2023,
  month: 1,
  day: 1,
  timeZone: 'Asia/Kuala_Lumpur',
});
// 2023-01-01T00:00:00+08:00[Asia/Kuala_Lumpur]

注: 一月是 1 而不是 0 哦,

完整的 time zone id 列表可以参考:Wikipedia – List of tz database time zones

 

日期格式

输出日期格式不是 Temporal 的职责. 它只有少量的 API 可以控制.

Temporal.Now.zonedDateTimeISO().toString(); // 2023-01-15T21:49:51.000590989+08:00[Asia/Kuala_Lumpur]
Temporal.Now.zonedDateTimeISO().toLocaleString('en-US'); // 1/15/2023, 9:49:51 PM GMT+8
Temporal.Now.zonedDateTimeISO().toLocaleString('ms-MY'); // 15/1/2023, 9:49:51 PTG MYT
Temporal.Now.zonedDateTimeISO().toLocaleString('zh-TW'); // 2023/1/15 下午9:49:51 [GMT+8]
Temporal.Now.zonedDateTimeISO().toLocaleString('zh-CN'); // 2023/1/15 GMT+8 21:49:51
Temporal.Now.zonedDateTimeISO().toInstant().toString(); // 2023-01-15T13:49:51.015591013Z
Temporal.Now.zonedDateTimeISO().toInstant().toString({ smallestUnit: 'millisecond' }); // 2023-01-15T13:49:51.017Z

两个点值得注意

1. 最小的单位是纳秒 nanosecond, 可以通过 options smallestUnit 控制最小单位输出. (millisecond > microsecond > nanosecond) 每一个级别差 1000 (1ms = 1000micro = 1000000ns)

2. instant 才可以输出程序员最常见的格式

Inlt.DateTimeFormat

想要配置多一点需要使用 Inlt.DateTimeFormat

const date = Temporal.Now.zonedDateTimeISO();
Intl.DateTimeFormat('en-MY', {
  year: 'numeric',
  month: 'short',
  day: '2-digit',
  hour: '2-digit',
  hour12: true,
  minute: '2-digit',
  second: '2-digit',
  weekday: 'short',
  timeZoneName: 'longOffset',
}).format(date); // Sun, 15 Jan 2023, 10:19:39 pm GMT+08:00

它已经可以配置很多东西了, 但依然没有完全自定义 format 的能力. 比如你不能像 C# 那样直接指定一个 format yyyy-MMM-dd HH:mm:ss tt %K 作为输出.

最后讲一下 time zone 的 format

下面这样会报错. zonedDateTimeISO 已经有明确 time zone 了, format 的时候不可以再声明其它的

const date = Temporal.Now.zonedDateTimeISO();
console.log(
  Intl.DateTimeFormat('en-MY', {
    dateStyle: 'long',
    timeStyle: 'long',
    timeZone: new Temporal.TimeZone('UTC'), // 不能配置不相同的 time zone
  }).format(date)
);

instand 则可以声明任何 time zone

const date = Temporal.Now.instant();
console.log(
  Intl.DateTimeFormat('en-MY', {
    dateStyle: 'long',
    timeStyle: 'long',
    timeZone: new Temporal.TimeZone('America/Chicago'),
    // timeZone: new Temporal.TimeZone('UTC'),
    // timeZone: new Temporal.TimeZone('Asia/Kuala_Lumpur'),
  }).format(date)
);

它会自动转换时间. 另外, 如果 instant 没有声明 time zone, 它不是 UTC 哦, 而是依据游览器的设置. 想要 UTC 需要声明 time zone 是 UTC/GMT

 

Duration

它类似 C# 的 TimeSpan, 玩法很简单

const duration1 = Temporal.Duration.from({ minutes: 5, seconds: 30 });
console.log(duration1.toString()); // PT5M30S
const duration2 = Temporal.Duration.from('PT10M35S');
console.log(duration2.minutes); // 10
console.log(duration2.seconds); // 35
console.log(duration2.total('second')); // 635

再来一个

Temporal.Duration.from({
  years: 1,
  months: 1,
  days: 1,
  hours: 1,
  minutes: 1,
  seconds: 1,
}).toString() // P1Y1M1DT1H1M1S

拆开来看 P 1Y 1M 1DT 1H 1M 1S.

P 应该是 stand for Plain

PT 是 PlainTime (PT for 只有 hours 和一下, 有 days 或以上一律 starts with P 没有 T)

切换加减

Temporal.Duration.from({ minutes: 5, seconds: 30 }).negated().abs();

 

获取时间细节

const date = Temporal.Now.zonedDateTimeISO();
const values = [
  date.year,        // 2023
  date.month,       // 1
  date.day,         // 15
  date.dayOfWeek,   // 7
  date.hour,        // 22
  date.minute,      // 35
  date.second,      // 37
  date.millisecond, // 663
  date.microsecond, // 337
  date.nanosecond,  // 652
  date.offset,      // '+08:00'
  date.offsetNanoseconds / 1000 / 1000 / 1000 / 60 / 60, // 8
];
date.toString() // 2023-01-15T22:37:14.513434495+08:00[Asia/Kuala_Lumpur]

算 ago

const commentDate = Temporal.ZonedDateTime.from('2023-01-02[Asia/Kuala_Lumpur]');
const now = Temporal.Now.zonedDateTimeISO();
console.log(now.since(commentDate).round({ smallestUnit: 'day' }).total('day') + 'days ago'); // 14 days ago

用了 since, round, total 技巧. 还有一个方法叫 until. 它和 since 是同个原理, 只是方向不一样.

now.since(previous) 是说从过往某天到现在的时间. now.until(future) 是从现在到未来某天的时间. 

 

DateTime Adjustment

const date = Temporal.Now.plainDateISO();
const oneDay = Temporal.Duration.from({ days: 1 });
const newDate = date.add(oneDay);
console.log(date.toString());    // 2023-01-15
console.log(newDate.toString()); // 2023-01-16

它是 immutable 的.

直接传对象也是可以的

const newDate = date.add({ days: 1 });

减法

const newDate = date.subtract({ days: 1 });

或者使用 add days: -1 也是可以的

直接修改

const date = Temporal.Now.plainDateISO();
const newDate = date.with({ day: 18 });
console.log(date.toString()); // 2023-01-15
console.log(newDate.toString()); // 2023-01-18

注: Temporal 的 with 和 传统 Date 对象的 setDate 不同, 如果给的数超过当月份, 那么它会停在最后一天, 比如 31号. 而 setDate 则会自动进位去修改月份.

 

Date Comparison for Sort

const today = Temporal.Now.plainDateISO();
const yesterday = today.subtract('P1D');
const tomorrow = today.add('P1D');
const days = [tomorrow, today, yesterday];
console.log(days.sort(Temporal.PlainDate.compare).map(d => d.toString())); // ['2023-01-14', '2023-01-15', '2023-01-16']

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章