Проблема: обращение к вложенным свойствам — дело ненадёжное
Лезть вглубь вложенного объекта — вполне нормально. Ровно до того момента, пока один из уровней вдруг не окажется отсутствующим:
user.address — это undefined, а попытка прочитать .city у undefined выбрасывает TypeError: Cannot read properties of undefined. Реальные данные — ответы API, распарсенный JSON, результаты запросов к DOM — сплошь состоят из полей, которых может и не быть. А писать защитные проверки на каждом уровне быстро превращается в шум:
const city = user && user.address && user.address.city;
Читается нормально при двух уровнях вложенности. А вот на четырёх — уже боль. Оператор ?. решает эту проблему куда элегантнее.
Как работает ?.: короткое замыкание на null и undefined
Optional chaining в JavaScript читает свойство только в том случае, если значение перед точкой — не null и не undefined. Если же там одно из этих значений, всё выражение сразу возвращает undefined и дальше не выполняется.
В первой строке name читается как обычно. Во второй строке мы обращаемся к user.address, получаем undefined — и дальше цепочка обрывается. Третья строка без ?. выбросила бы ошибку, потому что .toUpperCase() вызвался бы у undefined; а с ?. всё выражение спокойно возвращает undefined.
Запомнить это просто: ?. говорит «если то, что стоит передо мной, равно null или undefined — сдавайся и верни undefined, иначе продолжай дальше».
Optional chaining для массивов и вызова функций
Три варианта синтаксиса, суть одна:
?.[...]— доступ к элементу массива или динамическому свойству.?.()— вызов того, что может и не оказаться функцией.?.name— обычное обращение к свойству.
Все три варианта одинаково срабатывают по принципу короткого замыкания. user?.notAMethod?.() не выбросит ошибку, даже если notAMethod не существует — второй ?. увидит undefined и просто остановится.
Особенно удобно это с необязательными колбэками:
Больше никаких охранников в духе if (typeof onDone === "function") перед каждым вызовом.
Короткое замыкание срабатывает только на null и undefined
Вот тут новички часто спотыкаются. Оператор ?. реагирует исключительно на nullish-значения. Остальные falsy-значения — 0, "", false, NaN — это вполне валидные объекты для продолжения цепочки (ну, насколько это позволяет автобоксинг):
data.count равно 0 — значение ложное (falsy), но не является null или undefined, поэтому ?.toFixed(2) спокойно отрабатывает и возвращает "0.00". Сравните с тем, как ведёт себя &&:
Версия с && возвращает 0, потому что data.count — falsy-значение, и выражение закорачивается. Версия с ?. возвращает "0.00", потому что 0 не является nullish. Если вам действительно нужно остановиться на 0, берите &&. Если же останавливаться нужно только когда «значения нет», ваш выбор — ?..
Где ставить ? — это важно
Оператор ?. защищает то, что стоит перед ним, а не после. Поэтому ставьте его на том уровне, который может отсутствовать:
Все три варианта работают, потому что на деле ничего не отсутствует. Но если config.server может оказаться undefined, нужно писать config.server?.host — ставить ?. перед server бесполезно, ведь проблема именно в попытке достучаться до .host у несуществующего server.
Хорошее правило: ставьте ?. на тех уровнях, где значение перед оператором действительно может быть null или undefined. Если лепить ?. после каждой точки «на всякий случай», это маскирует настоящие баги и превращает код в кашу.
Присваивание через ?. не работает
Optional chaining работает только на чтение. Слева от оператора присваивания его использовать нельзя:
user?.address?.city = "Paris"; // SyntaxError
Оно и логично, если вдуматься — что вообще должно означать присваивание свойству undefined? Если нужно записать значение только при условии, что родительский объект существует, распишите это явно:
Когда стоит использовать ?., а когда — нет
Оператор ?. раскрывается во всей красе, когда значение действительно может отсутствовать:
- Ответы API, где поля могут быть, а могут и не быть.
- Поиск элементов в DOM:
document.querySelector(".banner")?.remove(). - Колбэки, которые могут и не прийти:
options.onError?.(err). - Цепочки вызовов библиотек, где промежуточный результат может оказаться
null.
А вот когда значение обязано быть на месте — это уже не тот инструмент. Если расставлять ?. просто чтобы заглушить ошибки, шумный и легко находимый баг («TypeError в строке 42») превращается в тихий: переменная загадочным образом оказывается undefined где-то через три функции. Если что-то должно существовать — пусть код падает. Стек-трейс в этом случае вам только на руку.
Пример из жизни
Достаём значение из ответа API, который может прийти неполным:
Каждый ?. отвечает за один уровень неопределённости. avatarUrl аккуратно получит undefined, а onClick?.() вызовет обработчик, только если он действительно есть.
Обратите внимание на ?? в строке с displayName — это вторая половина того же паттерна. ?. отдаёт undefined, когда чего-то нет; ?? позволяет подставить значение по умолчанию, не спотыкаясь о вполне валидные «ложные» значения вроде 0 или "".
Дальше: оператор нулевого слияния
?. и ?? — это одна и та же идея, применённая к разным задачам: оба считают «отсутствующими» только null и undefined, а остальные falsy-значения не трогают. Дальше разберём, как ?? помогает задавать адекватные значения по умолчанию — и почему || все эти годы тихо делал это неправильно.
Часто задаваемые вопросы
Что делает оператор ?. в JavaScript?
?. обращается к свойству, индексу массива или методу только если значение слева от него не null и не undefined. Если там как раз null или undefined, выражение замыкается и возвращает undefined вместо того, чтобы бросить ошибку. Например, user?.address?.city спокойно отработает, даже если user или address отсутствует.
Когда стоит использовать optional chaining?
Тогда, когда значение действительно может отсутствовать по смыслу: ответы API с необязательными полями, поиск DOM-элемента, которого может не оказаться, опциональные коллбэки. А вот маскировать ?. баги, где значение обязано быть, — плохая идея. В таких местах отсутствующее значение — это полезный сигнал, и лучше увидеть ошибку, чем молчаливый undefined.
В чём разница между ?. и &&?
В большинстве случаев результат одинаковый, но есть важный нюанс: ?. срабатывает только на null и undefined, а && — на любое falsy-значение, включая 0, '' и false. То есть obj && obj.count вернёт 0, если count равен 0, — и obj?.count тоже вернёт 0, но цепочка прерывается именно на null/undefined, а не на любом «пустом» значении.
Можно ли использовать ?. с массивами и вызовами функций?
Да. arr?.[0] безопасно читает элемент массива, а fn?.() вызывает функцию только если она существует. Правило то же: если слева от ?. оказался null или undefined, всё выражение даёт undefined, и дальше ничего не выполняется.