Immutability объектов moment

Библиотека Moment.js построена вокруг объекта moment, который инкапсулирует дату и время и предоставляет богатый набор методов для их преобразования. Ключевая особенность внутреннего поведения таких объектов — изменяемость состояния (mutable behavior). Это означает, что большинство операций над экземпляром moment не создают новый объект, а изменяют текущий.

Такой подход исторически был выбран для удобства цепочек вызовов и сокращения количества временных объектов, однако он приводит к важным последствиям при проектировании логики работы с датами.


Изменяемость как базовое свойство объекта moment

Экземпляр moment хранит внутреннее состояние даты и времени, включая:

  • год, месяц, день
  • часы, минуты, секунды, миллисекунды
  • таймзону (в зависимости от конфигурации)
  • дополнительные метаданные

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

Пример поведения:

const m = moment("2024-01-01");

m.add(7, "days");

console.log(m.format("YYYY-MM-DD")); // 2024-01-08

Метод add изменяет исходный объект m. Это фундаментальное отличие от функционально-иммутабельных моделей работы с датами.


Методы, изменяющие состояние объекта

Большая часть API Moment.js относится к категории mutating-методов. Они напрямую модифицируют текущий экземпляр.

Основные мутирующие методы

  • add
  • subtract
  • set
  • startOf
  • endOf
  • utc
  • local
  • zone (в некоторых конфигурациях)

Пример цепочки вызовов:

const m = moment("2024-01-01");

m.add(1, "month")
 .subtract(2, "days")
 .startOf("day");

Все операции выполняются над одним объектом. Промежуточные состояния не сохраняются, если явно не клонировать объект.


Проблема разделяемых ссылок

Изменяемость становится критичной при передаче объектов между функциями. Поскольку moment передаётся по ссылке, любая функция может изменить исходное значение.

Пример побочного эффекта:

function shiftDate(date) {
    date.add(3, "days");
}

const original = moment("2024-01-01");

shiftDate(original);

console.log(original.format("YYYY-MM-DD")); // 2024-01-04

Функция изменила внешний объект, хотя это не очевидно из сигнатуры.


Клонирование объектов moment

Для предотвращения побочных эффектов используется метод clone(). Он создаёт новый экземпляр с тем же состоянием.

const original = moment("2024-01-01");
const copy = original.clone();

copy.add(10, "days");

console.log(original.format("YYYY-MM-DD")); // 2024-01-01
console.log(copy.format("YYYY-MM-DD"));     // 2024-01-11

Клонирование является основным механизмом обеспечения логической неизменяемости при работе с Moment.js.


Функциональный стиль через clone

Иммутабельное поведение можно имитировать, всегда возвращая новый объект после операции.

const base = moment("2024-01-01");

const result = base.clone()
    .add(5, "days")
    .startOf("month")
    .subtract(1, "hour");

Такой подход делает код предсказуемым и снижает вероятность скрытых изменений состояния.


Методы, возвращающие новый объект

Несмотря на общую мутабельность, некоторые методы возвращают новый экземпляр вместо изменения текущего.

К таким относятся:

  • moment() как конструктор
  • clone() как явное копирование
  • moment.utc() при создании нового объекта
  • moment.parseZone() в некоторых сценариях

Однако важно различать создание нового объекта и трансформацию существующего: большинство трансформаций всё равно остаются мутабельными.


Цепочки вызовов и скрытая мутабельность

Цепочный API создаёт иллюзию функционального стиля, но фактически изменяет один объект последовательно.

const m = moment("2024-01-01");

m.add(1, "day")
 .add(1, "month")
 .year(2030);

Результат — один объект с финальным состоянием. Промежуточные даты не сохраняются.

Это может приводить к ошибкам, если цепочка используется повторно:

const base = moment("2024-01-01");

const a = base.add(1, "day");
const b = base.add(1, "day");

console.log(a.format()); // уже изменённое значение
console.log(b.format()); // ещё более изменённое значение

Влияние мутабельности на архитектуру кода

Использование Moment.js в больших приложениях требует строгого контроля владения объектами.

Типовые проблемы:

  • непредсказуемые изменения состояния в разных слоях приложения
  • ошибки при кэшировании дат
  • нарушение чистоты функций
  • сложности при тестировании

Чистые функции с датами требуют обязательного клонирования входных значений:

function getNextWeek(date) {
    return date.clone().add(7, "days");
}

Сравнение логики “до и после clone”

Без клонирования:

const d = moment("2024-01-01");
const result = d.add(10, "days");

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

С клонированием:

const d = moment("2024-01-01");
const result = d.clone().add(10, "days");

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

В первом случае d и result — один объект. Во втором — разные экземпляры.


Особенности форматирования и чтения состояния

Методы форматирования не изменяют объект, но используют его текущее состояние.

const m = moment("2024-01-01");

m.add(1, "day");

console.log(m.format("YYYY-MM-DD"));

Здесь format только читает состояние. Это важно для разделения операций:

  • mutating методы — изменяют состояние
  • formatting/reading методы — не изменяют состояние

Роль мутабельности в историческом дизайне библиотеки

Архитектура Moment.js ориентирована на эпоху, когда:

  • производительность создания объектов была критична
  • функциональные паттерны не были доминирующими
  • API стремились к краткости и цепочкам вызовов

Поэтому mutability стала компромиссом между удобством и предсказуемостью.


Практика безопасной работы с изменяемыми датами

Для контроля состояния применяется несколько устойчивых подходов:

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

Пример изолированного использования:

function formatDeadline(date) {
    const safe = date.clone();
    return safe.add(2, "days").format("YYYY-MM-DD");
}

Поведение при многократных преобразованиях

Каждое последующее изменение опирается на уже изменённое состояние:

const m = moment("2024-01-01");

m.add(1, "day");   // 2024-01-02
m.add(1, "day");   // 2024-01-03
m.add(1, "day");   // 2024-01-04

Отсутствие промежуточных копий делает результат линейно зависимым от порядка вызовов.


Итоговая модель поведения объекта moment

Поведение объекта Moment.js можно описать через три принципа:

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

Такая модель требует дисциплины в обращении с объектами, особенно в кодовых базах, где даты передаются между слоями приложения или используются в повторных вычислениях.