$ sudo teach IT
МОДУЛЬ 5 · УРОК 4

Изменение объектов, вложенные структуры и optional chaining

Реальные данные редко бывают плоскими. Пользователь живёт в городе, город — в стране, у адреса есть улица. Учимся работать с объектами внутри объектов и делать это безопасно.

⏱ ~30 минут 🎓 Для новичков 🔗 Вложенность

✏️ Добавление и изменение свойств

Объект в JavaScript — это не закрытая структура. После создания можно в любой момент добавить новые свойства или изменить существующие. Синтаксис — тот же, что и для чтения: точка или скобки, плюс оператор присваивания =.

// Начинаем с простого объекта
let user = {
  name: "Пётр",
  age: 30
};
console.log(user); // { name: "Пётр", age: 30 }

// Изменяем существующее свойство
user.age = 31;
console.log(user.age); // 31

// Добавляем новое свойство (которого раньше не было)
user.email = "petr@example.com";
user.isActive = true;
console.log(user);
// { name: "Пётр", age: 31, email: "petr@example.com", isActive: true }

// Добавляем через скобки (например, если ключ в переменной)
let field = "score";
user[field] = 850;
console.log(user.score); // 850

💡 Объекты — гибкие. В отличие от языков со статической типизацией (Java, C#), где структура объекта фиксирована, в JS можно добавлять и удалять свойства в любой момент. Это мощно, но требует аккуратности.

🪆 Вложенные объекты — объект внутри объекта

Значение свойства объекта может само быть объектом. Это называется вложенностью. В реальных данных это встречается постоянно: данные пользователя содержат адрес, адрес содержит город, город содержит страну.

Представьте, как выглядят данные пользователя в API:

let user = {
  id: 1,
  name: "Алексей",
  age: 25,
  address: {               // значение — тоже объект!
    country: "Россия",
    city: "Санкт-Петербург",
    street: "Невский проспект",
    building: 28
  },
  contacts: {              // ещё один вложенный объект
    phone: "+7-900-123-45-67",
    email: "alex@email.com",
    telegram: "@alex_dev"
  }
};

Чтобы добраться до вложенных свойств, используем цепочку точек — каждая точка «идёт вглубь» на один уровень:

// Обращаемся к вложенным свойствам через цепочку точек
console.log(user.name);             // "Алексей"
console.log(user.address.city);     // "Санкт-Петербург"
console.log(user.address.country);  // "Россия"
console.log(user.contacts.email);   // "alex@email.com"

// Глубокая вложенность — просто продолжаем цепочку
let company = {
  name: "Технологии Будущего",
  hq: {
    country: {
      name: "Россия",
      code: "RU"
    },
    city: "Москва"
  }
};

console.log(company.hq.country.name); // "Россия"
console.log(company.hq.country.code); // "RU"

Изменение вложенных свойств — тот же синтаксис, просто с цепочкой:

user.address.city = "Москва";           // переехал
user.contacts.telegram = "@new_handle"; // сменил никнейм

// Добавить новое вложенное свойство
user.address.zipCode = "190000";

console.log(user.address);
// { country: "Россия", city: "Москва", street: "Невский...", building: 28, zipCode: "190000" }

💥 Опасность: обращение к несуществующей вложенности

Вот типичная ловушка при работе с вложенными объектами. Представьте, что данные пришли с сервера, и у части пользователей нет адреса:

let user1 = {
  name: "Иван",
  address: {
    city: "Казань"
  }
};

let user2 = {
  name: "Мария"
  // нет поля address!
};

// Для user1 всё отлично
console.log(user1.address.city); // "Казань"

// Для user2 — ОШИБКА!
console.log(user2.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')
// user2.address → undefined → undefined.city → ОШИБКА

⚠️ Классическая ошибка: «Cannot read properties of undefined». Возникает когда пытаемся обратиться к свойству промежуточного объекта, которого нет. Чтобы этого избежать — раньше писали длинные проверки. Теперь есть элегантное решение.

🛡️ Optional chaining ?. — безопасный доступ

Оператор ?. (optional chaining, «опциональная цепочка») позволяет безопасно обращаться к вложенным свойствам. Если промежуточное значение равно null или undefined — выражение останавливается и возвращает undefined вместо ошибки.

let user1 = { name: "Иван", address: { city: "Казань" } };
let user2 = { name: "Мария" }; // нет address

// Старый способ (до ?.) — громоздкий
let city1 = user1.address && user1.address.city ? user1.address.city : "не указан";
let city2 = user2.address && user2.address.city ? user2.address.city : "не указан";

// Новый способ с ?.  — чисто и коротко!
let city3 = user1.address?.city ?? "не указан"; // "Казань"
let city4 = user2.address?.city ?? "не указан"; // "не указан" — без ошибки!

console.log(city3); // "Казань"
console.log(city4); // "не указан"

Как работает ?.: каждый шаг цепочки проверяется. Как только встречается null или undefined — вся цепочка возвращает undefined и вычисление прекращается.

let data = {
  user: {
    profile: {
      avatar: {
        url: "https://example.com/avatar.jpg"
      }
    }
  }
};

// Безопасный доступ на любую глубину
console.log(data.user?.profile?.avatar?.url);      // "https://..."
console.log(data.user?.settings?.theme);           // undefined (без ошибки)
console.log(data.user?.profile?.avatar?.size);     // undefined (без ошибки)

// Работает и с методами
let arr = null;
console.log(arr?.length);     // undefined (не ошибка)
console.log(arr?.map(x => x)); // undefined (не ошибка)

💡 ?? — оператор nullish coalescing. Часто идёт в паре с ?.. Выражение a ?? b возвращает b только если a равно null или undefined. Удобно для задания значений по умолчанию: user.city ?? "не указан".

📋 Spread оператор {...obj} — копирование объекта

Оператор spread ... «разворачивает» объект — вытаскивает все его свойства наружу. Это самый популярный способ создать поверхностную копию объекта или объединить несколько объектов в один.

// Копирование объекта
let original = { name: "Ольга", age: 28, city: "Уфа" };

let copy = { ...original };
copy.age = 29;           // меняем копию
console.log(original.age); // 28 — оригинал не изменился!
console.log(copy.age);     // 29

// Добавление новых свойств при копировании
let extended = { ...original, email: "olga@mail.ru", score: 100 };
console.log(extended);
// { name: "Ольга", age: 28, city: "Уфа", email: "olga@mail.ru", score: 100 }

// Перезапись свойств (новое значение "выигрывает")
let updated = { ...original, age: 30, city: "Москва" };
console.log(updated);
// { name: "Ольга", age: 30, city: "Москва" }

// Слияние двух объектов
let defaults = { theme: "light", lang: "ru", fontSize: 14 };
let userPrefs = { theme: "dark", fontSize: 16 };

let settings = { ...defaults, ...userPrefs }; // userPrefs перезаписывает defaults
console.log(settings);
// { theme: "dark", lang: "ru", fontSize: 16 }

⚠️ Поверхностная копия: spread копирует только первый уровень. Если значение свойства само является объектом — скопируется ссылка, а не объект. Изменение вложенного объекта затронет и оригинал. Для глубокого копирования используют другие методы (JSON.parse/JSON.stringify, structuredClone).

🧪 Практика: работа с профилем пользователя

// Данные приходят с сервера — иногда адрес есть, иногда нет
function displayUserCity(user) {
  // Безопасно достаём город через optional chaining
  // Если нет — показываем "Город не указан"
  return user?.address?.city ?? "Город не указан";
}

let userA = { name: "Коля", address: { city: "Новосибирск" } };
let userB = { name: "Таня" };
let userC = { name: "Саша", address: null };

console.log(displayUserCity(userA)); // "Новосибирск"
console.log(displayUserCity(userB)); // "Город не указан"
console.log(displayUserCity(userC)); // "Город не указан"
console.log(displayUserCity(null));  // "Город не указан" — даже null не ломает!

// Обновляем профиль через spread
function updateProfile(profile, changes) {
  return { ...profile, ...changes, updatedAt: new Date().toISOString() };
}

let profile = { name: "Коля", age: 22, city: "Новосибирск" };
let updated = updateProfile(profile, { age: 23, city: "Москва" });

console.log(profile); // { name: "Коля", age: 22, city: "Новосибирск" } — не изменился!
console.log(updated); // { name: "Коля", age: 23, city: "Москва", updatedAt: "..." }

✅ Итоги урока

  • Свойства объекта добавляются и изменяются через obj.key = value или obj[key] = value
  • Вложенные объекты — когда значение свойства само является объектом
  • Доступ к вложенным свойствам через цепочку точек: user.address.city
  • Обращение к несуществующему промежуточному объекту → TypeError
  • ?. (optional chaining) — безопасный доступ: возвращает undefined вместо ошибки
  • ?? — значение по умолчанию если результат null или undefined
  • {...obj} — поверхностная копия объекта; можно объединять и перезаписывать свойства

Следующий урок: перебираем коллекции с помощью for...of и for...in. 🚀

Тест: вложенные объекты и optional chaining

7 вопросов

Безопасное получение вложенного свойства

Напишите функцию getNested(obj, path), которая принимает объект и строку path вида 'a.b.c' и безопасно получает значение по этому пути, используя optional chaining. Если путь не существует — верните 'не найдено'. Подсказка: строку нужно разбить по точке и пройтись по ключам.

Слияние настроек через spread

Напишите функцию mergeSettings(defaults, userSettings), которая объединяет два объекта с помощью spread-оператора. Настройки пользователя должны перезаписывать значения по умолчанию. Функция возвращает новый объект, не изменяя исходные.