Типи даних у JavaScript
Типи даних у JavaScript поділяються на дві групи: сім примітивів (незмінні, передаються за значенням) та об'єкти (змінні, передаються за посиланням).
Теорія
TL;DR
- Примітив, це як надрукована листівка: копіюєш і отримуєш незалежний дублікат.
- Об'єкт, це як спільний Google Doc: усі «копії» вказують на одні й ті самі дані.
- Сім примітивів:
undefined,null,boolean,number,string,symbol,bigint. - Об'єкти охоплюють звичайні об'єкти, масиви, функції, дати, регулярні вирази та інше.
typeof nullповертає"object", а не"null". Відомий баг з першої версії JavaScript.
Швидкий приклад
// Примітиви: присвоєння копіює значення
let a = 5;
let b = a; // b отримує власну копію значення 5
b = 10;
console.log(a); // 5, не змінилось
// Об'єкти: присвоєння копіює посилання
let x = { val: 5 };
let y = x; // y вказує на той самий об'єкт
y.val = 10;
console.log(x.val); // 10, x теж змінивсяЦі два приклади пояснюють більшість багів, пов'язаних з цією темою.
Головна різниця
Коли присвоюєш примітив, JavaScript копіює саме значення в окрему ділянку пам'яті. Дві змінні стають незалежними. З об'єктами інакше: змінна тримає посилання (адресу в пам'яті), а не самі дані. Присвоїш таку змінну іншій, і обидві вказуватимуть на один і той самий об'єкт у heap. Зміниш через одну, побачиш результат через обидві.
Коли що використовувати
- Лічильник у циклі → примітив
number. - Ім'я користувача → примітив
string. - Профіль користувача з кількома полями → об'єкт.
- Прапорець у стані React → примітив
boolean. - Дані форми в стані React → об'єкт.
- Цілі числа, більші за 2^53 - 1 →
bigint.
Модель пам'яті
V8 зберігає примітиви у стеку (stack) для швидкого доступу. Об'єкти йдуть у heap, а у стеку живе лише вказівник. Саме цей вказівник копіюється під час присвоєння, тому дві змінні можуть непомітно ділити одні й ті самі дані. Object.create та схожі API напряму маніпулюють цими структурами в heap.
Типові помилки
Помилка 1: масив сприймають як примітив
let arr1 = [1, 2];
let arr2 = arr1; // копія посилання, не новий масив
arr2.push(3);
console.log(arr1); // [1, 2, 3], несподіваноМасиви є об'єктами. Виправлення: let arr2 = [...arr1] або arr1.slice().
Помилка 2: спроба змінити рядок посимвольно
let s = "hello";
s[0] = "H"; // помилки немає, але нічого не змінюється
console.log(s); // "hello"Рядки незмінні. Щоб замінити символ: s = "H" + s.slice(1).
Помилка 3: порівняння об'єктів через ===
console.log({} === {}); // false, різні посилання
console.log([1] === [1]); // falseОб'єкти порівнюються за посиланням, а не за вмістом. Для структурної перевірки використовуй JSON.stringify або lodash.isEqual. Про те, як поводиться == з різними типами, читай у статті про перетворення типів у JavaScript.
Помилка 4: typeof null
typeof null === "object" // true
typeof null === "null" // falseЦе баг з JavaScript 1.0 у Netscape. Внутрішній тег типу для null збігся з тегом для об'єктів. Пропозиції виправити провалились через зворотну сумісність. Перевіряй null явно: value === null.
Помилка 5: BigInt у JSON
const id = 123n;
JSON.stringify(id); // TypeErrorJSON не підтримує BigInt. Конвертуй перед серіалізацією: id.toString(), або передай функцію-замінник у JSON.stringify.
Де зустрічається в реальних проектах
- React: пропси як об'єкти
{ userId: 123, name: "Alice" }; ключі списків як примітивиkey={id}. - Express:
req.body- об'єкт, розпарсений з JSON;res.status(200)приймає числовий примітив. - Node.js fs:
errу колбеках - абоnull(примітив), або об'єктError. - Redux: стан - дерево об'єктів; тип дії (action type) - рядковий примітив
"INCREMENT". - Lodash:
_.cloneDeep(obj)глибоко копіює вкладені об'єкти без спільних посилань.
Питання на співбесіді
Q: Що станеться, якщо передати примітив у функцію і змінити його всередині?
A: Функція отримає копію. Зміни всередині не зачеплять оригінальну змінну. З об'єктами навпаки: функція отримує посилання і може змінити оригінал.
Q: Чому typeof null повертає "object"?
A: Це баг з першого релізу JavaScript у Netscape. Оригінальний тег типу для null збігся з тегом для об'єктів. Кілька пропозицій виправити провалились, бо зміна зламала б існуючі сайти.
Q: Назви всі сім примітивів.
A: undefined, null, boolean, number, string, symbol (ES2015), bigint (ES2020).
Q: Чим BigInt відрізняється від Number?
A: Number - 64-бітний float з межею безпечних цілих 2^53 - 1. BigInt підтримує цілі числа довільного розміру. Вони не перетворюються один в одного неявно, тому 1n + 1 кине TypeError.
Q (для досвідчених): Об'єкт передано в асинхронний колбек. До моменту виклику колбека об'єкт змінено ззовні. Що побачить колбек?
A: Змінену версію. Колбек тримає посилання на той самий об'єкт у heap, а не знімок стану. Це поширена причина багів із застарілими даними, коли спільний об'єкт змінюється між плануванням і виконанням колбека.
Приклади
Примітиви та об'єкти при передачі у функцію
function addTen(n) {
n = n + 10; // змінює лише локальну копію
}
function addScore(obj) {
obj.score = 100; // змінює сам об'єкт
}
let num = 5;
addTen(num);
console.log(num); // 5, не змінилось
let user = { name: "Alice" };
addScore(user);
console.log(user.score); // 100, змінилосьПередав примітив, і будь-яка мутація залишається всередині функції. Передав об'єкт, і виклик побачить кожну зміну. Я бачив, як це спантеличує junior-розробників, які думали, що JavaScript завжди робить копію аргументу.
Витягування примітиву з пропса в React
function UserProfile({ user }) {
// Витягуємо рядковий примітив з об'єкта-пропса в локальний стан
const [name, setName] = useState(user.name);
// Редагування поля змінює лише локальний стан
// user.name залишається незачепленим
return <input value={name} onChange={e => setName(e.target.value)} />;
}useState(user.name) копіює рядкове значення, а не посилання на user. Локальні зміни ніколи не мутують пропс. Якби замість user.name у useState передавався сам user, будь-яка мутація цього об'єкта ділилась би з батьківським компонентом.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.