24 жовтня 2023 р.

Опціональний ланцюжок '?.'

Нещодавнє доповнення
Це нещодавнє доповнення до мови. У старих браузерах може бути потрібен поліфіл.

Опціональний ланцюжок ?. – це безпечний спосіб доступу до вкладених властивостей об’єктів, навіть якщо проміжних властивостей не існує.

Проблема “відсутньої властивості”

Якщо ви тільки почали читати підручник і вивчати 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"

Воно недостатньо «розумне» для цього.

Підсумки

Синтаксис опціонального ланцюжка ?. має три форми:

  1. obj?.prop – повертає obj.prop, якщо існує obj, і undefined в іншому випадку.
  2. obj?.[prop] – повертає obj[prop], якщо існує obj, і undefined в іншому випадку.
  3. obj.method?.() – викликає obj.method(), якщо існує obj.method, в іншому випадку повертає undefined.

Як бачимо, всі вони прості та зрозумілі в використанні. ?. перевіряє ліву частину на рівність null/undefined і дозволяє продовжувати обчислення якщо це не так.

Ланцюжок ?. дозволяє без виникнення помилок звертатись до вкладених властивостей.

Однак, потрібно розумно застосовувати ?., тільки в тих випадках де допустимо що ліва частина не існує. Щоб таким чином не приховувати потенційні помилки програмування.

Навчальна карта