Опціональний ланцюжок ?.
– це безпечний спосіб доступу до вкладених властивостей об’єктів, навіть якщо проміжних властивостей не існує.
Проблема “відсутньої властивості”
Якщо ви тільки почали читати підручник і вивчати JavaScript, можливо ця проблема вам наразі незнайома, проте вона достатньо розповсюджена.
Наприклад, розглянемо об’єкт user
який містить інформацію про наших користувачів.
В більшості наших користувачів є адреса user.address
з вулицею user.address.street
, проте дехто вирішив взагалі не вказувати адресу.
Отож якщо користувач не вказав адресу, а ми своєю чергою спробуємо отримати доступ до властивості user.address.street
, то отримаємо помилку.
let user = {}; // користувач без властивості "address"
alert(user.address.street); // помилка!
Це очікуваний розвиток подій, так працює JavaScript. Оскільки user.address
є undefined
, то і спроба отримати user.address.street
закінчується помилкою.
Проте в багатьох життєвих ситуаціях було б набагато зручніше просто отримати undefined
, що буде означати “немає вулиці”.
…Ще один приклад. У веброзробці ми можемо отримати об’єкт котрий відповідає елементу на вебсторінці за допомогою спеціальних методів, наприклад: document.querySelector('.elem')
. Проте якщо ми намагатимемось отримати елемент, якого немає на сторінці, то нам вернеться null
.
// document.querySelector('.elem') рівний null якщо такого елемента не існує
let html = document.querySelector('.elem').innerHTML; // помилка оскільки null
Для закріплення. Якщо елемента немає на вебсторінці, ми отримаємо помилку при спробі доступу до властивості .innerHTML
від null
. І в деяких випадках, коли відсутність елемента для нас є нормою, ми хотіли б просто отримати .innerHTML = null
(тобто html = null
).
Як ми можемо це реалізувати?
Найочевиднішим рішенням було б перевірити властивість використавши if
або за допомогою умовного оператора ?
:
let user = {};
alert(user.address ? user.address.street : undefined);
Варіант робочий, помилки не буде… Але виглядає це вкрай неелегантно. Як ви бачите "user.address"
двічі з’являється в коді.
Ось як те ж саме виглядає для document.querySelector
:
let html = document.querySelector('.elem') ? document.querySelector('.elem').innerHTML : null;
Ми бачимо, що пошук елемента document.querySelector('.elem')
насправді викликається тут двічі. Не добре.
Для властивостей які лежать глибше, це стає проблемою оскільки потребує більшого дублювання.
Спробуймо отримати доступ до user.address.street.name
.
let user = {}; // користувач без властивості "address"
alert(user.address ? user.address.street ? user.address.street.name : null : null);
Виглядає просто жахливо та незрозуміло.
Але не хвилюйтесь, існує кращий варіант реалізації такої задачі за допомогою логічного оператора &&
:
let user = {}; // користувач без властивості "address"
alert( user.address && user.address.street && user.address.street.name ); // undefined (немає помилки)
Логічне “І” з ланцюжком властивостей гарантує нам, що всі вони існують (якщо ж ні – обчислення припиняється), але й це все ще не ідеал.
Як ви бачите, імена властивостей досі дублюються в коді. В прикладі вище властивість user.address
появляється тричі.
Ось чому опціональний ланцюжок ?.
був доданий в мову. Щоб розв’язати цю проблему раз і назавжди!
Опціональний ланцюжок
Опціональний ланцюжок ?.
припиняє обчислення, якщо значення перед ?.
є undefined
або null
, і повертає undefined
.
Для стислості надалі в цій статті ми будемо говорити про значення, що воно “існує”, якщо воно відрізняється від null
чи undefined
Іншими словами, value?.prop
:
- працює як
value.prop
, якщоvalue
існує, - інакше (коли
value
єundefined/null
) воно повертаєundefined
.
Ось безпечний спосіб доступу до властивості user.address.street
за допомогою ?.
:
let user = {}; // користувач без властивості "address"
alert( user?.address?.street ); // undefined (немає помилки)
Такий код виглядає коротшим і чистішим, взагалі немає ніякого дублювання.
Ось приклад із document.querySelector
:
let html = document.querySelector('.elem')?.innerHTML; // буде undefined, якщо немає елемента
Читання властивості “address” з user?.address
спрацює навіть коли в змінній user
зберігається зовсім не об’єкт:
let user = null;
alert( user?.address ); // undefined
alert( user?.address.street ); // undefined
Зверніть увагу, що синтаксис ?.
робить необов’язковою тільки властивість перед ним, а не будь-яку наступну.
Наприклад в user?.address.street.name
конструкція user?.
дозволяє user
залишатись null/undefined
(і повертати undefined
в такому випадку), але це працює тільки для user
. Доступ до решти властивостей здійснюється звичайним способом. Якщо ми хочемо, щоб якась з них була необов’язковою, тоді конкретно для цієї властивості нам доведеться замінити .
на ?.
.
Нам слід використовувати ?.
тільки в тих ситуаціях коли ми припускаємо що значення може не існувати.
Наприклад, якщо за нашою логікою об’єкт user
точно існує, але його властивість address
є необов’язковою, тоді нам слід використовувати конструкцію user.address?.street
. Проте аж ніяк не user?.address?.street
.
Тоді якщо помилково змінна user
виявиться пустою, ми побачимо програмну помилку і зможемо це виправити. В іншому випадку, якщо ми зловживаємо ?.
, помилки можуть замовчуватися там де це непотрібно й ускладнювати процес налагодження.
?.
повинна бути оголошеноюЯкщо змінної user
взагалі не існує, тоді конструкція user?.anything
видасть помилку:
// ReferenceError: user is not defined
user?.address;
Змінна обов’язково повинна бути оголошена (наприклад let/const/var user
або як параметр функції). Опціональний ланцюжок працює тільки з чинними змінними.
Скорочене обчислення
Як вже говорилось, ?.
негайно припиняє обчислення, якщо лівої частини не існує.
Таким чином, якщо є додаткові виклики функцій або операції праворуч від ?.
, вони не будуть виконані.
Наприклад:
let user = null;
let x = 0;
user?.sayHi(x++); // немає "user", отже до x++ обчислення не дійде
alert(x); // 0, значення не було збільшено
Інші способи застосування: ?.(), ?.[]
Опціональний ланцюжок ?.
– це не оператор, а спеціальна синтаксична конструкція, що також працює з функціями та квадратними дужками.
Наприклад, ?.()
використовується для виклику потенційно відсутньої функції.
В прикладі нижче не в усіх користувачів є метод admin
:
let userAdmin = {
admin() {
alert("Я адміністратор");
}
};
let userGuest = {};
userAdmin.admin?.(); // Я адміністратор
userGuest.admin?.(); // нічого (немає такого методу)
В обох випадках спочатку використовуємо крапку (userAdmin.admin
) для доступу до властивості admin
, оскільки об’єкт користувача точно існує, а це означає що ми можемо звернутись до будь-якої його властивості.
Вже потім ?.()
перевіряє ліву частину: якщо функція admin
існує, то вона виконається (у випадку з userAdmin
). Інакше (для userGuest
) обчислення припиниться без помилок.
Також існує синтаксис ?.[]
, якщо ми хочемо отримати доступ до властивості за допомогою квадратних дужок []
, а не через крапку .
. Як і в решті випадків, такий спосіб дає змогу безпечно читати властивості об’єкта яких може не існувати.
let key = "firstName";
let user1 = {
firstName: "Іван"
};
let user2 = null;
alert( user1?.[key] ); // Іван
alert( user2?.[key] ); // undefined
Ми також можемо використовувати ?.
з delete
:
delete user?.name; // видалити user.name, якщо користувач існує
?.
для безпечного читання і видалення властивостей, але не для записуОпціональний ланцюжок ?.
не має сенсу у лівій частині присвоювання.
Наприклад:
let user = null;
user?.name = "Іван"; // Помилка, не спрацює
// це по суті те ж саме що undefined = "John"
Воно недостатньо «розумне» для цього.
Підсумки
Синтаксис опціонального ланцюжка ?.
має три форми:
obj?.prop
– повертаєobj.prop
, якщо існуєobj
, іundefined
в іншому випадку.obj?.[prop]
– повертаєobj[prop]
, якщо існуєobj
, іundefined
в іншому випадку.obj.method?.()
– викликаєobj.method()
, якщо існуєobj.method
, в іншому випадку повертаєundefined
.
Як бачимо, всі вони прості та зрозумілі в використанні. ?.
перевіряє ліву частину на рівність null/undefined
і дозволяє продовжувати обчислення якщо це не так.
Ланцюжок ?.
дозволяє без виникнення помилок звертатись до вкладених властивостей.
Однак, потрібно розумно застосовувати ?.
, тільки в тих випадках де допустимо що ліва частина не існує. Щоб таким чином не приховувати потенційні помилки програмування.