В JavaScript есть две чаще всего используемые структуры данных – это Object
и Array
.
- Объекты позволяют нам создавать одну сущность, которая хранит элементы данных по ключам.
- Массивы позволяют нам собирать элементы данных в упорядоченный список.
Но когда мы передаём их в функцию, то ей может понадобиться не объект/массив целиком, а элементы по отдельности.
Деструктурирующее присваивание – это специальный синтаксис, который позволяет нам «распаковать» массивы или объекты в несколько переменных, так как иногда они более удобны.
Деструктуризация также прекрасно работает со сложными функциями, которые имеют много параметров, значений по умолчанию и так далее. Скоро мы увидим это.
Деструктуризация массива
Вот пример деструктуризации массива на переменные:
// у нас есть массив с именем и фамилией
let arr = ["Ilya", "Kantor"];
// деструктурирующее присваивание
// записывает firstName = arr[0]
// и surname = arr[1]
let [firstName, surname] = arr;
alert(firstName); // Ilya
alert(surname); // Kantor
Теперь мы можем использовать переменные вместо элементов массива.
Отлично смотрится в сочетании со split
или другими методами, возвращающими массив:
let [firstName, surname] = "Ilya Kantor".split(' ');
alert(firstName); // Ilya
alert(surname); // Kantor
Как вы можете видеть, синтаксис прост. Однако есть несколько странных моментов. Давайте посмотрим больше примеров, чтобы лучше понять это.
«Деструктурирующее присваивание» не уничтожает массив. Оно вообще ничего не делает с правой частью присваивания, его задача – только скопировать нужные значения в переменные.
Это просто короткий вариант записи:
// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];
Нежелательные элементы массива также могут быть отброшены с помощью дополнительной запятой:
// второй элемент не нужен
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert( title ); // Consul
В примере выше второй элемент массива пропускается, а третий присваивается переменной title
, оставшиеся элементы массива также пропускаются (так как для них нет переменных).
…На самом деле мы можем использовать любой перебираемый объект, не только массивы:
let [a, b, c] = "abc";
let [one, two, three] = new Set([1, 2, 3]);
Мы можем использовать что угодно «присваивающее» с левой стороны.
Например, можно присвоить свойству объекта:
let user = {};
[user.name, user.surname] = "Ilya Kantor".split(' ');
alert(user.name); // Ilya
alert(user.surname); // Kantor
В предыдущей главе мы видели метод Object.entries(obj).
Мы можем использовать его с деструктуризацией для цикличного перебора ключей и значений объекта:
let user = {
name: "John",
age: 30
};
// цикл по ключам и значениям
for (let [key, value] of Object.entries(user)) {
alert(`${key}:${value}`); // name:John, затем age:30
}
…то же самое для map:
let user = new Map();
user.set("name", "John");
user.set("age", "30");
// Map перебирает как пары [ключ, значение], что очень удобно для деструктурирования
for (let [key, value] of user) {
alert(`${key}:${value}`); // name:John, затем age:30
}
Существует хорошо известный трюк для обмена значений двух переменных с использованием деструктурирующего присваивания:
let guest = "Jane";
let admin = "Pete";
// Давайте поменяем местами значения: сделаем guest = "Pete", а admin = "Jane"
[guest, admin] = [admin, guest];
alert(`${guest} ${admin}`); // Pete Jane (успешно заменено!)
Здесь мы создаём временный массив из двух переменных и немедленно деструктурируем его в порядке замены.
Таким образом, мы можем поменять местами даже более двух переменных.
Остаточные параметры «…»
Обычно, если массив длиннее, чем список слева, «лишние» элементы опускаются.
Например, здесь берутся только первые два элемента, а остальные просто игнорируются:
let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(name1); // Julius
alert(name2); // Caesar
// Дальнейшие элементы нигде не присваиваются
Если мы хотим не просто получить первые значения, но и собрать все остальные, то мы можем добавить ещё один параметр, который получает остальные значения, используя оператор «остаточные параметры» – троеточие ("..."
):
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// rest это массив элементов, начиная с 3-го
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
Переменная rest
является массивом из оставшихся элементов.
Вместо rest
можно использовать любое другое название переменной, просто убедитесь, что перед переменной есть три точки и она стоит на последнем месте в деструктурирующем присваивании.
let [name1, name2, ...titles] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// теперь titles = ["Consul", "of the Roman Republic"]
Значения по умолчанию
Если в массиве меньше значений, чем в присваивании, то ошибки не будет. Отсутствующие значения считаются неопределёнными:
let [firstName, surname] = [];
alert(firstName); // undefined
alert(surname); // undefined
Если мы хотим, чтобы значение «по умолчанию» заменило отсутствующее, мы можем указать его с помощью =
:
// значения по умолчанию
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
alert(name); // Julius (из массива)
alert(surname); // Anonymous (значение по умолчанию)
Значения по умолчанию могут быть гораздо более сложными выражениями или даже функциями. Они выполняются, только если значения отсутствуют.
Например, здесь мы используем функцию prompt
для указания двух значений по умолчанию.
// prompt запустится только для surname
let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"];
alert(name); // Julius (из массива)
alert(surname); // результат prompt
Обратите внимание, prompt
будет запущен только для пропущенного значения (surname
).
Деструктуризация объекта
Деструктурирующее присваивание также работает с объектами.
Синтаксис:
let {var1, var2} = {var1:…, var2:…}
У нас есть существующий объект с правой стороны, который мы хотим разделить на переменные. Левая сторона содержит «шаблон» для соответствующих свойств. В простом случае это список названий переменных в {...}
.
Например:
let options = {
title: "Menu",
width: 100,
height: 200
};
let {title, width, height} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
Свойства options.title
, options.width
и options.height
присваиваются соответствующим переменным.
Порядок не имеет значения. Вот так – тоже работает:
// изменён порядок в let {...}
let {height, width, title} = { title: "Menu", height: 200, width: 100 }
Шаблон с левой стороны может быть более сложным и определять соответствие между свойствами и переменными.
Если мы хотим присвоить свойство объекта переменной с другим названием, например, свойство options.width
присвоить переменной w
, то мы можем использовать двоеточие:
let options = {
title: "Menu",
width: 100,
height: 200
};
// { sourceProperty: targetVariable }
let {width: w, height: h, title} = options;
// width -> w
// height -> h
// title -> title
alert(title); // Menu
alert(w); // 100
alert(h); // 200
Двоеточие показывает «что : куда идёт». В примере выше свойство width
сохраняется в переменную w
, свойство height
сохраняется в h
, а title
присваивается одноимённой переменной.
Для потенциально отсутствующих свойств мы можем установить значения по умолчанию, используя "="
, как здесь:
let options = {
title: "Menu"
};
let {width = 100, height = 200, title} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
Как и в случае с массивами, значениями по умолчанию могут быть любые выражения или даже функции. Они выполнятся, если значения отсутствуют.
В коде ниже prompt
запросит width
, но не title
:
let options = {
title: "Menu"
};
let {width = prompt("width?"), title = prompt("title?")} = options;
alert(title); // Menu
alert(width); // (результат prompt)
Мы также можем совмещать :
и =
:
let options = {
title: "Menu"
};
let {width: w = 100, height: h = 200, title} = options;
alert(title); // Menu
alert(w); // 100
alert(h); // 200
Если у нас есть большой объект с множеством свойств, можно взять только то, что нужно:
let options = {
title: "Menu",
width: 100,
height: 200
};
// взять только title, игнорировать остальное
let { title } = options;
alert(title); // Menu
Остаток объекта «…»
Что если в объекте больше свойств, чем у нас переменных? Можем ли мы взять необходимые нам, а остальные присвоить куда-нибудь?
Можно использовать троеточие, как и для массивов. В некоторых старых браузерах (IE) это не поддерживается, используйте Babel для полифила.
Выглядит так:
let options = {
title: "Menu",
height: 200,
width: 100
};
// title = свойство с именем title
// rest = объект с остальными свойствами
let {title, ...rest} = options;
// сейчас title="Menu", rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100
let
В примерах выше переменные были объявлены в присваивании: let {…} = {…}
. Конечно, мы могли бы использовать существующие переменные и не указывать let
, но тут есть подвох.
Вот так не будет работать:
let title, width, height;
// ошибка будет в этой строке
{title, width, height} = {title: "Menu", width: 200, height: 100};
Проблема в том, что JavaScript обрабатывает {...}
в основном потоке кода (не внутри другого выражения) как блок кода. Такие блоки кода могут быть использованы для группировки операторов, например:
{
// блок кода
let message = "Hello";
// ...
alert( message );
}
Так что здесь JavaScript считает, что видит блок кода, отсюда и ошибка. На самом-то деле у нас деструктуризация.
Чтобы показать JavaScript, что это не блок кода, мы можем заключить выражение в скобки (...)
:
let title, width, height;
// сейчас всё работает
({title, width, height} = {title: "Menu", width: 200, height: 100});
alert( title ); // Menu
Вложенная деструктуризация
Если объект или массив содержит другие вложенные объекты или массивы, то мы можем использовать более сложные шаблоны с левой стороны, чтобы извлечь более глубокие свойства.
В приведённом ниже коде options
хранит другой объект в свойстве size
и массив в свойстве items
. Шаблон в левой части присваивания имеет такую же структуру, чтобы извлечь данные из них:
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
// деструктуризация разбита на несколько строк для ясности
let {
size: { // положим size сюда
width,
height
},
items: [item1, item2], // добавим элементы к items
title = "Menu" // отсутствует в объекте (используется значение по умолчанию)
} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
Весь объект options
, кроме свойства extra
, которое в левой части отсутствует, присваивается в соответствующие переменные:
В итоге у нас есть width
, height
, item1
, item2
и title
со значением по умолчанию.
Заметим, что переменные для size
и items
отсутствуют, так как мы взяли сразу их содержимое.
Умные параметры функций
Есть ситуации, когда функция имеет много параметров, большинство из которых не обязательны. Это особенно верно для пользовательских интерфейсов. Представьте себе функцию, которая создаёт меню. Она может иметь ширину, высоту, заголовок, список элементов и так далее.
Вот так – плохой способ писать подобные функции:
function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
// ...
}
В реальной жизни проблема заключается в том, как запомнить порядок всех аргументов. Обычно IDE пытаются помочь нам, особенно если код хорошо документирован, но всё же… Другая проблема заключается в том, как вызвать функцию, когда большинство параметров передавать не надо, и значения по умолчанию вполне подходят.
Разве что вот так?
// undefined там, где подходят значения по умолчанию
showMenu("My Menu", undefined, undefined, ["Item1", "Item2"])
Это выглядит ужасно. И становится нечитаемым, когда мы имеем дело с большим количеством параметров.
На помощь приходит деструктуризация!
Мы можем передать параметры как объект, и функция немедленно деструктурирует его в переменные:
// мы передаём объект в функцию
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
// ...и она немедленно извлекает свойства в переменные
function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
// title, items – взято из options,
// width, height – используются значения по умолчанию
alert( `${title} ${width} ${height}` ); // My Menu 200 100
alert( items ); // Item1, Item2
}
showMenu(options);
Мы также можем использовать более сложное деструктурирование с вложенными объектами и двоеточием:
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
function showMenu({
title = "Untitled",
width: w = 100, // width присваиваем в w
height: h = 200, // height присваиваем в h
items: [item1, item2] // первый элемент items присваивается в item1, второй в item2
}) {
alert( `${title} ${w} ${h}` ); // My Menu 100 200
alert( item1 ); // Item1
alert( item2 ); // Item2
}
showMenu(options);
Полный синтаксис – такой же, как для деструктурирующего присваивания:
function({
incomingProperty: varName = defaultValue
...
})
Тогда для объекта с параметрами будет создана переменная varName
для свойства с именем incomingProperty
по умолчанию равная defaultValue
.
Пожалуйста, обратите внимание, что такое деструктурирование подразумевает, что в showMenu()
будет обязательно передан аргумент. Если нам нужны все значения по умолчанию, то нам следует передать пустой объект:
showMenu({}); // ок, все значения - по умолчанию
showMenu(); // так была бы ошибка
Мы можем исправить это, сделав {}
значением по умолчанию для всего объекта параметров:
function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
alert( `${title} ${width} ${height}` );
}
showMenu(); // Menu 100 200
В приведённом выше коде весь объект аргументов по умолчанию равен {}
, поэтому всегда есть что-то, что можно деструктурировать.
Итого
-
Деструктуризация позволяет разбивать объект или массив на переменные при присвоении.
-
Полный синтаксис для объекта:
let {prop : varName = defaultValue, ...rest} = object
Cвойство
prop
объектаobject
здесь должно быть присвоено переменнойvarName
. Если в объекте отсутствует такое свойство, переменнойvarName
присваивается значение по умолчанию.Свойства, которые не были упомянуты, копируются в объект
rest
. -
Полный синтаксис для массива:
let [item1 = defaultValue, item2, ...rest] = array
Первый элемент отправляется в
item1
; второй отправляется вitem2
, все остальные элементы попадают в массивrest
. -
Можно извлекать данные из вложенных объектов и массивов, для этого левая сторона должна иметь ту же структуру, что и правая.