掌握 JavaScript 日期和时间:从 Moment.js 到 Temporal

JavaScript 的 `Date` API 长期以来一直是开发人员的苦恼之源,因为它存在历史设计缺陷,包括:

  • 不可靠的解析行为
  • 易变的性质
  • 时区支持较弱
  • 为了克服这些问题和限制,开发人员开始使用 Moment.js 等库来处理更可靠、功能更丰富的日期和时间。现在,JavaScript 即将推出一种新的内置解决方案:Temporal API,它为日期和时间操作提供了一种现代而直观的方法。

    在本文中,我们将研究 JavaScript 的“Date”API 的局限性,讨论 Moment.js 等流行库的优缺点,并深入研究 Temporal API。

    JavaScript 日期 API 简介

    Brendan Eich 于 1995 年在 10 天内编写了 JavaScript。在匆忙的开发过程中,`Date` API 是通过复制 Java 的 `Date` 对象的实现创建的,无意中继承了它的几个问题。近 30 年后,这些问题仍然让开发人员感到沮丧。

    `Date` API 的主要缺陷之一是其可变性。对 `Date` 对象的任何更改都会影响原始实例,从而可能导致意外行为。此外,该 API 缺乏用于常见日期和时间操作的直观方法,迫使开发人员编写笨拙且容易出错的代码。

    在下面的示例中,我们创建了两个 `Date` 对象,`today` 和 `tomorrow`。将 `tomorrow` 设置为 `today` 的后一天后,`today` 和 `tomorrow` 最终都指向同一个日期。如果处理不当,此行为可能会导致意外结果或错误:

    const today = new Date();
    const tomorrow = today; // Both variables reference the same object
    
    // Modify tomorrow's date
    tomorrow.setDate(tomorrow.getDate() + 1);
    
    // Since today and tomorrow reference the same object, both will be affected
    console.log("Today:", today);
    console.log("Tomorrow:", tomorrow);

    上述示例中显示的另一个问题是缺少用于添加或减去日期的内置方法。要添加日期,我们必须使用 `getDate()` 提取日期,然后添加日期,最后使用 `setDate()` 更新日期。这种方法使代码更难阅读和维护:

    // What we have to do using the Date API
    tomorrow.setDate(tomorrow.getDate() + 1);
    // What we expect
    tomorrow.addDays(1)

    `Date` API 的其他值得注意的问题包括:

  • 解析器行为不可靠。日期解析可能因浏览器和时区设置而异,并且日期解析方式通常不可预测,从而导致结果不正确
  • 缺乏对全面时区处理的支持。Date API 仅支持用户本地时区和 UTC,跨时区计算变得复杂
  • Date API 无法表示没有时间部分的日期。如果创建 Date 对象时未指定时间,则默认为当地时区的午夜 (00:00:00)。这可能会导致计算或比较出现意外结果
  • 处理夏令时 (DST) 是另一项重大挑战。DST 转换期间的日期行为通常不可预测,这可能导致时间敏感型应用程序出现错误
  • Date API 缺乏边界检查,这可能会导致意外结果。当使用超出有效范围的值的 Date 构造函数时,Date 对象会自动将溢出调整到下个月,而不会发出警告或错误。例如,new Date(2021, 1, 31) 意外地表示 3 月 3 日
  • JavaScript 的 Moment.js 和 date-fns 库

    由于原生 JavaScript `Date` API 难以使用,因此出现了各种 JavaScript 库来简化日期和时间操作,其中之一就是 Moment.js。

    与原生的“Date”API 相比,Moment.js 提供了更直观的日期和时间处理 API。以下是示例:

    // use Date API to add one day for tomorrow
    const date = new Date();
    date.setDate(date.getDate() + 1); 
    // use moment.js to add one day for tomorrow
    const momentDate = moment().add(1, "day");

    可以看出,Moment.js 提供了一种更简洁易读的方式来操作日期。它还提供了许多附加功能,例如:

  • 时区处理
  • 持续时间计算
  • 相对时间格式
  • 有关更多详细信息,请参阅使用 Moment.js 操作数据和时间的指南。

    Moment.js 虽然功能丰富,但易变且繁重。如果不小心使用,易变的特性可能会导致意外行为。此外,它在 2020 年被弃用,这意味着它不再得到积极维护。

    幸运的是,像 date-fns 这样的替代库提供了类似的功能。date-fns 是不可变的,并且具有摇树功能,这使我们能够只加载必要的组件。

    介绍 Temporal API

    Temporal API Proposal

    上图说明了时间日期对象字符串表示;它的结构支持纯日期、时间、时间偏移、区域日期/时间值以及所有类型的日历。

    Temporal API 的一些主要功能包括:

  • 不可变性:时间对象是不可变的,这意味着任何日期操作都会创建新的实例,而不会修改原始实例
  • 精度:支持纳秒精度
  • 复杂的日期和时间处理:Temporal 内置对时区、夏令时和日历系统的支持
  • 性能提升:Temporal 内置于 JavaScript 中,旨在处理复杂的计算。作为原生解决方案,它不会增加包大小,并且比外部库性能更高
  • Temporal API、Moment.js 和 date-fns 之间的比较

    Temporal API 的基本用法

    由于 Temporal API 仍处于第 3 阶段,在大多数 JavaScript 环境中尚不可用,因此在撰写本文时,您需要安装 polyfill 才能使用它。要安装 `js-temporal/polyfill`,请运行以下命令:

    npm install @js-temporal/polyfill

    然后,将其导入 JavaScript 项目以开始使用时间特征:

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

    Temporal API 引入了几种日期类型,包括:

  • PlainDateTime:表示没有时区的日期和时间
  • ZonedDateTime:表示特定时区的日期和时间
  • 其他类型,例如“Temporal.Duration”,可以精确处理时间间隔,使 Temporal 能够适用于各种日期和时间场景。

    纯日期/时间

    我们可以使用普通的日期对象来表示没有时区的日期,这对于生日或截止日期等场景非常理想。

    我们可以使用 `Temporal.Now.plainDateISO()` 或 `Temporal.PlainDate.from()` 创建纯日期。类似地,纯日期时间对象表示没有时区的日期和时间,纯时间仅表示时间:

    // plain date
    const today = Temporal.Now.plainDateISO();
    const newYear = Temporal.PlainDate.from("2024-01-01");
    console.log(newYear.toString()); //2024-01-01
    console.log(today.toString()); //2024-11-06
    
    // plain date time
    const now = Temporal.Now.plainDateTimeISO();
    console.log(now.toString()); //2024-11-06T20:24:57.927697925
    
    // plain time
    const currentTime = Temporal.Now.plainTimeISO();
    console.log(currentTime.toString()); //20:48:04.025084025

    构造简单日期的另一种方法是从指定年、月、日的对象开始:

    const firstDateNov = Temporal.PlainDate.from({ year: 2024, month: 11, day: 1 });
    console.log(firstDateNov.toString()); // 2024-11-01

    分区日期/时间

    分区日期时间表示带有时区详细信息的日期时间对象。我们可以使用它来跨时区转换日期/时间,或计算考虑夏令时的日期时间。

    Temporal API 使用 IANA 时区数据库,该数据库以“区域/位置”形式定义具有唯一名称的时区,例如“America/New_York”:

    // Zoned date: with time zone info
    const today = Temporal.Now.zonedDateTimeISO();
    console.log(today.toString()); //2024-11-06T21:04:23.019063018+10:00[Australia/Sydney] 
    const zonedDateTime = Temporal.ZonedDateTime.from("2024-10-30T10:00[Australia/Sydney]");
    console.log(zonedDateTime.toString());//2024-10-30T10:00:00+11:00[Australia/Sydney]

    立即的

    `Temporal.Instant` 表示一个精确的时间点。与限制为毫秒的传统 JavaScript `Date` 对象不同,`Temporal.Instant` 提供纳秒精度。瞬间始终采用 UTC,因此适用于记录来自不同时区的事件等用例,而无需担心本地偏移:

    const currentInstant = Temporal.Now.instant();
      console.log('current instant:',currentInstant.toString()); //current instant: 2024-11-10T00:17:06.085826084Z 
      const instantFromString = Temporal.Instant.from('2024-11-10T10:41:51Z');
      console.log('from string:', instantFromString.toString()); //from string: 2024-11-10T10:41:51Z 
      const sydneyTime = instantFromString.toString({ timeZone: 'Australia/Sydney' });
      console.log('sydney time:',sydneyTime); //sydney time: 2024-11-10T21:41:51+11:00

    期间

    在 Temporal API 中,持续时间表示时间间隔,例如天、小时甚至年。它简化了日期和时间计算,例如在特定日期中添加或减去时间,或者在定义的时间间隔内安排事件。

    以下是向日期添加持续时间的示例:

    // Create a duration of 1 year, 1 months, and 10 days
    const duration = Temporal.Duration.from({ years: 1, months: 1, days: 10 });
    console.log(duration.toString()); // "P1Y1M10D"
    // Add the duration to a date
    const startDate = Temporal.PlainDate.from("2024-10-30");
    const newDate = startDate.add(duration);
    console.log(newDate.toString()); // 2025-12-10

    类似地,我们可以使用持续时间来计算时间:

    const timeDuration = Temporal.Duration.from({ hours: 3, minutes: 45 }); 
    console.log(timeDuration.toString()); // "PT3H45M" 
    // Subtracting time duration from a specific time 
    const time = Temporal.PlainTime.from("12:00"); 
    const newTime = time.subtract(timeDuration); 
    console.log(newTime.toString()); // 08:15:00

    计算日期和时间:加、减、四舍五入以及

    Temporal API 允许我们执行诸如添加或减去时间、四舍五入到特定单位以及使用“with”替换日期时间组件等操作。“round”方法将日期和时间值四舍五入到最接近的指定单位。“with”方法可以替换特定组件(例如年、月或小时)而不更改其他部分:

    const initialDateTime = Temporal.PlainDateTime.from("2024-11-01T10:23:45.678");
      // Add 2 days and 5 hours
      const addedDateTime = initialDateTime.add({ days: 2, hours: 5 });
      console.log("After Adding:", addedDateTime.toString()); // "2024-11-03T15:23:45.678"
      // Subtract 1 hour and 30 minutes
      const subtractedDateTime = addedDateTime.subtract({ hours: 1, minutes: 30 });
      console.log("After Subtracting:", subtractedDateTime.toString()); // "2024-11-03T13:53:45.678"
      // Round to the nearest minute
      const roundedDateTime = subtractedDateTime.round({ smallestUnit: "minute" });
      console.log("After Rounding:", roundedDateTime.toString()); // "2024-11-03T13:54:00"
      // Use 'with' to modify specific components (change only the month)
      const finalDateTime = roundedDateTime.with({ month: 12 });
      console.log("Final Date-Time:", finalDateTime.toString()); // "2024-12-03T13:54:00"

    Temporal API 的主要优势:不变性和性能

    Temporal API 的一个主要优势是其专注于不变性。这意味着一旦创建了 Temporal 对象,其值就无法修改。这种不变性可确保日期和时间计算是可预测的,并避免意外的副作用:

    const now = Temporal.Now.plainDateISO();
    const futureDate = now.add({ days: 5 });
    
    // Modifying futureDate won't affect now
    futureDate.add({ hours: 2 });
    
    console.log("Now:", now.toString()); // Now: 2024-11-07 
    console.log("Future Date:", futureDate.toString()); //Future Date: 2024-11-12

    与 Moment.js 和 date-fns 等库相比,Temporal API 的性能显著提升,这主要归功于其原生实现以及对日期和时间操作的有效处理。

    与庞大且可变的 Moment.js 不同,Temporal API 不会增加包大小,其不可变设计可最大限度地减少内存使用量。此外,由于它内置于 JavaScript(无需额外解析或开销),Temporal 速度更快,尤其适用于精确、大量日期时间任务,如调度和时区管理。

    高级用法:时区、日程安排和非公历

    使用 `temporal.zonedDatetime`,我们可以轻松处理时区和调度。它允许我们使用本地时间并管理跨时区的事件。

    使用时区

    使用“Temporal.ZonedDateTime”,我们可以在选定的时区内创建特定的日期和时间,确保诸如加减时间之类的操作自动遵守时区规则,包括夏令时调整:

    // Create a ZonedDateTime in the "Australia/Sydney" time zone
    const sydneyTime = Temporal.ZonedDateTime.from(
      '2024-10-30T15:00:00[Australia/Sydney]'
    );
    console.log(sydneyTime.toString()); // 2024-10-30T15:00:00+11:00[Australia/Sydney]
    // Convert to a different time zone
    const londonTime = sydneyTime.withTimeZone('Europe/London');
    console.log(londonTime.toString()); // 2024-10-30T04:00:00+00:00[Europe/London]

    夏令时 (DST) 开始时,时钟会向前调整一小时。这不会改变时间,但会改变时间偏移,使其看起来像是跳过了一个小时。Temporal 会自动处理这些变化,确保计算在 DST 转换期间保持准确:

    // Adding 24 hours during a daylight saving change
    const beforeDST = Temporal.ZonedDateTime.from("2024-10-05T12:00:00[Australia/Sydney]");
    const afterDST = beforeDST.add({ days: 1 }); // Automatically adjusts for DST
    console.log(beforeDST.toString());// 2024-10-05T12:00:00+10:00[Australia/Sydney] 
    console.log(afterDST.toString()); // 2024-10-06T12:00:00+11:00[Australia/Sydney]

    安排活动

    Temporal API 还使得跨时区安排和计算事件时间变得更加容易。

    在下面的例子中,我们使用 `withTimeZone` 将安排在伦敦时间的活动转换为悉尼当地时间。然后,我们使用 `since` 和 `until` 计算从圣诞节到活动的时间。`since` 测量从较晚日期到较早日期的时间,而 `until` 计算从较早日期到较晚日期的时间:

    const scheduledTime = Temporal.ZonedDateTime.from({
      year: 2025,
      month: 1,
      day: 4,
      hour: 10,
      minute: 30,
      timeZone: 'Europe/London',
    });
    console.log(scheduledTime.toString()); // 2025-01-04T10:30:00+00:00[Europe/London]
    const localEventTime = scheduledTime.withTimeZone('Australia/Sydney');
    console.log(localEventTime.toString()); // 2025-01-04T21:30:00+11:00[Australia/Sydney]
    const christmasDate = Temporal.ZonedDateTime.from({ year: 2024, month: 12, day: 25 ,timeZone: 'Australia/Sydney'});
    const timeUntilScheduled = christmasDate.until(scheduledTime, { largestUnit: 'hours' });
    const timeSinceChristmas = scheduledTime.since(christmasDate, { largestUnit: 'hours' });
    console.log(`Duration until scheduled: ${timeUntilScheduled.hours} hours, from Christmas ${timeSinceChristmas.hours} hours`); 
    //Duration until scheduled: 261 hours, from Christmas 261 hours

    我们还可以使用 Temporal API 中的“compare”辅助方法对事件时间进行排序以进行调度:

    const sessionA = Temporal.PlainDateTime.from({
        year: 2024,
        day: 20,
        month: 11,
        hour: 8,
        minute: 45,
      });
      const lunch = Temporal.PlainDateTime.from({
        year: 2024,
        day: 20,
        month: 11,
        hour: 11,
        minute: 30,
      });
      const sessionB = Temporal.PlainDateTime.from({
        year: 2024,
        day: 20,
        month: 11,
        hour: 10,
        minute: 0,
      });
      const sessionC = Temporal.PlainDateTime.from({
        year: 2024,
        day: 20,
        month: 11,
        hour: 13,
        minute: 0,
      });
    // The events can be sorted chronologically or in reverse order
      const sorted = Array.from([sessionC, lunch, sessionA, sessionB]).sort(
        Temporal.PlainDateTime.compare
      );
      console.log('sorted:', sorted);
      console.log('sorted reverse:', sorted.reverse());
    // sorted: ["2024-11-20T08:45:00","2024-11-20T10:00:00","2024-11-20T11:30:00","2024-11-20T13:00:00"] 
    // sorted reverse: ["2024-11-20T13:00:00","2024-11-20T11:30:00","2024-11-20T10:00:00","2024-11-20T08:45:00"]

    非公历

    虽然公历是最广泛使用的日历,但有时也需要使用希伯来历、伊斯兰历、佛教历、印度历、中国历和日本历来表示具有文化或宗教意义的日期。Temporal API 支持多种日历系统:

    const eventDate = Temporal.PlainDate.from({ year: 2024, month: 12, day: 15 });
      console.log(`Japanese calendar:${eventDate.withCalendar("japanese").toLocaleString('en-US', { calendar: 'japanese' })}`); //Japanese calendar:12/15/6 R 
      console.log(`Hebrew calendar:${eventDate.withCalendar("hebrew").toLocaleString('en-US', { calendar: 'hebrew' })}`); //Hebrew calendar:14 Kislev 5785 
      console.log(`Islamic calendar:${eventDate.withCalendar("islamic").toLocaleString('en-US', { calendar: 'islamic' })}`); //Islamic calendar:6/14/1446 AH
      console.log(`Buddhist calendar:${eventDate.withCalendar("buddhist").toLocaleString('en-US', { calendar: 'buddhist' })}`); //Buddhist calendar:12/15/2567 BE

    在上面的例子中,我们使用 `withCalendar` 将日期转换为各种日历系统。它有助于以不同地区用户的本地格式显示日期。

    从 Moment.js 或 date-fns 迁移到 Temporal

    如前所述,在撰写本文时,Temporal API 仍处于第 3 阶段提案,这意味着它可能会发生更改,并且尚未得到所有平台的支持。但是,您现在可以开始尝试它,为将来的采用做好准备。从 Moment.js 或 date-fns 迁移到 Temporal API 时,请考虑以下步骤:

  • 确定您在 Moment.js 或 date-fns 中使用的关键功能,并找到它们的 Temporal 等效项
  • 对于较大的项目,请逐步迁移。从日期创建和格式等核心功能开始,然后处理时区等复杂功能
  • 考虑构建一个日期时间服务来封装所有日期和时间函数,以简化维护和未来的更新
  • 编写全面的单元测试以确保迁移保留原始行为
  • 结论

    JavaScript 的 Temporal API 提供对不变性、时区管理、精确持续时间和丰富辅助方法的原生支持,无需额外依赖。一旦 Temporal 成为 JavaScript 的核心部分,采用它将使您的代码库保持现代、可扩展,并为长期可靠的日期处理做好准备。

    希望本文对您有所帮助。您可以在此处找到代码片段。

    LogRocket:通过理解上下文更轻松地调试 JavaScript 错误

    调试代码总是一项繁琐的任务。但是你对错误的了解越多,修复它们就越容易。

    LogRocket 可让您以新颖独特的方式了解这些错误。我们的前端监控解决方案可跟踪用户与您的 JavaScript 前端的互动情况,让您能够准确了解用户的操作导致错误。

    LogRocket Signup

    LogRocket 记录控制台日志、页面加载时间、堆栈跟踪、带有标头 + 正文的慢速网络请求/响应、浏览器元数据和自定义日志。了解 JavaScript 代码的影响从未如此简单!

    免费试用。