Избежание мутаций

Работа с датами в JavaScript традиционно связана с объектом Date, который является мутабельным. Это означает, что большинство его методов изменяют исходный экземпляр, а не создают новый. Такое поведение часто становится источником трудноуловимых ошибок, особенно в приложениях со сложной логикой состояния.

Библиотека date-fns строится на противоположном принципе: любая операция над датой возвращает новый объект Date, не изменяя исходный. Этот подход формирует предсказуемую модель работы и устраняет целый класс проблем, связанных с побочными эффектами.


Мутации в нативном JavaScript Date

Классический пример мутации — методы установки значений:

const date = new Date(2024, 0, 10);

date.setDate(20);

console.log(date); // 20 января 2024

В этом случае исходный объект был изменён. Если ссылка на него используется в другом месте программы, изменение проявится там же, что может привести к неожиданному поведению.

Особенно опасно это в следующих сценариях:

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

Принцип неизменяемости в date-fns

Все функции date-fns работают по принципу:

входная дата остаётся неизменной, результатом всегда является новый экземпляр Date.

Пример:

import { addDays } from "date-fns";

const date = new Date(2024, 0, 10);
const result = addDays(date, 5);

console.log(date);   // 10 января 2024
console.log(result); // 15 января 2024

Исходный объект не изменился. Это фундаментальное отличие от нативного API.


Функции, которые создают новые объекты

Практически все операции в библиотеке следуют этому правилу.

Добавление и вычитание времени

import { addMonths, subDays } from "date-fns";

const date = new Date(2024, 0, 10);

const next = addMonths(date, 2);
const prev = subDays(date, 5);

Каждый результат — новый объект. Даже при цепочке вызовов исходные данные сохраняются.


Округление дат

import { startOfDay, endOfMonth } from "date-fns";

const date = new Date(2024, 0, 10, 15, 30);

const start = startOfDay(date);
const end = endOfMonth(date);

Здесь важно, что даже операции, которые «обнуляют» время, не модифицируют оригинал.


Установка значений

import { set } from "date-fns";

const date = new Date(2024, 0, 10);

const upd ated = se t(date, {
  year: 2025,
  month: 5,
  date: 1
});

Оригинальная дата остаётся неизменной, несмотря на замену всех компонентов.


Почему неизменяемость критична

Предсказуемость поведения

Если функция не изменяет входные данные, её результат зависит только от аргументов. Это упрощает отладку и тестирование:

const a = new Date(2024, 0, 10);

const b = addDays(a, 3);
const c = addDays(a, 3);

console.log(b.getTime() === c.getTime()); // true

Поведение стабильно независимо от контекста вызова.


Безопасность при повторном использовании данных

Рассмотрим ошибочный подход с мутацией:

function shiftByWeek(date) {
  date.setDate(date.getDate() + 7);
  return date;
}

const original = new Date(2024, 0, 10);
const shifted = shiftByWeek(original);

console.log(original === shifted); // true (тот же объект)

Любой последующий код, использующий original, получает уже изменённое значение.

В стиле date-fns это исключено:

import { addWeeks } from "date-fns";

const original = new Date(2024, 0, 10);
const shifted = addWeeks(original, 1);

console.log(original); // не изменён
console.log(shifted);  // новая дата

Работа в React и других UI-фреймворках

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

Пример проблемы:

const [date, setDate] = useState(new Date());

function upd ate() {
  date.setDate(date.getDate() + 1);
  setDate(date);
}

Здесь объект не меняется по ссылке, поэтому обновление может не сработать корректно.

Корректный вариант с использованием неизменяемых функций:

import { addDays } from "date-fns";

function upd ate() {
  setDate(prev => addDays(prev, 1));
}

Каждый вызов создаёт новый объект, гарантируя обновление состояния.


Композиция функций без побочных эффектов

Неизменяемость позволяет свободно комбинировать функции:

import { addDays, format, startOfMonth } from "date-fns";

const date = new Date(2024, 0, 10);

const result = format(
  addDays(
    startOfMonth(date),
    10
  ),
  "yyyy-MM-dd"
);

Каждый шаг возвращает новый объект, который передаётся дальше. Исходная дата не затрагивается ни на одном этапе.


Сравнение с императивным подходом

Императивный стиль с мутациями:

const date = new Date(2024, 0, 10);

date.setMonth(date.getMonth() + 1);
date.setDate(1);
date.setHours(0, 0, 0, 0);

Каждая строка изменяет один и тот же объект. Логика распределена по времени выполнения и зависит от порядка операций.

Функциональный стиль:

import { addMonths, se t, startOfDay } from "date-fns";

const result = startOfDay(
  se t(
    addMonths(new Date(2024, 0, 10), 1),
    { date: 1 }
  )
);

Здесь каждый шаг изолирован, и результат каждого вызова можно рассматривать отдельно.


Клонирование как скрытая основа модели

Хотя разработчик не вызывает явное клонирование, оно происходит внутри функций date-fns.

Упрощённо это можно представить так:

function safeAddDays(date, amount) {
  const copy = new Date(date.getTime());
  copy.setDate(copy.getDate() + amount);
  return copy;
}

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

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

Работа с массивами дат

При обработке коллекций неизменяемость особенно важна:

import { addDays } from "date-fns";

const dates = [
  new Date(2024, 0, 10),
  new Date(2024, 0, 11)
];

const shifted = dates.map(d => addDays(d, 7));

Исходный массив остаётся полностью неизменным. Это позволяет безопасно повторно использовать данные в разных вычислениях.


Влияние на архитектуру кода

Использование неизменяемых операций формирует архитектурные свойства:

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

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


Ошибки при попытке «обойти» неизменяемость

Иногда разработчики пытаются комбинировать подходы:

import { addDays } from "date-fns";

const date = new Date(2024, 0, 10);

addDays(date, 5);
date.setFullYear(2025);

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

Согласованное использование date-fns исключает необходимость в таких смешанных стратегиях.


Поведение при копировании ссылок

Важно учитывать, что даже при передаче объектов между функциями неизменяемость сохраняет безопасность:

function process(date) {
  const result = addDays(date, 10);
  return result;
}

const original = new Date(2024, 0, 10);
const a = process(original);
const b = process(original);

Обе переменные a и b независимы, несмотря на использование одной исходной ссылки.


Итоговое свойство модели

Неизменяемость в date-fns задаёт строгую и предсказуемую модель:

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