Работа с датами в JavaScript традиционно связана с объектом
Date, который является мутабельным. Это
означает, что большинство его методов изменяют исходный экземпляр, а не
создают новый. Такое поведение часто становится источником
трудноуловимых ошибок, особенно в приложениях со сложной логикой
состояния.
Библиотека date-fns строится на противоположном принципе:
любая операция над датой возвращает новый объект
Date, не изменяя исходный. Этот подход формирует
предсказуемую модель работы и устраняет целый класс проблем, связанных с
побочными эффектами.
Классический пример мутации — методы установки значений:
const date = new Date(2024, 0, 10);
date.setDate(20);
console.log(date); // 20 января 2024
В этом случае исходный объект был изменён. Если ссылка на него используется в другом месте программы, изменение проявится там же, что может привести к неожиданному поведению.
Особенно опасно это в следующих сценариях:
Все функции 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); // новая дата
В реактивных системах состояние должно изменяться предсказуемо. Мутация объектов приводит к тому, что изменения могут не фиксироваться системой сравнения ссылок.
Пример проблемы:
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 задаёт строгую и предсказуемую модель: