Вложенные объекты и безопасный доступ

В прошлом уроке объект user хранил простые значения: имя, возраст, город. Но данные часто сами имеют внутреннюю структуру. Например, адрес – это не одно значение: у него есть город, дом, а позже могут появиться улица, квартира и индекс.

Вместо десятка отдельных свойств удобнее собрать адрес в собственный объект и положить его внутрь user:

nested.jsjs
const user = {  name: 'Ира',  address: {    city: 'Москва',    house: 15,  },};console.log(user.address.city);console.log(user.address.house);
Консоль
Москва
15

Такой объект называют вложенным. Здесь значением свойства address является ещё один объект.

user name : 'Ира' address city : 'Москва' house : 15
Каждая точка делает один шаг по этой структуре.

Цепочка точек – несколько обращений подряд

JavaScript не прыгает сразу к city. Он выполняет цепочку по одному шагу слева направо:

  1. берёт значение user;
  2. читает у него address;
  3. получает объект { city: 'Москва', house: 15 };
  4. уже у этого объекта читает city.

user.address.city – просто короткая запись этой последовательности обращений.

Цепочка точек: шаг за шагом вглубь
const user = {
name: 'Ира',
address: {
city: 'Москва',
house: 15,
},
};
console.log(user.address.city);
Сначала у user читаем свойство address.
Консоль
Нет вывода в консоль.

Меняем данные на нужной глубине

Читать и изменять вложенные свойства можно той же цепочкой:

nested-change.jsjs
const user = {  name: 'Ира',  address: { city: 'Москва', house: 15 },};user.address.city = 'Казань';console.log(user.address.city);user.address = { city: 'Тверь', house: 3 };console.log(user.address.house);
Консоль
Казань
3

В первом присваивании меняется только свойство city внутри объекта address. Во втором присваивание стоит уже после user.address, поэтому старый объект address целиком заменяется новым:

меняется одно свойство внутри address
user.address.city = 'Казань';// address: { city: 'Казань', house: 15 }
старый объект address заменяется новым
user.address = { city: 'Тверь', house: 3 };// address: { city: 'Тверь', house: 3 }

Когда вложенного объекта нет

Не у каждого пользователя указан адрес:

no-address.jsjs
const guest = {  name: 'Гость',};console.log(guest.address);console.log(guest.address.city);
Консоль
undefined
TypeError: Cannot read properties of undefined (reading 'city')

Первое обращение совершенно допустимо: guest.address → undefined. Отсутствующее свойство объекта само по себе ошибки не вызывает.

Проблема начинается на следующем шаге. После первого обращения выражение по смыслу превращается в undefined.city. А прочитать свойство у undefined нельзя – поэтому JavaScript выбрасывает TypeError:

guest.address.city читаем address undefined.city пытаемся читать city TypeError
Ошибка возникает на втором шаге – при чтении свойства у undefined.

Сообщение Cannot read properties of undefined (reading 'city') буквально подсказывает причину: JavaScript попытался прочитать city у значения undefined. Поэтому искать проблему нужно в предыдущем звене цепочки: почему guest.address оказался undefined.

Проверка перед обращением

guard.jsjs
const guest = {  name: 'Гость',};if (guest.address) {  console.log(guest.address.city);} else {  console.log('Адрес не указан');}
Консоль
Адрес не указан

Проверка решает проблему: к .city мы доходим только тогда, когда address существует. Здесь это безопасно, потому что address ожидается либо объектом, либо undefined. Для одной проверки это нормально. Но если цепочка длинная, такой код быстро разрастается. Для безопасного чтения свойства в JavaScript есть короткая запись – ?..

Optional chaining: ?.

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

  • если там объект – свойство читается как обычно;
  • если там null или undefined – выражение сразу возвращает undefined, а следующий переход не выполняется.
optional.jsjs
const guest = {  name: 'Гость',};const user = {  name: 'Ира',  address: {    city: 'Москва',  },};console.log(guest.address?.city);console.log(user.address?.city);
Консоль
undefined
Москва

У guest свойства address нет: слева от ?. оказался undefined, переход к city не выполнился, результат – undefined. У user адрес есть, и ?. сработал как обычная точка.

Останавливают ?. только два значения – null и undefined. Ноль, пустая строка и false для него обычные значения: на них цепочка не прервётся и пойдёт дальше:

optional-stops.jsjs
const empty = '';const address = null;let city;console.log(empty?.length);console.log(address?.city);console.log(city?.length);
Консоль
0
undefined
undefined
Как ?. останавливает цепочку
const guest = {
name: 'Гость',
};
console.log(guest.address?.city);
Сначала читаем guest.address.
partial.jsjs
const order = {  customer: { name: 'Ира' },};// customer есть, address нет – упадёт на .cityconsole.log(order.customer?.address.city);
Консоль
TypeError: Cannot read properties of undefined (reading 'city')

Запись order.customer?.address.city проверяет только customer. Если customer отсутствует, ?. вернёт undefined. Но если customer существует, а address нет, код всё равно дойдёт до undefined.city и упадёт. Поэтому ?. ставят перед каждым переходом, где промежуточного значения действительно может не быть:

full-chain.jsjs
console.log(order.customer?.address?.city);
Консоль
undefined

Если в order может лежать undefined или null, цепочка начинается так: order?.customer?.address?.city.

?. работает и с квадратными скобками

optional-brackets.jsjs
const user = {  settings: {    theme: 'dark',  },};const guest = {};const key = 'theme';console.log(user.settings?.[key]);console.log(guest.settings?.[key]);
Консоль
dark
undefined

Для динамического ключа пишут ?.[key]. Идея та же: если settings существует, JavaScript вычислит key и прочитает нужное свойство. Если settings равен null или undefined, результатом станет undefined.

Через ?. нельзя присваивать

optional-assign.jsjs
// SyntaxError: слева от присваивания ?. нельзяuser.address?.city = 'Казань';

Optional chaining нельзя использовать слева от обычного присваивания. Если свойство нужно изменить, сначала убедитесь, что промежуточный объект существует:

assign-guarded.jsjs
if (user.address) {  user.address.city = 'Казань';}

// теперь вы знаете

  1. Значение свойства может быть объектом; цепочка user.address.city – несколько обращений подряд, по одному на точку
  2. user.address.city = … меняет свойство внутри address, user.address = { … } заменяет вложенный объект целиком
  3. Отсутствующее свойство даёт undefined, а чтение свойства у undefined – TypeError; причину ищут в предыдущем звене
  4. ?. проверяет значение слева: объект – читаем дальше, null или undefined – всё выражение даёт undefined
  5. ?. защищает только переход сразу после себя; с динамическим ключом пишут ?.[key]; слева от присваивания ?. нельзя
  6. ?. ставят там, где отсутствие данных – нормальная ситуация, а не «на всякий случай»

Проверьте себя

Верно ли: если у объекта нет свойства address, то уже чтение user.address вызывает ошибку?

Что появится в консоли?

const user = { name: 'Ира' };
console.log(user.address?.city);

Последняя строка падает с ошибкой TypeError: Cannot read properties of undefined (reading 'address'). Чтение какого свойства вернуло undefined? Впишите имя свойства.

const order = {
id: 7,
customer: {
name: 'Ира',
address: { street: 'Лесная', house: 4 },
},
};
console.log(order.delivery.address.street);

Что появится в консоли?

const user = {
name: 'Ира',
nickname: '',
address: null,
};
console.log(user.nickname?.length);
console.log(user.address?.city);

По правилам программы у каждого заказа обязательно есть покупатель – свойство customer. Верно ли, что здесь ?. не нужен и лучше писать order.customer.name?

const order = { id: 3, total: 1200 };
console.log(order.customer?.name);

Что произойдёт при выполнении последней строки?

const order = { customer: { name: 'Ира' } };
console.log(order.customer?.address.city);

Можно ли записать user.address?.city = 'Казань', чтобы изменить город только при наличии адреса?

Что появится в консоли?

const guest = {};
const key = 'theme';
console.log(guest.settings?.[key]);