Примітиви та об'єкти в пам'яті
JavaScript тримає дані у двох місцях пам'яті: у стеку (stack) лежать примітиви, а в купі (heap) лежать об'єкти, і в стеку на них зберігається лише посилання. Саме тому примітив копіюється за значенням, а об'єкт, за посиланням.
Теорія
TL;DR
- Стек, це швидка пам'ять для примітивів і для посилань на об'єкти.
- Купа, це гнучка пам'ять, де лежать самі об'єкти, масиви, функції,
Date,Map,Set. - Примітив копіюється за значенням: у нової змінної власна копія даних.
- Об'єкт копіюється за посиланням: обидві змінні дивляться на одну ділянку купи.
- Spread розриває посилання лише на верхньому рівні; для вкладених структур потрібна глибока копія (
structuredClone()).
Швидкий приклад
// примітив: копія значення
let a = 10;
let b = a;
b = 20;
console.log(a, b); // 10 20
// об'єкт: копія посилання
const user1 = { name: 'Tim' };
const user2 = user1;
user2.name = 'Alex';
console.log(user1.name, user2.name); // "Alex" "Alex"Примітиви: стек і копіювання за значенням
Примітиви, це string, number, boolean, null, undefined, symbol, bigint. Вони зберігаються у стеку, швидкій ділянці пам'яті, де лежать самі значення, а не посилання.
let a = 10;
let b = a; // копіюється значення 10
b = 20;
console.log(a); // 10
console.log(b); // 20У пам'яті (stack):
+---------------+
| a = 10 |
| b = 20 | <- окрема копія
+---------------+Кожна змінна зберігає власне значення, тому зміни в одній не впливають на іншу.
Об'єкти: купа і копіювання за посиланням
Об'єкти, а разом з ними масиви, функції, дати та інші складні структури, зберігаються в купі, області пам'яті для «гнучких» структур. А в стеку лежить лише посилання (вказівник) на цей об'єкт.
const user1 = { name: 'Tim' };
const user2 = user1; // копіюється посилання (вказівник)
user2.name = 'Alex';
console.log(user1.name); // "Alex"У пам'яті:
STACK (стек) HEAP (купа)
+---------------+ +----------------------+
| user1 --+ | | { name: "Alex" } |
| user2 --+---------> +----------------------+
+---------------+Обидві змінні вказують на один і той самий об'єкт, тому зміна через user2 видно і в user1.
Поверхнева копія і чому вона не рятує вкладені об'єкти
Спеціальні способи копіювання створюють нове посилання:
const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // оператор spread
arr2.push(4);
console.log(arr1); // [1, 2, 3]
console.log(arr2); // [1, 2, 3, 4]У пам'яті:
arr1 --> [1, 2, 3]
arr2 --> [1, 2, 3, 4]Різні посилання, тож зміни не зачіпають оригінал.
Але якщо об'єкт вкладений, spread робить лише поверхневу копію (shallow copy):
const user1 = { name: 'Tim', address: { city: 'Kyiv' } };
const user2 = { ...user1 };
user2.address.city = 'London';
console.log(user1.address.city); // "London"Чому так:
user1.address --+
user2.address --+--> вказують на один і той самий внутрішній об'єктТобто внутрішні об'єкти все ще копіюються за посиланням.
Глибоке копіювання (deep copy)
Щоб отримати повністю незалежну копію, треба «розірвати» всі посилання всередині.
Старий спосіб:
const user1 = { name: 'Tim', address: { city: 'Kyiv' } };
const user2 = JSON.parse(JSON.stringify(user1));
user2.address.city = 'Berlin';
console.log(user1.address.city); // "Kyiv"Сучасний спосіб (Node.js 17+ та браузери приблизно з 2022 року):
const user2 = structuredClone(user1);structuredClone() робить глибоку копію і не лишає спільних посилань узагалі.
Stack проти heap і підсумкова таблиця
STACK (швидка пам'ять)
------------------------
a = 5
b = "Hello"
user -> (посилання на об'єкт)
HEAP (гнучка пам'ять)
------------------------
{ name: "Tim", age: 25 }Стек зберігає прості значення і посилання, а купа, самі складні об'єкти.
Побутова аналогія:
- Примітив, це аркуш паперу з числом: зробив копію, отримав ще один аркуш, і зміна числа на ньому не псує оригінал.
- Об'єкт, це тека з документами: якщо ти даєш колезі посилання на ту саму теку, ви обидва бачите одні й ті самі файли.
| Тип даних | Де зберігається | Як копіюється | Що передається |
|---|---|---|---|
Примітиви (string, number, boolean, null, undefined, symbol, bigint) | Stack | За значенням | Саме значення |
Об'єкти (object, array, function, Date, Map, Set) | Heap | За посиланням | Вказівник на об'єкт |
Коротко:
- Примітиви копіюються за значенням, створюється нова копія.
- Об'єкти копіюються за посиланням, обидві змінні «дивляться» на одну ділянку пам'яті.
- Для незалежної копії об'єкта використовуй
structuredClone(),JSON.parse(JSON.stringify(obj))абоlodash.cloneDeep().
Типові помилки
- Думати, що
[...arr]чи{ ...obj }дають повну копію. Вони копіюють лише верхній рівень. - Вважати, що
constробить об'єкт незмінним.constфіксує посилання, а не вміст купи. - Застосовувати
JSON.parse(JSON.stringify(obj))до чого завгодно: він втрачаєundefined, функції таSymbol, перетворюєDateна рядок і падає на циклічних посиланнях. - Забувати, що рядки в JavaScript незмінні:
str[0] = 'X'нічого не змінить, бо примітив не редагується на місці. - Очікувати, що великий об'єкт «звільниться» одразу після присвоєння
nullодній зі змінних. Поки на нього є хоча б одне живе посилання, garbage collector його не збере.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.