Деструктурирующее присваивание

В JavaScript встроен достаточно гибкий механизм присваивания значений переменным по данным массивов и объектов. Например, у нас есть массив марок машин:

let cars = ["yaguar", "porshe", "mercedes"];

и мы хотим каждый элемент присвоить отдельной переменной. Это можно сделать так:

let [car1, car2, car3] = cars;

Здесь у нас формируются три переменные с соответствующими значениями:

console.log(car1, car2, car3);

Это называется деструктурирующим присваиванием. Если, например, нам нужно пропустить второе значение, то это можно реализовать так:

let [car1, , car2] = ["yaguar", "porshe", "mercedes"];

Как видите, механизм очень гибкий. То есть, мы можем элементы массива с легкостью присваивать нужным переменным. Один такой практический пример выглядит следующим образом:

let [firstName, middleName, lastName] = "Иван Иванович Иванов".split(" ");
console.log(firstName, middleName, lastName);

Мы здесь разбиваем строку на составляющие имени и сразу присваиваем их переменным. Быстро, просто и очень удобно.

Если у нас достаточно длинный массив и мы хотим выбрать из него, например, только первые два элемента, а остальные поместить в массив меньшей длины, то это делается так:

let [fr1, fr2, ...last] = ["Груша", "Слива", "Яблоко", "Персик", "Виноград"];
console.log(fr1, fr2, last);

Здесь fr1 = "Груша", fr2 = "Слива", а last – массив из оставшихся элементов. То есть, чтобы нам «собрать» остаточные значения, нужно поставить троеточие и указать имя массива, в который мы их сложим.

Все это будет работать, если число переменных превышает число элементов массива:

let [fr1, fr2, fr3, fr4] = ["Груша", "Слива"];
console.log(fr1, fr2, fr3, fr4);

И, чтобы значения не были undefined, можно прописать значения по умолчанию:

let [fr1, fr2, fr3 = "Яблоко", fr4 = "Персик"] = ["Груша", "Слива"];

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

let [name = prompt('name?'), lastname = prompt('lastname?')] = ["Иван"];
console.log(name, lastname);

Здесь prompt запустится только для lastname, так как для него будет использовано значение по умолчанию. Первой переменной просто присваивается значение из массива и prompt будет пропущен.

Деструктурирующее присваивание работает с любым перебираемым объектом, например, с обычной строкой:

let [a1, a2, a3, a4] = "Иван"; 
console.log(a1, a2, a3, a4);

Здесь каждой переменной присваивается отдельный символ строки. Или, с набором:

let [one, two, three] = new Set([1, 2, 3]);
console.log(one, two, three);

Очень удобно использовать деструктурирующее присваивание для перебора свойств объекта:

for(let [key, value] of Object.entries(car))
    console.log(`car[${key}] = ${value}`);

Здесь метод entries преобразует объект в двумерный массив по формату «ключ-значение».

Для перебора свойств коллекции Map можно использовать такой же подход:

let car = new Map();
car.set("model", "toyota").
    set("color", 0xaf).
    set("price", 1000);
 
for(let [key, value] of car)
    console.log(`car[${key}] = ${value}`);

Деструктурирующее присваивание можно выполнять и с обычными объектами. Например, имеется объект

let args = {
    width: 100,
    height: 200,
    tag: "div",
    class: "div-id"
};

И далее, указываем свойства, которые хотим выбрать из этого объекта:

let {width, tag, height} = args;
console.log(width, tag, height);

Причем, эти свойства можно записывать не по порядку, механизм деструктурирующего присваивания отработает корректно. В результате будут созданы отдельные переменные width, tag, height с соответствующими значениями.

Но, что если мы хотим именовать переменные не так как именованы свойства? Для этого используется такой синтаксис:

let {width: w, tag, height: h} = args;
console.log(w, tag, h);

Теперь, вместо переменной width будет создана переменная w и то же самое для переменной height. Переменная tag остается без изменения.

Здесь также как и в случае с массивами можно использовать аргументы по умолчанию:

let {width: w = 0, tag, height: h = 0} = {tag: "span"};
console.log(w, tag, h);

И, если в объекте не будет таких свойств, то переменным будут присвоены эти значения, иначе они были бы равны undefined.

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

let {width, height, ...last} = args;
console.log(width, height, last);

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

let width, height;
{width, height} = args;

Здесь JavaScript-машина выдаст нам сообщение об ошибке. Дело в том, что вот эти фигурные скобки у нее будут восприниматься не как элемент деструктурирующего присваивания, а как блок кода:

{
    width, height
}
 = args;

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

let width, height;
({width, height} = args);
console.log(width, height);

И тогда все заработает.

Наконец, если объект или массив имеют более сложную структуру, например, такую:

let args = {
    tag: "div",
    class: "div-class",
    size: {width: 100, height: 200}
};

То для деструктурирующего присваивания можно использовать более сложные шаблоны:

let { 
         size: {width, height},
         class: cl
    } = args;
 
console.log(width, height, cl);

Мы здесь говорим, что переменные width и height должны браться из вложенного объекта size, а свойство class должно быть скопировано в переменную cl. В итоге, получаем три переменные width, height, cl с соответствующими значениями.

Механизм деструктурирующего присваивания часто используют для передачи параметров аргументам функций. Есть ситуации, когда функция имеет много параметров, большинство из которых не обязательны. Например, при создании пользовательских интерфейсов. Представьте себе функцию, которая создаёт меню. Ей передаются значения ширины, высоты, заголовок, список элементов и так далее:

function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
    // ...
}

Но такой функцией пользоваться неудобно – нужно наизусть помнить порядок аргументов. Это неудобно. Исправить ситуацию можно с помощью деструктурирующего присваивания, определяя аргументы как результат развертывания некоторого объекта:

function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
    console.log(title);
    console.log(width, height);
    console.log(items);
}

А сами аргументы описать с помощью объекта:

let args = {
    title: "Заголовок меню",
    items: ["Пункт 1", "Пункт 2", "Пункт 3"]
};

Теперь можно вызвать функцию с этими параметрами:

showMenu(args);

Смотрите, как теперь все выглядит изящно и красиво. Если какие-то свойства не указываем, то им присваиваются значения по умолчанию. Например, здесь width = 200, height = 100. А что, если мы не будем передавать функции никаких аргументов и вызовем ее вот так:

showMenu();

У нас возникнет ошибка, так как предполагается деструктирующее присваивание, а никакого объекта (даже пустого) передано не было. Ситуацию можно было бы исправить вот так:

showMenu({});

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

function showMenu({title = "Untitled", width = 200, height = 100, items = []} = {}) {
    //...
}

И теперь ее можно свободно вызывать без аргументов:

showMenu();

Вот что из себя представляет механизм деструктурирующего присваивания и как он может использоваться на практике.

Видео по теме