Skip to main content

Примітиви та об'єкти в пам'яті

JavaScript тримає дані у двох місцях пам'яті: у стеку (stack) лежать примітиви, а в купі (heap) лежать об'єкти, і в стеку на них зберігається лише посилання. Саме тому примітив копіюється за значенням, а об'єкт, за посиланням.

Теорія

TL;DR

  • Стек, це швидка пам'ять для примітивів і для посилань на об'єкти.
  • Купа, це гнучка пам'ять, де лежать самі об'єкти, масиви, функції, Date, Map, Set.
  • Примітив копіюється за значенням: у нової змінної власна копія даних.
  • Об'єкт копіюється за посиланням: обидві змінні дивляться на одну ділянку купи.
  • Spread розриває посилання лише на верхньому рівні; для вкладених структур потрібна глибока копія (structuredClone()).

Швидкий приклад

javascript
// примітив: копія значення 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. Вони зберігаються у стеку, швидкій ділянці пам'яті, де лежать самі значення, а не посилання.

javascript
let a = 10; let b = a; // копіюється значення 10 b = 20; console.log(a); // 10 console.log(b); // 20

У пам'яті (stack):

text
+---------------+ | a = 10 | | b = 20 | <- окрема копія +---------------+

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

Об'єкти: купа і копіювання за посиланням

Об'єкти, а разом з ними масиви, функції, дати та інші складні структури, зберігаються в купі, області пам'яті для «гнучких» структур. А в стеку лежить лише посилання (вказівник) на цей об'єкт.

javascript
const user1 = { name: 'Tim' }; const user2 = user1; // копіюється посилання (вказівник) user2.name = 'Alex'; console.log(user1.name); // "Alex"

У пам'яті:

text
STACK (стек) HEAP (купа) +---------------+ +----------------------+ | user1 --+ | | { name: "Alex" } | | user2 --+---------> +----------------------+ +---------------+

Обидві змінні вказують на один і той самий об'єкт, тому зміна через user2 видно і в user1.

Поверхнева копія і чому вона не рятує вкладені об'єкти

Спеціальні способи копіювання створюють нове посилання:

javascript
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]

У пам'яті:

text
arr1 --> [1, 2, 3] arr2 --> [1, 2, 3, 4]

Різні посилання, тож зміни не зачіпають оригінал.

Але якщо об'єкт вкладений, spread робить лише поверхневу копію (shallow copy):

javascript
const user1 = { name: 'Tim', address: { city: 'Kyiv' } }; const user2 = { ...user1 }; user2.address.city = 'London'; console.log(user1.address.city); // "London"

Чому так:

text
user1.address --+ user2.address --+--> вказують на один і той самий внутрішній об'єкт

Тобто внутрішні об'єкти все ще копіюються за посиланням.

Глибоке копіювання (deep copy)

Щоб отримати повністю незалежну копію, треба «розірвати» всі посилання всередині.

Старий спосіб:

javascript
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 року):

javascript
const user2 = structuredClone(user1);

structuredClone() робить глибоку копію і не лишає спільних посилань узагалі.

Stack проти heap і підсумкова таблиця

text
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 його не збере.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.