[recent browser="new"]
Опціональний ланцюжок ?.
-- це безпечний спосіб доступу до вкладених властивостей об’єктів, навіть якщо проміжних властивостей не існує.
Якщо ви тільки почали читати підручник і вивчати 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` виявиться пустою, ми побачимо програмну помилку і зможемо це виправити. В іншому випадку, якщо ми зловживаємо `?.`, помилки можуть замовчуватися там де це непотрібно й ускладнювати процес налагодження.
````warn header="Змінна перед ?.
повинна бути оголошеною"
Якщо змінної `user` взагалі не існує, тоді конструкція `user?.anything` видасть помилку:
// ReferenceError: user is not defined
user?.address;
Змінна обов’язково повинна бути оголошена (наприклад let/const/var user
або як параметр функції). Опціональний ланцюжок працює тільки з чинними змінними.
## Скорочене обчислення
Як вже говорилось, `?.` негайно припиняє обчислення, якщо лівої частини не існує.
Таким чином, якщо є додаткові виклики функцій або операції праворуч від `?.`, вони не будуть виконані.
Наприклад:
```js run
let user = null;
let x = 0;
user?.sayHi(x++); // немає "user", отже до x++ обчислення не дійде
alert(x); // 0, значення не було збільшено
```
## Інші способи застосування: ?.(), ?.[]
Опціональний ланцюжок `?.` -- це не оператор, а спеціальна синтаксична конструкція, що також працює з функціями та квадратними дужками.
Наприклад, `?.()` використовується для виклику потенційно відсутньої функції.
В прикладі нижче не в усіх користувачів є метод `admin`:
```js run
let userAdmin = {
admin() {
alert("Я адміністратор");
}
};
let userGuest = {};
*!*
userAdmin.admin?.(); // Я адміністратор
*/!*
*!*
userGuest.admin?.(); // нічого (немає такого методу)
*/!*
```
В обох випадках спочатку використовуємо крапку (`userAdmin.admin`) для доступу до властивості `admin`, оскільки об’єкт користувача точно існує, а це означає, що ми можемо звернутись до будь-якої його властивості.
Вже потім `?.()` перевіряє ліву частину: якщо функція `admin` існує, то вона виконається (у випадку з `userAdmin`). Інакше (для `userGuest`) обчислення припиниться без помилок.
Також існує синтаксис `?.[]`, якщо ми хочемо отримати доступ до властивості за допомогою квадратних дужок `[]`, а не через крапку `.`. Як і в решті випадків, такий спосіб дає змогу безпечно читати властивості об’єкта яких може не існувати.
```js run
let key = "firstName";
let user1 = {
firstName: "Іван"
};
let user2 = null;
alert( user1?.[key] ); // Іван
alert( user2?.[key] ); // undefined
```
Ми також можемо використовувати `?.` з `delete`:
```js run
delete user?.name; // видалити user.name, якщо користувач існує
```
````warn header="Ми можемо використовувати `?.` для безпечного читання і видалення властивостей, але не для запису"
Опціональний ланцюжок `?.` не має сенсу у лівій частині присвоювання.
Наприклад:
```js run
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
і дозволяє продовжувати обчислення якщо це не так.
Ланцюжок ?.
дозволяє без виникнення помилок звертатись до вкладених властивостей.
Однак, потрібно розумно застосовувати ?.
, тільки в тих випадках де допустимо, що ліва частина не існує. Щоб таким чином не приховувати потенційні помилки програмування.