Skip to main content

Необов'язковий ланцюг (?.) та нульове об'єднання (??) у JavaScript

Необов'язковий ланцюг (?.) та нульове об'єднання (??) - два оператори ES2020, які вирішують різні задачі: ?. зупиняє доступ до властивостей при першому null або undefined, ?? повертає запасне значення тільки коли ліва частина є null або undefined, але не коли це 0, "" або false.

Теорія

TL;DR

  • ?. повертає undefined і зупиняє ланцюг при першому null або undefined
  • ?? повертає праву частину тільки якщо ліва - null або undefined (не інші falsy-значення)
  • || повертає праву частину для будь-якого falsy-значення, включно з 0 і false - зазвичай це баг
  • Разом: user?.profile?.name ?? "Guest" - безпечний доступ із запасним значенням
  • Правило: спочатку ланцюг через ?., потім значення за замовчуванням через ?? в кінці

Швидкий приклад

javascript
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/undefined0, "", false, NaNЗначення за замовчуванням
||Ліва - будь-яке falsyТільки truthy-значенняПеревірка на truthiness
&&Ліва - truthyN/AУмовне виконання

Як це працює всередині

Обидва оператори входять до ES2020 і підтримуються в Node.js 14+ та всіх сучасних браузерах. Коли рушій зустрічає obj?.prop, він перевіряє: obj є null або undefined? Якщо так - повертає undefined і нічого більше не обчислює. Ланцюг a?.b?.c?.d зупиняється на першому nullish-значенні, тому жодна наступна частина не виконується. Babel транспілює обидва оператори для старих цільових середовищ через аналогічну умовну логіку.

Типові помилки

Помилка 1: || замість ??

javascript
// Баг: timeout 0 означає "без затримки", але || замінить його const timeout = settings.timeout || 5000; // 5000, навіть якщо timeout дорівнює 0 const timeout = settings.timeout ?? 5000; // Правильно

Помилка 2: зайвий ?. після явної перевірки

javascript
// Зайве - address вже підтверджено if (user && user.address) { const city = user.address?.city; } // Чистіше const city = user?.address?.city;

Помилка 3: забутий ?. перед необов'язковими методами

javascript
options.onSuccess(); // TypeError, якщо onSuccess не передали options.onSuccess?.(); // undefined без помилки

Помилка 4: думати, що ?? ловить усі falsy-значення

javascript
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-компоненті

javascript
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 тільки якщо значення взагалі не було задано.

Середній рівень: необов'язкові методи та доступ до масивів

javascript
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, але застосовується до виклику функції.

Коротка відповідь

Для співбесіди
Premium

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

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