Деструктуризация выглядит как набор отдельных трюков: тут скобки фигурные, тут квадратные, тут запятые подряд, тут троеточие. На самом деле правило одно, и оно объясняет весь синтаксис целиком.
Слева от знака равенства стоит не переменная, а шаблон, который повторяет форму данных справа. Движок кладёт шаблон поверх значения, и там, где в шаблоне встретилось имя, появляется переменная с тем, что оказалось под ним.
const settings = { theme: 'dark', fontSize: 16, autosave: true };
// без деструктуризации: имя переменной пишется трижды
const theme = settings.theme;
const fontSize = settings.fontSize;
// с деструктуризацией: форма шаблона повторяет форму объекта
const { theme, fontSize } = settings;
Ничего нового в языке при этом не происходит: это по-прежнему обычное присваивание, просто записанное в другой форме. Поэтому шаблон работает везде, где работает присваивание — в объявлении через const и let, в параметрах функции, в заголовке цикла. Если разница между var, let и const ещё не улеглась, стоит начать с разбора объявления переменных.
Объекты: имя ключа становится именем переменной
В объекте у значений нет порядка, зато есть ключи. Поэтому в фигурном шаблоне важны именно имена, а не позиции — переставлять их местами можно как угодно.
const city = { label: 'Kyiv', population: 2952000, country: 'UA' };
const { country, label } = city;
console.log(label); // 'Kyiv'
console.log(country); // 'UA'
Имя ключа не всегда удобно как имя переменной: оно может быть слишком общим или конфликтовать с тем, что уже объявлено в этой области видимости. Тогда после двоеточия пишется новое имя — это не значение, а переименование.
const { label: cityTitle, country: countryCode } = city;
console.log(cityTitle); // 'Kyiv'
console.log(countryCode); // 'UA'
// console.log(label); // переменной label не появится: слева от двоеточия — ключ, а не имя
Шаблон разворачивается вглубь ровно так же, как объект. Полезно держать в голове, что промежуточные уровни при этом переменными не становятся: они работают как путь до нужного значения.
const order = {
id: 'A-1187',
customer: {
login: 'mkovalenko',
contacts: { email: 'kovalenko@example.com', phone: '+380441234567' }
}
};
const { customer: { login, contacts: { email } } } = order;
console.log(login); // 'mkovalenko'
console.log(email); // 'kovalenko@example.com'
// console.log(customer); // ReferenceError: customer не объявлен — это был путь, а не имя
Отдельный случай — когда ключ известен только во время выполнения: пришёл из настроек, из адреса страницы, из ответа сервера. Такой ключ берётся в квадратные скобки прямо внутри шаблона, и для него обязателен алиас — иначе движку неоткуда взять имя переменной.
const sortField = 'population';
const { [sortField]: sortValue } = city;
console.log(sortValue); // 2952000
// const { [sortField] } = city; // SyntaxError: имя переменной не выводится из выражения
Массивы: имя выбирает позиция
У элементов массива ключей нет, есть порядок. Поэтому в квадратном шаблоне работает счёт слева направо, а имена переменных ни на что не влияют — их можно назвать как угодно.
const coords = [50.45, 30.52, 179];
const [lat, lon] = coords;
console.log(lat); // 50.45
console.log(lon); // 30.52
Раз позиция считается запятыми, лишнюю позицию можно просто пропустить: каждая запятая двигает счётчик на один элемент вперёд, даже если между ними ничего не написано.
const [, , altitude] = coords;
console.log(altitude); // 179
Вложенные массивы разбираются тем же способом — шаблон просто повторяет вложенность:
const route = [['Kyiv', 'Warsaw'], ['Warsaw', 'Berlin']];
const [[from], [, to]] = route;
console.log(from); // 'Kyiv'
console.log(to); // 'Berlin'
Самый известный побочный эффект позиционного шаблона — обмен значениями без временной переменной. Справа собирается новый массив из текущих значений, слева он тут же разбирается обратно, но уже в другом порядке.
let current = 'Vienna';
let previous = 'Berlin';
[current, previous] = [previous, current];
console.log(current); // 'Berlin'
console.log(previous); // 'Vienna'
Значения по умолчанию и когда они молчат
Если под именем в шаблоне ничего не оказалось, переменная получит undefined. Чтобы этого не случилось, прямо в шаблоне пишется запасное значение через знак равенства.
const profile = { login: 'anna', theme: undefined, notifications: null };
const { login, theme = 'light', role = 'guest', notifications = true } = profile;
console.log(theme); // 'light' — ключ есть, но значение undefined
console.log(role); // 'guest' — ключа нет вовсе
console.log(notifications); // null — а вот тут умолчание не сработало
Последняя строка — главная ловушка умолчаний, и на неё наступают почти все. Запасное значение подставляется только вместо undefined. Любое другое значение считается настоящим, даже если по смыслу оно «пустое»: null, ноль, пустая строка и false пройдут мимо умолчания насквозь.
Практический вывод такой: если сервер отдаёт отсутствующие поля как null (а так делает большинство API), умолчания в шаблоне не помогут, и подставлять запасное значение придётся отдельной строкой.
const { notifications: rawNotifications } = profile;
const notifications = rawNotifications ?? true;
console.log(notifications); // true
Ещё пара деталей про умолчания, которые редко попадают в туториалы. Запасным значением может быть любое выражение, в том числе опирающееся на переменную, связанную левее в этом же шаблоне — шаблон разбирается по порядку:
const { width = 320, height = width * 2 } = { width: 200 };
console.log(width); // 200
console.log(height); // 400
И то же самое работает в позиционном шаблоне — удобно для параметров пагинации, которых в ответе может не оказаться:
const [page = 1, perPage = 20] = [];
console.log(page); // 1
console.log(perPage); // 20
Остаток шаблона: троеточие
Когда из данных нужно вынуть одно-два поля, а всё остальное сохранить целиком, в конце шаблона ставится троеточие с именем. Оно соберёт всё, что шаблон не разобрал поимённо. Подробный разбор самого оператора — в статье про rest и spread в JavaScript, здесь важна только его роль внутри шаблона.
Самое частое применение — убрать ключ из объекта, не трогая исходник:
const account = {
id: 42,
login: 'anna',
email: 'anna@example.com',
passwordHash: '$2b$10$N9qo8uLOickgx2ZMRZo'
};
const { passwordHash, ...publicAccount } = account;
console.log(publicAccount); // { id: 42, login: 'anna', email: 'anna@example.com' }
console.log(account.passwordHash); // на месте — исходный объект не изменился
Копия при этом получается поверхностная: вложенные объекты в остатке остаются теми же самыми ссылками, что и в исходнике. Если из такого «очищенного» объекта нужна независимая копия, читайте разбор способов клонирования объектов.
В позиционном шаблоне остаток работает так же и всегда собирает массив — даже если собирать нечего:
const [winner, ...rest] = ['Anna', 'Petro', 'Maria', 'Oleh'];
console.log(winner); // 'Anna'
console.log(rest); // ['Petro', 'Maria', 'Oleh']
const [only, ...nothing] = ['Anna'];
console.log(nothing); // []
Единственное жёсткое ограничение: остаток обязан быть последним элементом шаблона. Иначе движку непонятно, где ему остановиться, и код не доживёт даже до выполнения.
const [...others, last] = [1, 2, 3];
// SyntaxError: Rest element must be last element
Шаблон в параметрах функции
Параметр функции — это тоже присваивание, просто аргумент попадает в него при вызове. Значит, на месте параметра можно написать шаблон, и функция сразу получит распакованные поля вместо объекта целиком.
function formatPrice({ amount, currency = 'EUR', locale = 'de-DE' }) {
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
}
console.log(formatPrice({ amount: 1990.5 })); // '1.990,50 €'
console.log(formatPrice({ amount: 1990.5, locale: 'uk-UA' })); // '1 990,50 €'
Выигрыш тут не только в том, что внутри не надо писать точку через каждое слово. Сигнатура функции становится документацией: по первой строке видно, какие поля функция реально трогает и какие из них необязательные. Другие способы объявить ту же функцию — в обзоре шести форм записи функций.
У этого приёма есть один острый угол. Функция с шаблоном-объектом падает, если её вызвали вообще без аргументов: разбирать undefined нечем. Лечится умолчанием на весь параметр целиком:
formatPrice();
// TypeError: Cannot destructure property 'amount' of 'undefined'
function formatPrice({ amount = 0, currency = 'EUR' } = {}) {
// ...
}
formatPrice(); // теперь вызов без аргументов отрабатывает
Позиционный шаблон в параметрах чаще всего встречается в колбэках, когда перебирается что-то парное. Пары ключ-значение отдаёт Object.entries, и каждая такая пара разбирается прямо в заголовке цикла:
const prices = { bread: 1.2, milk: 0.9, coffee: 7.4 };
for (const [product, price] of Object.entries(prices)) {
console.log(product + ': ' + price + ' EUR');
}
const labels = Object.entries(prices).map(([product, price]) => product + ' — ' + price);
Так же читается и обход Map: он отдаёт пары сам, без вспомогательных методов. Остальные способы пройтись по объекту собраны в статье про перебор ключей и значений.
Не только объекты и массивы
Квадратный шаблон умеет разбирать не массив, а вообще любой перебираемый объект — строку, Set, Map, результат document.querySelectorAll, генератор. Массив тут просто самый частый случай, а не условие.
const [firstChar, secondChar] = 'Kyiv';
console.log(firstChar, secondChar); // 'K' 'y'
const [firstTag] = new Set(['css', 'js', 'html']);
console.log(firstTag); // 'css'
Причём берётся ровно столько элементов, сколько имён в шаблоне: перебор останавливается, как только шаблон закончился. Поэтому разобрать можно даже бесконечный источник — и браузер не повиснет:
function* counter() {
let n = 1;
while (true) yield n++;
}
const [first, second] = counter();
console.log(first, second); // 1 2
Ещё один удобный источник пар — регулярные выражения. Метод exec возвращает массив, где нулевой элемент это всё совпадение целиком, а дальше идут группы захвата. Нулевой элемент обычно не нужен, и его пропускают запятой:
const sizePattern = /(\d+)(kb|mb|gb|tb)/i;
const [, amount, unit] = sizePattern.exec('10222mb');
console.log(amount); // '10222'
console.log(unit); // 'mb'
Такой код ломается от любой правки регулярки: добавили группу в начало — и все позиции разъехались молча, без ошибки. Поэтому лучше давать группам имена и разбирать не позиции, а объект groups. Регулярка становится чуть длиннее, зато порядок групп перестаёт что-либо значить:
const sizePattern = /(?<amount>\d+)(?<unit>kb|mb|gb|tb)/i;
const { groups: { unit, amount } } = sizePattern.exec('10222mb');
console.log(amount); // '10222'
console.log(unit); // 'mb'
Где спотыкаются чаще всего
Синтаксис шаблона выучивается за вечер, а вот эти пять мест отнимают потом по полчаса каждое.
Присваивание без объявления требует скобок
Если переменные уже объявлены, а шаблон стоит в начале строки, движок читает фигурную скобку не как начало шаблона, а как начало блока кода. Лечится обёрткой в круглые скобки — выражение перестаёт быть началом инструкции.
let theme;
{ theme } = settings; // SyntaxError: Unexpected token '='
({ theme } = settings); // так работает
Точка с запятой перед квадратной скобкой
С позиционным шаблоном ситуация обратная и куда коварнее: ошибки не будет, будет неправильное поведение. Строка, начинающаяся с квадратной скобки, приклеивается к предыдущей — и движок читает её как обращение по индексу.
let current = 'Vienna';
let previous = 'Berlin';
const pair = ['a', 'b']
[current, previous] = [previous, current]
// TypeError: Cannot read properties of undefined
// движок прочитал это как pair = ['a', 'b'][current, previous] = ...
Достаточно поставить точку с запятой в конце предыдущей строки — и те же две строки отработают как задумано:
const pair = ['a', 'b'];
[current, previous] = [previous, current];
console.log(current); // 'Berlin'
Ловушка срабатывает только в проектах, где точки с запятой не ставят принципиально. Если такой стиль принят в команде, линтер обычно умеет подстраховывать сам — в ESLint за это отвечает правило no-unexpected-multiline.
Шаблон не переживает null и undefined
Разобрать можно только то, у чего есть свойства. Пустой объект шаблон переживёт спокойно — переменные просто станут undefined. А вот null и undefined дадут исключение прямо на строке с шаблоном.
const { login: emptyLogin } = {}; // undefined — не падает
const { login: nullLogin } = null; // TypeError: Cannot destructure property 'login' of 'null'
// страховка на один оператор
const { login } = maybeUser ?? {};
Глубокий шаблон падает на промежуточном уровне
Тот же TypeError прилетает и из середины вложенного шаблона, и это неочевидно: кажется, что раз внутреннему имени задано умолчание, всё защищено. Защищать нужно каждый уровень отдельно.
const response = {};
const { data: { items } } = response;
// TypeError: Cannot destructure property 'items' of 'undefined'
const { data: { items = [] } = {} } = response;
console.log(items); // [] — умолчание стоит и на промежуточном уровне
Только помните про ловушку из раздела об умолчаниях: если сервер пришлёт data: null, а не отсутствующее поле, второй вариант тоже упадёт. Умолчание сработает лишь на undefined.
Ресурсные объявления шаблон не принимают
Последний пункт — свежий. В языке появились объявления using и await using: такая переменная держит ресурс (файл, соединение, подписку) и автоматически освобождает его на выходе из блока. В отличие от const и let, шаблон справа от using по спецификации запрещён — допускается только обычное имя.
using { stream } = openReport('report.csv'); // SyntaxError
// сначала имя для самого ресурса, потом уже шаблон
using report = openReport('report.csv');
const { stream, encoding } = report;
Причина простая: освобождать ресурс движку надо по одной конкретной ссылке, а шаблон её размывает — непонятно, у какой из полученных переменных вызывать метод освобождения.
Шпаргалка
Всё, что разобрано выше, помещается в одну таблицу. Левая колонка — задача, средняя — форма шаблона, правая — что в итоге появится в области видимости.
| Задача | Шаблон | Результат |
| Взять поле объекта | const { login } = user; | переменная с именем ключа |
| Переименовать при извлечении | const { login: userLogin } = user; | переменная под новым именем |
| Ключ известен в рантайме | const { [field]: value } = user; | алиас обязателен |
| Взять элемент по позиции | const [, second] = list; | запятая двигает счётчик |
| Подставить запасное значение | const { role = 'guest' } = user; | сработает только на undefined |
| Собрать остаток | const { id, ...other } = user; | поверхностная копия без id |
| Распаковать аргумент функции | function f({ a, b } = {}) {} | вызов без аргументов не падает |
| Разобрать пары в цикле | for (const [k, v] of entries) | ключ и значение отдельными именами |
Итог
Деструктуризация не добавляет языку новых возможностей — всё то же самое пишется через точку и индексы. Она меняет другое: код перестаёт описывать, как добраться до значения, и начинает показывать, какой формы данные пришли. Сигнатура функции с шаблоном читается как список требований к аргументу, а строка с остатком — как «всё, кроме этого».
Держите в голове три вещи, и ловушек не будет. Фигурный шаблон считает имена, квадратный — позиции. Умолчание срабатывает исключительно на undefined. И на входе всегда должен быть объект: перед разбором данных из сети страховка через ?? стоит один символ, а экономит вечер в отладчике.
Комментарии (0)