Необов'язковий ланцюг (?.) та нульове об'єднання (??) у JavaScript
Необов'язковий ланцюг (?.) та нульове об'єднання (??) - два оператори ES2020, які вирішують різні задачі: ?. зупиняє доступ до властивостей при першому null або undefined, ?? повертає запасне значення тільки коли ліва частина є null або undefined, але не коли це 0, "" або false.
Теорія
TL;DR
?.повертаєundefinedі зупиняє ланцюг при першомуnullабоundefined??повертає праву частину тільки якщо ліва -nullабоundefined(не інші falsy-значення)||повертає праву частину для будь-якого falsy-значення, включно з0іfalse- зазвичай це баг- Разом:
user?.profile?.name ?? "Guest"- безпечний доступ із запасним значенням - Правило: спочатку ланцюг через
?., потім значення за замовчуванням через??в кінці
Швидкий приклад
const user = { name: "Alice" };
// ?. зупиняється на першому null/undefined
console.log(user?.address?.city); // undefined (без помилки)
console.log(user?.address?.city ?? "NYC"); // "NYC" (запасне значення)
// ?? проти || - головна різниця
const count = 0;
console.log(count ?? 10); // 0 - нуль валідний, ?? його не чіпає
console.log(count || 10); // 10 - || вважає 0 хибним і замінюєОстанні два рядки - найпоширеніше джерело багів. Якщо count може бути 0, завжди використовуй ??.
Ключова різниця
?. - це захисний доступ до властивостей. ?? - це підстановка значення за замовчуванням. Вони вирішують різні проблеми і добре працюють разом. ?. запобігає TypeError: Cannot read properties of undefined. ?? гарантує, що запасне значення не перезапише 0 або false, які || замінив би без жодного попередження.
Коли використовувати
?.: доступ до вкладених об'єктів у відповідях API, виклик необов'язкових callback-ів, читання конфігурації, яка може бути не задана??: значення за замовчуванням для налаштувань, де0,""абоfalseє валідними?.і??разом:response?.data?.count ?? 0- безпечний обхід із значущим fallback- Залишай
||для перевірки на truthiness: перемикання UI-станів або трактування порожніх рядків як «відсутніх» - це задача для||
Таблиця порівняння
| Оператор | Повертає праву частину коли | Зберігає | Типове застосування |
|---|---|---|---|
?. | Ліва - null/undefined | Повертає undefined при зупинці | Безпечний вкладений доступ |
?? | Ліва - null/undefined | 0, "", false, NaN | Значення за замовчуванням |
|| | Ліва - будь-яке falsy | Тільки truthy-значення | Перевірка на truthiness |
&& | Ліва - truthy | N/A | Умовне виконання |
Як це працює всередині
Обидва оператори входять до ES2020 і підтримуються в Node.js 14+ та всіх сучасних браузерах. Коли рушій зустрічає obj?.prop, він перевіряє: obj є null або undefined? Якщо так - повертає undefined і нічого більше не обчислює. Ланцюг a?.b?.c?.d зупиняється на першому nullish-значенні, тому жодна наступна частина не виконується. Babel транспілює обидва оператори для старих цільових середовищ через аналогічну умовну логіку.
Типові помилки
Помилка 1: || замість ??
// Баг: timeout 0 означає "без затримки", але || замінить його
const timeout = settings.timeout || 5000; // 5000, навіть якщо timeout дорівнює 0
const timeout = settings.timeout ?? 5000; // ПравильноПомилка 2: зайвий ?. після явної перевірки
// Зайве - address вже підтверджено
if (user && user.address) {
const city = user.address?.city;
}
// Чистіше
const city = user?.address?.city;Помилка 3: забутий ?. перед необов'язковими методами
options.onSuccess(); // TypeError, якщо onSuccess не передали
options.onSuccess?.(); // undefined без помилкиПомилка 4: думати, що ?? ловить усі falsy-значення
const message = "" ?? "default"; // "" - порожній рядок не є null/undefined
const message = "" || "default"; // "default" - || ловить порожні рядкиДе зустрічається в реальному коді
- React:
props?.user?.avatar ?? "/default.png"- необов'язкові пропси в компонентах - Express.js:
req?.query?.search ?? ""- читання необов'язкових query-параметрів - Redux-селектори:
state?.auth?.user?.role ?? "guest"- вкладений стан стору - API-відповіді:
response?.data?.items?.[0]?.id ?? null- обхід nullable-полів - Необов'язкові callback-и:
options.onSuccess?.()- виклик, якщо callback передали
Питання на співбесіді
Q: Яка різниця між obj?.prop і obj && obj.prop?
A: Обидва запобігають помилкам, але повертають різне. obj && obj.prop повертає саме falsy-значення, якщо obj це 0 або "". ?. завжди повертає undefined, коли ланцюг зупиняється. До того ж ?. чіткіше сигналізує про намір.
Q: Чому 0 ?? 10 повертає 0, а 0 || 10 повертає 10?
A: Тому що ?? перевіряє тільки null або undefined, а не хибність взагалі. Нуль є falsy, але не nullish, тому ?? вважає його валідними даними і не замінює.
Q: Чи можна використовувати optional chaining (необов'язковий ланцюг) у деструктуризації?
A: Напряму - ні. Обхід: const { name } = user?.profile ?? {}. Без ?? {} деструктуризація кинула б помилку, якщо user?.profile повернув undefined.
Q: Що відбувається при кількох ?. в ланцюгу a?.b?.c?.d?
A: Весь ланцюг зупиняється на першому null або undefined і повертає undefined. Кожен ?. незалежний: якщо a.b є null, ні c, ні d не обчислюються.
Q (рівень senior): У циклі, який обробляє мільйони об'єктів, що краще: obj?.prop ?? default чи obj && obj.prop || default?
A: Сучасні рушії оптимізують обидва варіанти приблизно однаково, а ?. - це один оператор проти двох окремих операцій. Головна причина обирати ?. - читабельність: він одразу показує, що тут null-safe доступ. Тягнись до даних профілювання, перш ніж щось міняти в гарячому шляху.
Приклади
Базовий: відповідь API в React-компоненті
function UserProfile({ apiResponse }) {
const userName = apiResponse?.data?.user?.name ?? "Anonymous";
const avatarUrl = apiResponse?.data?.user?.avatar ?? "/default-avatar.png";
const isAdmin = apiResponse?.data?.user?.permissions?.isAdmin ?? false;
return (
<div>
<h1>Hello, {userName}</h1>
<img src={avatarUrl} alt={userName} />
{isAdmin && <span>Admin</span>}
</div>
);
}
// Працює навіть якщо apiResponse є null або дані частково відсутні?? на isAdmin тут важливий. isAdmin || false теж спрацює, але ?? точніше передає намір: підставити false тільки якщо значення взагалі не було задано.
Середній рівень: необов'язкові методи та доступ до масивів
const response = {
items: [
{ id: 1, process: () => "processed" },
{ id: 2 } // Метод process відсутній
]
};
console.log(response.items?.[0]?.process?.()); // "processed"
console.log(response.items?.[1]?.process?.()); // undefined - без помилки
console.log(response.items?.[5]?.process?.()); // undefined - індекс не існує
const result = response.items?.[1]?.process?.() ?? "default processing";
console.log(result); // "default processing"
// Без optional chaining - падає
response.items[1].process(); // TypeError: process is not a functionСинтаксис ?.() для виклику методів легко забути. Він працює за тією ж логікою що і ?.prop, але застосовується до виклику функції.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.