Мутабельность Date при конвертации

Одной из ключевых причин появления js-joda стала фундаментальная проблема встроенного объекта Date в JavaScript — его мутабельность и связанные с этим побочные эффекты при передаче, копировании и преобразовании значений времени.

В стандартной модели Date представляет собой изменяемый объект: любые операции модификации меняют сам экземпляр. В противоположность этому, типы js-joda реализуют неизменяемую (immutable) модель, где каждая операция возвращает новый объект, не затрагивая исходный.

При конвертации между этими двумя моделями возникает критическая точка, в которой часто появляются ошибки, связанные с неожиданным изменением состояния или потерей точности.


Поведение Date как источника мутабельности

Встроенный Date позволяет изменять своё состояние через набор методов:

const d = new Date(2024, 0, 1);

d.setFullYear(2025);
d.setMonth(5);

После выполнения этих операций исходный объект уже содержит новое значение времени. Это означает:

  • объект нельзя безопасно передавать по ссылке без риска изменения;
  • функции могут непредсказуемо изменять входные данные;
  • копирование требует явного создания нового экземпляра.

Даже операции, которые выглядят как «получение нового значения», часто скрыто мутируют объект:

const d = new Date(2024, 0, 1);
const copy = d;
copy.setDate(10);

console.log(d); // уже изменён

Такое поведение создаёт проблемы при работе с временными вычислениями, особенно в сложных доменных моделях.


Модель неизменяемости в js-joda

В js-joda все классы времени (например, LocalDate, LocalDateTime, Instant) являются неизменяемыми.

import { LocalDate } from '@js-joda/core';

const date = LocalDate.of(2024, 1, 1);
const upd ated = date.plusDays(10);

console.log(date.toString());    // 2024-01-01
console.log(upd ated.toString()); // 2024-01-11

Ключевая характеристика:

  • исходный объект никогда не изменяется;
  • каждая операция создаёт новый объект;
  • состояние гарантированно стабильно во всех частях программы.

Проблема конвертации: точка разрыва моделей

Конвертация между Date и js-joda выглядит простой, но именно здесь возникает наиболее частый источник ошибок.

Из Date в LocalDate

import { LocalDate } from '@js-joda/core';

const jsDate = new Date(2024, 0, 15);

const localDate = LocalDate.of(
  jsDate.getFullYear(),
  jsDate.getMonth() + 1,
  jsDate.getDate()
);

На этом этапе важно понимать:

  • Date может быть уже изменён ранее в коде;
  • значения извлекаются «на момент вызова»;
  • любые последующие изменения Date не влияют на LocalDate.

Потенциальная ловушка с мутацией исходного объекта

function toLocalDate(date) {
  date.setDate(date.getDate() + 1); // побочный эффект
  return LocalDate.of(
    date.getFullYear(),
    date.getMonth() + 1,
    date.getDate()
  );
}

Проблема:

  • функция не только конвертирует, но и изменяет входной объект;
  • вызывающий код теряет контроль над состоянием;
  • результат зависит от порядка вызовов.

Такое поведение особенно опасно в асинхронных системах и при работе с кэшированием.


Обратная конвертация: js-jodaDate

При преобразовании из неизменяемого объекта в мутабельный требуется явное создание нового экземпляра:

import { LocalDate } from '@js-joda/core';

const localDate = LocalDate.of(2024, 1, 15);

const jsDate = new Date(
  localDate.year(),
  localDate.monthValue() - 1,
  localDate.dayOfMonth()
);

Здесь важно учитывать:

  • Date создаётся как независимый объект;
  • никакой связи с исходным LocalDate не сохраняется;
  • любые дальнейшие изменения Date не влияют на js-joda объект.

Побочные эффекты при совместном использовании моделей

Смешивание Date и js-joda в одном коде часто приводит к скрытым ошибкам.

Пример некорректного кеширования

const cache = new Map();

function getDate(key, date) {
  cache.se t(key, date);
  return LocalDate.of(
    date.getFullYear(),
    date.getMonth() + 1,
    date.getDate()
  );
}

Проблема заключается в том, что в cache сохраняется ссылка на изменяемый объект. Если позже date будет изменён, значение в кэше также «изменится».


Правильный подход: разрыв мутабельности

function getDate(key, date) {
  const snapshot = new Date(date.getTime());

  cache.se t(key, snapshot);

  return LocalDate.of(
    snapshot.getFullYear(),
    snapshot.getMonth() + 1,
    snapshot.getDate()
  );
}

Здесь создаётся независимая копия, исключающая влияние внешних изменений.


Временные зоны и усиление проблемы мутабельности

Date всегда хранит момент времени в UTC, но отображает его в локальной временной зоне. Это добавляет ещё один уровень неопределённости при конвертации.

const d = new Date('2024-01-01T00:00:00Z');

При извлечении компонентов:

d.getDate();
d.getMonth();

значения зависят от локальной зоны исполнения.

В js-joda модель разделяет:

  • LocalDate — календарная дата без зоны;
  • Instant — абсолютный момент времени;
  • ZonedDateTime — дата-время с зоной.

Это устраняет неоднозначность, но требует аккуратной конвертации.


Ошибки при неявной мутации в функциях

Одной из наиболее распространённых проблем является передача Date в функции, которые не документируют своё поведение.

function shiftByDay(date) {
  date.setDate(date.getDate() + 1);
  return date;
}

Вызов:

const d = new Date();
const result = shiftByDay(d);

console.log(d === result); // true

Вся система начинает зависеть от скрытых изменений.


Безопасная стратегия взаимодействия

При работе с js-joda и Date одновременно используется принцип:

мутабельные объекты — только на границах системы

Внутри бизнес-логики:

  • используются только immutable-типы js-joda;
  • любые преобразования происходят на входе и выходе.

Пример:

function process(inputDate) {
  const local = LocalDate.of(
    inputDate.getFullYear(),
    inputDate.getMonth() + 1,
    inputDate.getDate()
  );

  const result = local.plusDays(7);

  return new Date(
    result.year(),
    result.monthValue() - 1,
    result.dayOfMonth()
  );
}

Такой подход гарантирует:

  • отсутствие скрытых изменений;
  • предсказуемость вычислений;
  • изоляцию доменной логики от внешних эффектов.

Разница в философии моделей

Date отражает императивную модель времени:

  • объект можно изменять;
  • состояние живёт во времени;
  • побочные эффекты допустимы.

js-joda реализует функциональный подход:

  • состояние неизменно;
  • операции чистые;
  • результат всегда новый объект.

Конвертация между ними — это не просто техническая операция, а переход между двумя различными парадигмами работы со временем.