Деструктуризация выглядит как набор отдельных трюков: тут скобки фигурные, тут квадратные, тут запятые подряд, тут троеточие. На самом деле правило одно, и оно объясняет весь синтаксис целиком.

Слева от знака равенства стоит не переменная, а шаблон, который повторяет форму данных справа. Движок кладёт шаблон поверх значения, и там, где в шаблоне встретилось имя, появляется переменная с тем, что оказалось под ним.

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
Поддержка браузерами
chrome
Chrome
60
firefox
Firefox
55
edge
Edge
79
safari
Safari
11.1
opera
Opera
47

Шаблон в параметрах функции

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

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'

Поддержка браузерами
chrome
Chrome
64
firefox
Firefox
78
edge
Edge
79
safari
Safari
11.1
opera
Opera
51

Где спотыкаются чаще всего

Синтаксис шаблона выучивается за вечер, а вот эти пять мест отнимают потом по полчаса каждое.

Присваивание без объявления требует скобок

Если переменные уже объявлены, а шаблон стоит в начале строки, движок читает фигурную скобку не как начало шаблона, а как начало блока кода. Лечится обёрткой в круглые скобки — выражение перестаёт быть началом инструкции.

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;

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

Поддержка браузерами
chrome
Chrome
134
firefox
Firefox
141
edge
Edge
134
safari
Safari
 
opera
Opera
119

Шпаргалка

Всё, что разобрано выше, помещается в одну таблицу. Левая колонка — задача, средняя — форма шаблона, правая — что в итоге появится в области видимости.

Задача Шаблон Результат
Взять поле объекта 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. И на входе всегда должен быть объект: перед разбором данных из сети страховка через ?? стоит один символ, а экономит вечер в отладчике.