Різниця між примітивами та непримітивами в JavaScript
Примітиви - це незмінні значення, які зберігаються безпосередньо в пам'яті за значенням; непримітиви (об'єкти) - це змінювані структури, до яких звертаються через посилання (reference) на heap-пам'ять.
Теорія
TL;DR
- Примітив - як паперова листівка: копія повністю незалежна, зміна копії не торкається оригіналу
- Об'єкт - як посилання на спільний Google Doc: всі хто тримають це посилання редагують один документ
- Присвоєння примітива копіює значення; присвоєння об'єкта копіює посилання (pointer)
- Прості стабільні дані (вік, ID, прапорець) -> примітив; згруповані або змінювані дані (кошик, конфіг, профіль) -> об'єкт
===для примітивів порівнює значення;===для об'єктів перевіряє чи це одне й те саме посилання в пам'яті
Швидкий приклад
// Примітиви: присвоєння копіює значення
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: очікування що масив копіюється при присвоєнні
let arr1 = [1, 2];
let arr2 = arr1; // копіює посилання, не елементи
arr2.push(3);
console.log(arr1); // [1, 2, 3] — несподівано
// Виправлення: використай spread
let arr2 = [...arr1];Помилка 2: спроба змінити рядок напряму
let str = 'hello';
str[0] = 'H';
console.log(str); // 'hello' — рядки незмінні, присвоєння не дає ефекту
// Виправлення: будуй новий рядок
str = 'H' + str.slice(1); // 'Hello'Помилка 3: порівняння об'єктів через ===
console.log({ a: 1 } === { a: 1 }); // false — два різних посилання
// Виправлення: порівнюй за вмістом
JSON.stringify(obj1) === JSON.stringify(obj2);
// Для вкладених структур: lodash isEqualПомилка 4: забути що null це примітив
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, який коректно обробляє крайні випадки.
Приклади
Примітив: копія значення у функції
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
// ПОГАНО: мутує існуючий об'єкт стану
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 покладається на нерівність посилань для виявлення змін.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.