Боксинг та анбоксинг у JavaScript
Boxing - це автоматичне загортання примітивного значення в тимчасовий об'єкт для виклику методів на ньому. Анбоксинг - зворотний процес: рушій витягує примітив назад після операції.
Теорія
TL;DR
- Коли ти викликаєш
.toUpperCase()на рядку, JavaScript загортає цей рядок в об'єктString, запускає метод і викидає обгортку - Анбоксинг теж відбувається автоматично: об'єктна обгортка перетворюється назад у примітив під час арифметики або порівняння
- Обгортка живе рівно одну операцію. Ти її не бачиш і не тримаєш на неї посилання
- Ніколи не пиши
new String()абоnew Number(). Це створює постійний об'єкт, а не примітив, і ламає===таtypeof
Короткий приклад
const str = "hello";
console.log(str.toUpperCase()); // "HELLO"
// Що насправді робить рушій:
// 1. Бачить виклик методу на примітиві
// 2. Створює: new String("hello") <- тимчасова обгортка
// 3. Викликає: wrapper.toUpperCase()
// 4. Повертає: "HELLO"
// 5. Викидає обгорткуКожен .trim(), .toFixed(), .charAt() на примітиві проходить через це. Примітив залишається незмінним. Обгортка зникає.
Як працює boxing
Коли рушій бачить звернення до властивості або виклик методу на примітиві, він загортає значення у відповідний об'єкт: String для рядків, Number для чисел, Boolean для булевих, Symbol для символів. Метод виконується на цій обгортці. Результат повертається як примітив, а обгортка знищується.
Сучасні рушії на кшталт V8 використовують escape analysis і в більшості випадків виконують метод без виділення реального об'єкта в пам'яті. Витрати практично нульові.
Саме boxing пояснює, чому "hello".length працює. Рядок - не об'єкт. Але рушій створює тимчасову обгортку String, зчитує з неї length і повертає 5. Ось і весь механізм.
Коли зустрічається
Boxing відбувається автоматично. Ти не вмикаєш його вручну і не можеш вимкнути. Важливо знати, коли це відбувається:
- Виклики методів на примітивах:
.toUpperCase(),.toFixed(2),.charAt(0)- boxing це і є причина, чому вони взагалі працюють - Доступ до властивостей примітивів:
.lengthна рядку теж викликає boxing, не тільки методи - Відлагодження сюрпризів
typeof: якщо щось повертає"object"замість очікуваного"string", хтось у кодовій базі написавnew String() - Уникай
new String(),new Number(),new Boolean()у власному коді повністю
Типові помилки
Використання new String() замість рядкового літерала
// Неправильно
const a = new String("hello");
const b = new String("hello");
console.log(a === b); // false - два різні об'єкти
console.log(typeof a); // "object" - не рядок
// Правильно
const c = "hello";
const d = "hello";
console.log(c === d); // true
console.log(typeof c); // "string"new String() створює постійний об'єкт-обгортку. Це не примітив. Порівняння ламаються. Об'єкт завжди truthy, навіть якщо рядок порожній, бо це об'єкт. Boxing, який рушій робить під час виклику методів, тимчасовий. Цей - ні.
Очікування, що властивість збережеться на примітиві
const str = "hello";
str.customProp = "world";
console.log(str.customProp); // undefinedКоли ти пишеш str.customProp = "world", boxing створює тимчасову обгортку і додає властивість до неї. Потім обгортка знищується. Наступного разу при зверненні до str.customProp boxing створює нову обгортку без жодних властивостей. Примітиви не можуть зберігати властивості. Тільки об'єкти можуть.
Порівняння примітиву з об'єктною обгорткою
const x = 42;
const y = new Number(42);
console.log(x === y); // false - примітив проти об'єкта
console.log(x == y); // true - нестрогий оператор спочатку розпаковує y
console.log(typeof x); // "number"
console.log(typeof y); // "object"Змішування примітивів і об'єктних обгорток - джерело важко помітних багів. Завжди використовуй примітивні літерали.
Де зустрічається в реальному коді
- React:
props.name.toUpperCase()боксує рядок при кожному рендері - Express:
price.toFixed(2)перед відправкою JSON-відповіді - Колбеки масивів:
[1, 2, 3].map(n => n.toString())боксує кожне число по одному разу - DOM:
element.getAttribute("data-id").trim()- ланцюжок з двох операцій boxing в одному рядку - Валідація форм:
input.value.length > 0зчитує.lengthчерез boxing
Питання на співбесіді
Q: Чому "hello".length працює, якщо рядки є примітивами?
A: Boxing. Рушій загортає "hello" в тимчасовий об'єкт String, зчитує з нього властивість length і повертає 5. Обгортка знищується.
Q: Що таке анбоксинг (unboxing)?
A: Анбоксинг - це коли об'єктна обгортка перетворюється назад у примітив. Відбувається автоматично під час арифметики: new Number(42) + 8 спочатку розпаковує Number до 42, а потім додає. Можна також викликати .valueOf() вручну, але на практиці це не потрібно.
Q: Чи впливає boxing на продуктивність?
A: У сучасному коді - ні. V8 розпізнає цей патерн і часто пропускає створення реального об'єкта в пам'яті. У рідкісних випадках об'єкт все ж виділяється, але збирач сміття прибирає його швидко. У звичайних умовах ти не побачиш boxing у профайлері.
Q: Що повертає typeof "hello".toUpperCase()?
A: "string". Хоча boxing створив тимчасовий об'єкт String для виконання методу, toUpperCase() повертає примітивний рядок. typeof перевіряє результат, а не обгортку.
Q: (Senior) typeof new String("hello").toUpperCase() також повертає "string", хоча new String("hello") - об'єкт. Чому?
A: Бо toUpperCase() завжди повертає примітивний рядок, незалежно від того, на чому його викликали. typeof перевіряє повернене значення, а не отримувача. Головна різниця не в типі результату, а в самому об'єкті: new String("hello") залишається в пам'яті як "object", а обгортка від boxing - ні. Саме тому new String є антипатерном.
Приклади
Виклики методів на рядковому примітиві
const name = "alice";
const upper = name.toUpperCase(); // "ALICE" - boxing #1
const first = name.charAt(0); // "a" - boxing #2
const found = name.includes("alice"); // true - boxing #3
console.log(typeof name); // "string" - name не змінивсяТри операції boxing, три тимчасові обгортки, три видалення. Змінна name залишається примітивним рядком весь час. Саме цей патерн запускається при кожному виклику рядкового методу.
Валідація введення користувача у форм-хендлері
function processInput(input) {
const trimmed = input.trim(); // boxing: загортає в String, викликає trim()
const cleaned = trimmed.toLowerCase(); // boxing: загортає знову, викликає toLowerCase()
const valid = cleaned.length > 0; // boxing: загортає знову, зчитує length
if (valid) {
return cleaned;
}
return null;
}
console.log(processInput(" Hello World ")); // "hello world"Кожен виклик у ланцюжку боксує рядок наново. Переглядаючи різні кодові бази, помічаю, що розробники іноді думають, ніби такі ланцюжки створюють об'єкти, що зберігаються між викликами. Це не так. Кожна операція - це окремий цикл загортання і знищення.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.