6 августа 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. И в некоторых случаях, когда отсутствие элемента является нормальным, мы хотели бы избежать ошибки и просто принять 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.

Нам нужно проверить как user.address, так и user.address.street:

let user = {}; // у пользователя нет адреса

alert(user.address ? user.address.street ? user.address.street.name : null : null);

Это просто ужасно, у кого-то могут даже возникнуть проблемы с пониманием такого кода.

Есть немного лучший способ написать это, используя оператор &&:

let user = {}; // пользователь без адреса

alert( user.address && user.address.street && user.address.street.name ); // undefined (без ошибки)

Проход при помощи логического оператора И && через весь путь к свойству гарантирует, что все компоненты существуют (если нет, вычисление прекращается), но также не является идеальным.

Как вы можете видеть, имена свойств по-прежнему дублируются в коде. Например, в приведённом выше коде user.address появляется три раза.

Вот почему в язык была добавлена опциональная цепочка ?.. Чтобы решить эту проблему – раз и навсегда!

Опциональная цепочка

Опциональная цепочка ?. останавливает вычисление и возвращает undefined, если значение перед ?. равно undefined или null.

Далее в этой статье, для краткости, мы будем говорить, что что-то «существует», если оно не является null и не undefined.

Другими словами, value?.prop:

  • работает как value.prop, если значение value существует,
  • в противном случае (когда value равно undefined/null) он возвращает undefined.

Вот безопасный способ получить доступ к user.address.street, используя ?.:

let user = {}; // пользователь без адреса

alert( user?.address?.street ); // undefined (без ошибки)

Код лаконичный и понятный, в нем вообще нет дублирования.

А вот пример с document.querySelector:

let html = document.querySelector('.elem')?.innerHTML; // будет undefined, если элемента нет

Считывание адреса с помощью user?.address работает, даже если объект user не существует:

let user = null;

alert( user?.address ); // undefined
alert( user?.address.street ); // undefined

Обратите внимание: синтаксис ?. делает необязательным значение перед ним, но не какое-либо последующее.

Так например, в записи user?.address.street.name ?. позволяет user безопасно быть null/undefined (и в этом случае возвращает undefined), но это так только для user. Доступ к последующим свойствам осуществляется обычным способом. Если мы хотим, чтобы некоторые из них были необязательными, тогда нам нужно будет заменить больше . на ?..

Не злоупотребляйте опциональной цепочкой

Нам следует использовать ?. только там, где нормально, что чего-то не существует.

К примеру, если, в соответствии с логикой нашего кода, объект user должен существовать, но address является необязательным, то нам следует писать user.address?.street, но не user?.address?.street.

В этом случае, если вдруг user окажется undefined, мы увидим программную ошибку по этому поводу и исправим её. В противном случае, если слишком часто использовать ?., ошибки могут замалчиваться там, где это неуместно, и их будет сложнее отлаживать.

Переменная перед ?. должна быть объявлена

Если переменной user вообще нет, то user?.anything приведёт к ошибке:

// ReferenceError: user is not defined
user?.address;

Переменная должна быть объявлена (к примеру, как let/const/var user или как параметр функции). Опциональная цепочка работает только с объявленными переменными.

Сокращённое вычисление

Как было сказано ранее, ?. немедленно останавливает вычисление, если левая часть не существует.

Так что если после ?. есть какие-то вызовы функций или операции, то они не произойдут.

Например:

let user = null;
let x = 0;

user?.sayHi(x++); // нет "user", поэтому выполнение не достигает вызова sayHi и x++

alert(x); // 0, значение не увеличилось

Другие варианты применения: ?.(), ?.[]

Опциональная цепочка ?. — это не оператор, а специальная синтаксическая конструкция, которая также работает с функциями и квадратными скобками.

Например, ?.() используется для вызова функции, которая может не существовать.

В приведённом ниже коде у некоторых наших пользователей есть метод admin, а у некоторых его нет:

let userAdmin = {
  admin() {
    alert("Я админ");
  }
};

let userGuest = {};

userAdmin.admin?.(); // Я админ

userGuest.admin?.(); // ничего не произойдет (такого метода нет)

Здесь в обеих строках мы сначала используем точку (userAdmin.admin), чтобы получить свойство admin, потому что мы предполагаем, что объект userAdmin существует, так что читать из него безопасно.

Затем ?.() проверяет левую часть: если функция admin существует, то она запускается (это так для userAdmin). В противном случае (для userGuest) вычисление остановится без ошибок.

Синтаксис ?.[] также работает, если мы хотим использовать скобки [] для доступа к свойствам вместо точки .. Как и в предыдущих случаях, он позволяет безопасно считывать свойство из объекта, который может не существовать.

let key = "firstName";

let user1 = {
  firstName: "John"
};

let user2 = null;

alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined

Также мы можем использовать ?. с delete:

delete user?.name; // удаляет user.name если пользователь существует
Мы можем использовать ?. для безопасного чтения и удаления, но не для записи

Опциональная цепочка ?. не имеет смысла в левой части присваивания.

Например:

let user = null;

user?.name = "John"; // Ошибка, не работает
// то же самое что написать 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 и позволяет продолжить вычисление, если это не так.

Цепочка ?. позволяет безопасно получать доступ к вложенным свойствам.

Тем не менее, мы должны использовать ?. осторожно, только там, где по логике кода допустимо, что левая часть не существует. Чтобы он не скрывал от нас ошибки программирования, если они возникнут.

Карта учебника