Learn JavaScript

Мутации в JavaScript: почему сломалось само

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

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() делают поверхностную копию.
  • Чтобы избежать случайных мутаций, данные копируют перед изменением и копируют нужную ветку для вложенных структур.