Опціональний ланцюжок ?. – це безпечний спосіб доступу до вкладених властивостей об’єктів, навіть якщо проміжних властивостей не існує.
Проблема “відсутньої властивості”
Якщо ви тільки почали читати підручник і вивчати 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 і дозволяє продовжувати обчислення якщо це не так.
Ланцюжок ?. дозволяє без виникнення помилок звертатись до вкладених властивостей.
Однак, потрібно розумно застосовувати ?., тільки в тих випадках де допустимо, що ліва частина не існує. Щоб таким чином не приховувати потенційні помилки програмування.
Коментарі
<code>, для кількох рядків – обгорніть їх тегом<pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)