Angularшлях до Strong Junior
Модулі / JavaScript: основа / Посилання проти копії
🔥 0

Фундамент Ф1 · урок 6 з 12

Посилання проти копії

Чому в Angular tasks().push(task) «не працює», хоча масив змінився? Чому два однакові обʼєкти не рівні між собою? Чому функція «зіпсувала» дані, які їй передали? Відповідь на все це одна — посилання. Це, мабуть, найважливіша ідея фундаменту для Angular: на ній стоять сигнали, OnPush і вся тема незмінності.

Примітиви копіюються, обʼєкти — ні

Коли присвоюєш число чи рядок іншій змінній, копіюється саме значення — як ксерокопія записки. Змінив копію — оригінал не постраждав.

З обʼєктами (і масивами, бо масив — теж обʼєкт) інакше. Обʼєкт живе окремо, у памʼяті, а змінна тримає лише посилання на нього — по суті, адресу. Присвоєння копіює адресу, а не обʼєкт. Дві змінні з однією адресою — як двоє людей з ключами від однієї квартири: що один переставив, те другий і побачить.

Вгадай, що виведуть обидва console.log, потім запусти.

Порівняння: === порівнює адреси

Для обʼєктів === перевіряє не вміст, а чи це той самий обʼєкт:

{ id: 1 } === { id: 1 };      // false — два різні обʼєкти з однаковим вмістом

const t = { id: 1 };
const same = t;
t === same;                   // true — одна адреса

[] === [];                    // false — те саме з масивами

Це не баг, а саме те, на що спирається Angular: перевірити «чи змінились дані» через === — миттєво, навіть для величезного обʼєкта. Але тільки якщо зміна завжди означає новий обʼєкт.

Функції отримують посилання

Передаючи обʼєкт у функцію, ти передаєш адресу. Тож функція може змінити дані того, хто її викликав, — і це неприємний сюрприз:

function markDone(task) {
  task.done = true;           // змінює ОРИГІНАЛ, не копію
  return task;
}

const report = { title: 'Звіт', done: false };
markDone(report);
report.done;                  // true — функція тихо змінила наші дані

Функція, яка змінює свої аргументи, — нечиста (урок 3). Такий код важко тестувати, і він ламає оновлення екрана в Angular.

Як зробити копію

Spread ... «розкладає» властивості обʼєкта чи елементи масиву в новий обʼєкт або масив:

const copy = { ...task };                    // новий обʼєкт з тими самими полями
const updated = { ...task, done: true };     // копія з одним зміненим полем
const listCopy = [...tasks];                 // новий масив з тими самими елементами

Порядок важливий: поля, написані після spread, перезаписують скопійовані. Тому { ...task, done: true } означає «усе як у task, але done — true».

Але spread копіює лише верхній рівень. Якщо всередині є інший обʼєкт чи масив, копіюється посилання на нього — і він лишається спільним:

Для повної, глибокої копії є structuredClone(obj). Але в Angular найчастіше вона не потрібна: ти копіюєш лише те, що змінюєш, а решту залишаєш спільним — так дешевше.

Незмінність: не змінюй, а створюй нове

Правило, яке Angular вимагає від даних стану: ніколи не мутуй — завжди створюй нову версію. Ось три операції, які ти писатимеш постійно, — і тепер у кожній зрозуміла кожна частина:

// додати задачу — новий масив зі старими елементами й новим у кінці
const added = [...tasks, newTask];

// видалити — новий масив без задачі з цим id (filter, урок 5)
const removed = tasks.filter(t => t.id !== id);

// змінити одну — map проходить усі; потрібну замінює копією зі зміною,
// решту повертає як є (тернарний — урок 2, spread — цей урок)
const updated = tasks.map(t => t.id === id ? { ...t, done: true } : t);

Останній рядок — той самий, що ти бачив в уроці про сигнали в модулі 2. Тепер він має читатися без перекладу.

Навіщо це Angular

Сигнал перевіряє, чи змінилось значення, через порівняння посилань. Компонент з OnPush перевіряє, чи змінились вхідні дані, — теж через посилання. Якщо ти зробив tasks().push(task), масив той самий, адреса та сама — і для Angular нічого не сталось: екран не оновиться. Якщо ж tasks.update(list => [...list, task]) — масив новий, адреса нова, і Angular одразу бачить зміну.

Функція toggle зараз мутує вхідний масив — так у Angular не можна. Перепиши її незмінно: вона має повертати новий масив, де в задачі з потрібним id перемкнено done, а оригінал лишається без змін. Чотири перевірки внизу підкажуть, чи вийшло.

return list.map(t => t.id === id ? { ...t, done: !t.done } : t);
Типові помилки
  • const copy = original і думка, що це копія. Це друге посилання на той самий обʼєкт.
  • Spread і віра в глибоку копію — вкладені обʼєкти лишаються спільними.
  • push, splice, sort або obj.x = … на даних стану в Angular — екран не оновлюється.
  • Порівняння вмісту обʼєктів через === — для цього порівнюють поля: a.id === b.id.

Підсумок

  • Примітиви копіюються значенням, обʼєкти й масиви — посиланням: дві змінні можуть вказувати на одну річ.
  • === для обʼєктів порівнює адреси, а не вміст.
  • Копія — { ...obj } і [...arr], але лише верхній рівень. Глибока — structuredClone.
  • Angular бачить зміну лише за новим посиланням: додати — [...list, x], видалити — filter, змінити — map + { ...t, поле }.