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

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

Дочитали статтю?