Skip to main content

Різниця між примітивами та непримітивами в JavaScript

Примітиви - це незмінні значення, які зберігаються безпосередньо в пам'яті за значенням; непримітиви (об'єкти) - це змінювані структури, до яких звертаються через посилання (reference) на heap-пам'ять.

Теорія

TL;DR

  • Примітив - як паперова листівка: копія повністю незалежна, зміна копії не торкається оригіналу
  • Об'єкт - як посилання на спільний Google Doc: всі хто тримають це посилання редагують один документ
  • Присвоєння примітива копіює значення; присвоєння об'єкта копіює посилання (pointer)
  • Прості стабільні дані (вік, ID, прапорець) -> примітив; згруповані або змінювані дані (кошик, конфіг, профіль) -> об'єкт
  • === для примітивів порівнює значення; === для об'єктів перевіряє чи це одне й те саме посилання в пам'яті

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

javascript
// Примітиви: присвоєння копіює значення let a = 5; let b = a; // b отримує власну копію числа 5 a = 10; console.log(a); // 10 console.log(b); // 5 — не змінилось // Об'єкти: присвоєння копіює посилання let x = { count: 5 }; let y = x; // y вказує на той самий об'єкт що і x x.count = 10; console.log(x.count); // 10 console.log(y.count); // 10 — той самий об'єкт, той самий результат

x і y вказують на один об'єкт у пам'яті. Другого об'єкта немає. Тому мутація через x видна через y.

Головна різниця

Коли ти пишеш let b = a з примітивом, JavaScript створює новий слот у пам'яті і копіює туди саме значення. Коли ти пишеш let y = x з об'єктом, JavaScript копіює лише адресу об'єкта (64-бітний pointer у V8). Обидві змінні вказують на одне місце в heap-пам'яті. Будь-яка мутація через одну змінну відображається в іншій.

Коли використовувати

  • Вік, ціна, прапорець (toggle), ID користувача -> примітив
  • Кошик, профіль користувача, конфіг застосунку -> об'єкт
  • Унікальний ключ, який не повинен випадково збігатись з чимось -> Symbol (примітив)
  • Згруповані дані з методами -> об'єкт

Таблиця порівняння

АспектПримітивиНепримітиви (Об'єкти)
ЗберіганняСтек (пряме значення)Heap (дані) + стек (посилання)
ПрисвоєнняКопіює значенняКопіює посилання
ЗмінюваністьНезмінніЗмінювані
Порівняння ===Перевіряє значенняПеревіряє посилання
РозмірФіксований, малийДинамічний
Типиstring, number, boolean, null, undefined, bigint, symbol{}, [], () => {}
Коли використовуватиКонстанти, ID, обчисленняСтруктури даних, конфіги, стан

Як це працює всередині

V8 (Chrome і Node.js) розміщує примітиви на стеку для швидкого доступу без потреби в garbage collection. Об'єкти йдуть на heap; стек тримає лише 64-бітний pointer на це місце. Коли ти пишеш obj.prop = 1, V8 іде за pointer-ом і мутує спільні heap-дані. Коли на об'єкт більше ніщо не вказує, garbage collector звільняє пам'ять.

Ще один нюанс: V8 інтернує (intern) короткі ідентичні рядки, тобто вони можуть ділити один слот у пам'яті. Це оптимізація, яку ти не контролюєш, але саме тому однакові рядкові примітиви завжди === рівні.

Типові помилки

Помилка 1: очікування що масив копіюється при присвоєнні

javascript
let arr1 = [1, 2]; let arr2 = arr1; // копіює посилання, не елементи arr2.push(3); console.log(arr1); // [1, 2, 3] — несподівано // Виправлення: використай spread let arr2 = [...arr1];

Помилка 2: спроба змінити рядок напряму

javascript
let str = 'hello'; str[0] = 'H'; console.log(str); // 'hello' — рядки незмінні, присвоєння не дає ефекту // Виправлення: будуй новий рядок str = 'H' + str.slice(1); // 'Hello'

Помилка 3: порівняння об'єктів через ===

javascript
console.log({ a: 1 } === { a: 1 }); // false — два різних посилання // Виправлення: порівнюй за вмістом JSON.stringify(obj1) === JSON.stringify(obj2); // Для вкладених структур: lodash isEqual

Помилка 4: забути що null це примітив

javascript
let obj = null; obj.prop = 'test'; // TypeError: Cannot set properties of null // Виправлення: перевір перед зверненням if (obj) obj.prop = 'test';

Де це зустрічається

  • React: стан - це об'єкт (useState({ name: 'Alice' })); для оновлення створюй новий об'єкт ({ ...user, name: 'Bob' }), бо пряма мутація обходить механізм виявлення змін React
  • Express: req.body - змінюваний об'єкт; res.status(200) приймає примітивне число
  • Node.js: process.env - об'єкт, спільний для всього застосунку; parseInt(process.env.PORT) дає примітив
  • Lodash cloneDeep: коли потрібна повністю незалежна копія вкладеного об'єкта, а не копія pointer-а

На практиці модель посилань породжує більше багів ніж модель значень. Більшість питань «чому мій стан змінився несподівано?» в React зводяться до спільних посилань.

Питання на співбесіді

Q: Що відбувається після let x = 5; x = { value: 5 };?
A: x переходить від зберігання примітивного числа до зберігання посилання на новий об'єкт. JavaScript не накладає обмежень на типи змінних, тому перепризначення просто спрацьовує. Число 5 зникає.

Q: Чому typeof null повертає 'object'?
A: Це баг 1995 року, який так і не виправили заради зворотної сумісності. null - примітив. Результат typeof тут хибний, але зміна зламала б занадто багато існуючого коду.

Q: Чи можуть примітиви мати методи як .toUpperCase() або .toString()?
A: Так, через auto-boxing. Коли ти викликаєш 'hello'.toUpperCase(), JavaScript тимчасово загортає рядковий примітив у об'єкт String, викликає метод, потім відкидає обгортку. Оригінальний примітив залишається незмінним.

Q: Якщо передати масив у функцію і всередині зробити push, чи зміниться оригінальний масив?
A: Так. Масиви - об'єкти, тому передача масиву передає посилання. Будь-яка мутація всередині функції зачіпає оригінал. Щоб уникнути цього, передавай копію: fn([...arr]).

Q: (Senior) Два об'єкти з однаковими властивостями дають === false. Як перевірити структурну рівність?
A: JSON.stringify(a) === JSON.stringify(b) працює для плоских об'єктів з однаковим порядком ключів. Для вкладених структур або об'єктів з Date чи RegExp краще lodash isEqual, який коректно обробляє крайні випадки.

Приклади

Примітив: копія значення у функції

javascript
function increment(n) { n += 1; return n; } let count = 5; let result = increment(count); console.log(count); // 5 — оригінал не змінився console.log(result); // 6

Функція отримує копію числа 5. Зміна n всередині нічого не робить з count зовні. Це модель передачі за значенням у дії.

Посилання об'єкта: баг з мутацією стану в React

javascript
// ПОГАНО: мутує існуючий об'єкт стану const [user, setUser] = useState({ name: 'Alice', prefs: { theme: 'dark' } }); user.prefs.theme = 'light'; // пряма мутація setUser(user); // React бачить те саме посилання — перерендеру немає // ДОБРЕ: створюй новий об'єкт на кожному рівні що змінився setUser({ ...user, prefs: { ...user.prefs, theme: 'light' } }); // React бачить нове посилання, перерендер відбувається коректно

setUser(user) передає той самий pointer. Поверхнева перевірка React бачить однакове посилання і пропускає перерендер. Виправлення завжди одне: повертай новий об'єкт, бо React покладається на нерівність посилань для виявлення змін.

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

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

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