Иногда код выглядит нормально, но данные вдруг меняются “сами”. Обычно причина в мутациях - изменениях объектов и массивов через общую ссылку.
1) Примитивы и объекты ведут себя по-разному
Примитивы копируются по значению
Примитивы - это number, string, boolean, null, undefined, symbol, bigint.
let a = 10;
let b = a;
b = 99;
console.log(a); // 10
console.log(b); // 99
b получил копию значения, поэтому изменение b не влияет на a.
Объекты и массивы передаются по ссылке
Объекты и массивы - ссылочные типы.
const a = { n: 1 };
const b = a;
b.n = 99;
console.log(a.n); // 99
console.log(b.n); // 99
a и b указывают на один и тот же объект. Изменение через одну переменную видно через другую.
То же самое с массивами:
const a = [1, 2, 3];
const b = a;
b.push(4);
console.log(a); // [1, 2, 3, 4]
console.log(b); // [1, 2, 3, 4]
2) Почему это выглядит как “сломалось само”
Частый сценарий:
- данные кладутся в переменную
- потом где-то в другом месте кода массив/объект меняется
- изменения “всплывают” там, где этого не ожидалось
Пример с “настройками”:
const defaultSettings = { theme: "light" };
function enableDark(settings) {
settings.theme = "dark";
return settings;
}
const userSettings = defaultSettings;
enableDark(userSettings);
console.log(defaultSettings.theme); // "dark"
Ожидание - изменится только userSettings. Но на самом деле это одна и та же ссылка.
3) Мутация - это не всегда плохо
Мутация - это просто факт: объект изменился. Иногда это удобно и нормально, особенно в небольших скриптах.
Проблемы начинаются, когда:
- одна и та же структура данных используется в разных местах
- изменения происходят “глубоко” и сложно отследить источник
4) Поверхностная копия и “скрытая” мутация
Spread и slice() делают поверхностную копию. Это значит:
- копируется только верхний уровень
- вложенные объекты остаются общими по ссылке
const a = { user: { name: "Sam" } };
const b = { ...a };
b.user.name = "Alex";
console.log(a.user.name); // "Alex"
console.log(b.user.name); // "Alex"
Сверху b - другой объект, но b.user и a.user - одна и та же ссылка.
То же самое с массивами объектов:
const a = [{ n: 1 }, { n: 2 }];
const b = a.slice();
b[0].n = 99;
console.log(a[0].n); // 99
console.log(b[0].n); // 99
5) Как избегать случайных мутаций
Делать копию перед изменением
Массив:
const nums = [1, 2, 3];
const next = [...nums, 4];
console.log(nums); // [1, 2, 3]
console.log(next); // [1, 2, 3, 4]
Объект:
const user = { name: "Sam", age: 20 };
const next = { ...user, age: 21 };
console.log(user); // { name: "Sam", age: 20 }
console.log(next); // { name: "Sam", age: 21 }
Для вложенных структур копировать нужную ветку
const state = {
user: { name: "Sam", age: 20 },
settings: { theme: "light" }
};
const next = {
...state,
user: {
...state.user,
age: 21
}
};
console.log(state.user.age); // 20
console.log(next.user.age); // 21
Копируется только та часть, которую нужно изменить.
6) Методы, которые часто мутируют
Полезно знать, какие операции меняют данные:
- массив:
push,pop,shift,unshift,splice,sort,reverse - объект: прямое присваивание
obj.key = ...,delete obj.key
И какие обычно не мутируют:
- массив:
map,filter,slice,concat - объект: создание копий через
{ ...obj }
Это не полный список, но полезная база.
Частые ошибки
1) Использование sort без копии
sort() меняет массив.
const nums = [3, 1, 2];
const sorted = nums.sort((a, b) => a - b);
console.log(nums); // [1, 2, 3]
console.log(sorted); // [1, 2, 3]
Если нужна сортировка без изменения исходного:
const nums = [3, 1, 2];
const sorted = nums.slice().sort((a, b) => a - b);
console.log(nums); // [3, 1, 2]
console.log(sorted); // [1, 2, 3]
2) Ожидание “глубокой” копии от spread
Spread копирует только верхний уровень, вложенные ссылки сохраняются.
Что стоит запомнить
- Примитивы копируются по значению, объекты и массивы передаются по ссылке.
- Если две переменные указывают на один объект, изменения видны через обе.
- Spread и
slice()делают поверхностную копию. - Чтобы избежать случайных мутаций, данные копируют перед изменением и копируют нужную ветку для вложенных структур.