Урок 16 из 49 · Месяц 2. Git и введение в JavaScript

Деструктуризация, spread, rest, ?. и ??

Содержание урока

Современный JavaScript полон коротких записей с тремя точками, фигурными скобками слева от знака = и странными знаками ?. и ??. В этом уроке разберём их по очереди: деструктуризацию (быстро достать значения из объекта или массива), spread (развернуть), rest (собрать), optional chaining (безопасно добраться до вложенного свойства) и оператор нулевого слияния (подставить значение по умолчанию). Без этих приёмов не обходится ни один React-проект.

Деструктуризация объектов

Деструктуризация — способ одной строкой «распаковать» свойства объекта в отдельные переменные. Аналогия: вы открыли посылку и сразу разложили вещи по полкам.

Без деструктуризации:

const user = { name: "Айдана", age: 23, city: "Бишкек" };

const name = user.name;
const age = user.age;
const city = user.city;

С деструктуризацией:

const user = { name: "Айдана", age: 23, city: "Бишкек" };
const { name, age, city } = user;

console.log(name); // "Айдана"
console.log(age);  // 23

Как это читать: «создай переменные name, age, city и положи в них одноимённые свойства из user». Порядок не важен — значения ищутся по имени ключа.

Переименование и значения по умолчанию

const product = { title: "Ноутбук", price: 65000 };

const { title: productName, price, discount = 0 } = product;

console.log(productName); // "Ноутбук"
console.log(discount);    // 0 — свойства нет, взяли значение по умолчанию
// console.log(title);    // ReferenceError — переменная называется productName
  • title: productName — взять свойство title, но назвать переменную productName;
  • discount = 0 — если свойства нет (оно undefined), используется 0.

Вложенная деструктуризация

const order = {
  id: 77,
  customer: { name: "Тимур", phone: "+996 555 000 000" },
};

const {
  id,
  customer: { name, phone },
} = order;

console.log(id, name, phone); // 77 "Тимур" "+996 555 000 000"

Здесь customer: { ... } значит «зайди в свойство customer и распакуй его». Переменной customer при этом не создаётся.

Деструктуризация в параметрах функции

Самый частый случай на практике — функция получает объект и сразу его распаковывает:

function renderCard({ title, price, inStock = true }) {
  return `${title} — ${price} сом ${inStock ? "" : "(нет в наличии)"}`;
}

console.log(renderCard({ title: "Кружка", price: 350 }));
// "Кружка — 350 сом "

Так в React описывают пропсы компонентов: function Card({ title, price }) { ... }.

Деструктуризация массивов

У массивов значения берутся по позиции, поэтому используются квадратные скобки:

const colors = ["red", "green", "blue"];
const [first, second] = colors;

console.log(first);  // "red"
console.log(second); // "green"

const [, , third] = colors; // пропуск элементов запятыми
console.log(third); // "blue"

Удобный трюк — поменять значения двух переменных местами:

let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1

Вы ещё встретите это в React: const [count, setCount] = useState(0) — именно деструктуризация массива.

Spread — «развернуть»

Spread (..., «разворот») раскладывает массив или объект на отдельные элементы. Аналогия: высыпать содержимое одной коробки в другую.

Spread с массивами

const fruits = ["яблоко", "банан"];
const vegetables = ["морковь", "огурец"];

const all = [...fruits, ...vegetables];
console.log(all); // ["яблоко", "банан", "морковь", "огурец"]

const copy = [...fruits];        // копия массива
const withKiwi = [...fruits, "киви"]; // новый массив + элемент

const nums = [4, 18, 7];
console.log(Math.max(...nums));  // 18 — передали элементы как отдельные аргументы

Spread с объектами

const user = { name: "Руслан", age: 30 };

const userCopy = { ...user };
const updated = { ...user, age: 31, city: "Ош" };

console.log(updated); // { name: "Руслан", age: 31, city: "Ош" }
console.log(user);    // { name: "Руслан", age: 30 } — исходный не изменился

Правило: если ключ повторяется, побеждает тот, что правее. Поэтому { ...user, age: 31 } перезаписывает возраст, а { age: 31, ...user } — нет.

Это основной способ «изменить» данные, не трогая оригинал. В React так обновляют состояние:

const settings = { theme: "light", lang: "ru", notifications: true };
const newSettings = { ...settings, theme: "dark" };
const original = { name: "Ali", address: { city: "Бишкек" } };
const shallow = { ...original };
shallow.address.city = "Ош";
console.log(original.address.city); // "Ош" — вложенный объект общий!

const deep = structuredClone(original);
deep.address.city = "Талас";
console.log(original.address.city); // "Ош" — не изменился

Rest — «собрать остальное»

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

  • ... справа от = или при вызове функции — это spread (разворачиваем);
  • ... слева от = или в параметрах функции — это rest (собираем).

Rest в деструктуризации

const [winner, ...others] = ["Аня", "Бека", "Влад", "Гуля"];
console.log(winner); // "Аня"
console.log(others); // ["Бека", "Влад", "Гуля"]

const { password, ...safeUser } = { login: "admin", password: "123", role: "owner" };
console.log(safeUser); // { login: "admin", role: "owner" }

Второй пример — популярный приём: убрать из объекта лишнее поле (например, пароль перед выводом на страницу), не изменяя исходный объект.

Rest в параметрах функции

Функция, которая принимает любое количество аргументов:

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2));        // 3
console.log(sum(5, 10, 15, 20)); // 50

...numbers собирает все переданные аргументы в настоящий массив, поэтому у него есть reduce и другие методы из урока Методы перебора массива.

Rest-параметр может быть только последним: function f(first, ...rest) — можно, function f(...rest, last) — ошибка.

Optional chaining ?.

Представьте, что данные пришли с сервера, и у одного пользователя нет адреса:

const user = { name: "Ислам" };
console.log(user.address.city); // TypeError: Cannot read properties of undefined (reading 'city')

Ошибка останавливает весь скрипт. Optional chaining (?., «необязательная цепочка») проверяет: если слева null или undefined, дальше не идти и вернуть undefined:

console.log(user.address?.city);  // undefined — без ошибки
console.log(user?.name);          // "Ислам"

Работает и с массивами, и с вызовом методов:

const data = { items: [] };

console.log(data.items?.[0]?.title);  // undefined — первого элемента нет
console.log(user.sayHi?.());          // undefined — метода нет, но ошибки тоже нет
  • obj?.prop — свойство;
  • obj?.[key] — свойство по ключу или индексу;
  • obj.method?.() — вызвать метод, только если он существует.

Пример с DOM: элемента может не быть на странице.

document.querySelector("#promo")?.classList.add("visible");

Оператор нулевого слияния ??

?? возвращает правую часть, только если левая — null или undefined. Это способ задать значение по умолчанию.

const userName = null;
console.log(userName ?? "Гость"); // "Гость"

const city = "Бишкек";
console.log(city ?? "Не указан"); // "Бишкек"

Чем ?? отличается от ||

|| подставляет значение по умолчанию для любого falsy значения: 0, "", false, null, undefined, NaN. Иногда это ошибка:

const settings = { volume: 0, nickname: "" };

console.log(settings.volume || 50); // 50 — а пользователь специально поставил 0!
console.log(settings.volume ?? 50); // 0  — правильно

console.log(settings.nickname || "Аноним"); // "Аноним"
console.log(settings.nickname ?? "Аноним"); // ""

Сравнение на разных значениях слева:

Значение x     x || "default"    x ?? "default"
null           "default"         "default"
undefined      "default"         "default"
0              "default"         0
""             "default"         ""
false          "default"         false

Правило: если 0, пустая строка или false — нормальные значения, используйте ??.

Есть и короткая форма присваивания: x ??= 10 — записать 10, только если x равен null или undefined.

?. и ?? вместе

Эти операторы отлично работают в паре: безопасно достать значение и подставить запасное:

const response = { user: { profile: null } };

const avatar = response.user?.profile?.avatar ?? "default-avatar.png";
console.log(avatar); // "default-avatar.png"

Пример: всё вместе

Обработаем данные, похожие на ответ сервера:

const apiResponse = {
  status: "ok",
  data: {
    users: [
      { id: 1, name: "Айгуль", contacts: { email: "aigul@example.com" }, rating: 0 },
      { id: 2, name: "Эрлан", contacts: null },
    ],
  },
};

const { data: { users = [] } = {} } = apiResponse;

const cards = users.map(({ id, name, contacts, rating, ...rest }) => ({
  id,
  title: name,
  email: contacts?.email ?? "нет почты",
  rating: rating ?? "без оценки",
  ...rest,
}));

console.log(cards);
// [
//   { id: 1, title: "Айгуль", email: "aigul@example.com", rating: 0 },
//   { id: 2, title: "Эрлан", email: "нет почты", rating: "без оценки" }
// ]

Разбор:

  1. const { data: { users = [] } = {} } = apiResponse — достаём users из вложенного data; если data или users нет, получим пустой массив вместо ошибки.
  2. В map деструктурируем каждого пользователя прямо в параметрах.
  3. contacts?.email ?? "нет почты" — у Эрлана contacts равен null, поэтому ?. вернёт undefined, а ?? подставит запасной текст.
  4. rating ?? "без оценки" — у Айгуль рейтинг 0 сохранился (с || он бы потерялся).
  5. ...rest собирает остальные поля, а в новом объекте ...rest их разворачивает — ничего не потеряется, если сервер добавит новые поля.

Типичные ошибки

  • Путают фигурные и квадратные скобки: {} — для объектов (по имени), [] — для массивов (по позиции).
  • Деструктуризация undefined: const { a } = undefined — ошибка. Задавайте значение по умолчанию: = {}.
  • Ожидание глубокой копии от spread — вложенные объекты остаются общими.
  • Rest не последним в параметрах или деструктуризации — синтаксическая ошибка.
  • || вместо ?? там, где 0 или "" — допустимые значения.
  • Смешивание ?? с || или && без скобок — синтаксическая ошибка: пишите (a || b) ?? c.

Практика

  1. Распаковка. Дан const book = { title: "JS для всех", author: "Иванов", year: 2024 }. Деструктурируйте его, переименовав title в bookTitle и добавив pages = 300 по умолчанию. Ожидаемый результат: все четыре переменные выводятся в консоль.
  2. Слияние. Объедините два массива [1, 2, 3] и [4, 5] через spread, добавив 0 в начало, и найдите максимум через Math.max(...). Ожидаемый результат: [0, 1, 2, 3, 4, 5] и 5.
  3. Обновление профиля. Напишите функцию updateUser(user, changes), которая возвращает новый объект через spread, не меняя исходный. Ожидаемый результат: исходный объект прежний, новый — с изменениями.
  4. Средняя оценка. Напишите average(...grades) с rest-параметром. Если оценок нет, верните 0. Ожидаемый результат: average(5, 4, 3) → 4, average() → 0.
  5. Безопасные данные. Создайте массив из трёх «ответов сервера», где у некоторых нет profile, profile.city или stats.likes равен 0. Выведите для каждого строку «Имя — город — лайки», используя ?. и ??. Ожидаемый результат: нигде нет ошибки, вместо пустых данных — «не указан», 0 лайков сохраняется.

Итоги

  • Деструктуризация объекта { a, b } = obj берёт значения по имени, массива [x, y] = arr — по позиции.
  • При деструктуризации можно переименовывать (a: newName) и задавать значения по умолчанию (a = 0).
  • Spread ... разворачивает массив или объект — для копий и объединения; копия поверхностная.
  • Rest ... собирает «остальное» в массив или объект — в деструктуризации и параметрах функции.
  • ?. безопасно обращается к свойствам, которых может не быть.
  • ?? подставляет значение по умолчанию только для null и undefined, в отличие от ||.
Отзыв