Двома найбільш вживаними структурами даних у JavaScript є Object
та Array
.
- Об’єкти дозволяють нам створити єдину сутність, яка зберігатиме дані за ключем.
- Масиви дозволяють нам зібрати елементи даних у впорядкований список.
Однак, коли ми передаємо їх у функцію, нам може знадобитися не все. Функції можуть знадобитися лише певні елементи або властивості.
Деструктуроване присвоєння – це спеціальний синтаксис, що дозволяє нам “розпаковувати” масиви чи об’єкти в купу змінних, оскільки іноді це зручніше.
Деструктурування також чудово працює зі складними функціями, які мають багато параметрів, типових значень тощо. Незабаром ми це побачимо.
Деструктурування масиву
Ось приклад того, як масив деструктурується на змінні:
// у нас є масив з іменем та прізвищем
let arr = ["Іван", "Петренко"]
// деструктуроване присвоєння
// встановлює firstName = arr[0]
// та surname = arr[1]
let [firstName, surname] = arr;
alert(firstName); // Іван
alert(surname); // Петренко
Тепер ми можемо працювати зі змінними замість елементів масиву.
Це чудово виглядає в поєднанні зі split
або іншими методами повернення масиву:
let [firstName, surname] = "Іван Петренко".split(' ');
alert(firstName); // Іван
alert(surname); // Петренко
Як бачите, синтаксис простий. Хоча є кілька особливих деталей. Давайте розглянемо більше прикладів, щоб краще це зрозуміти.
Це називається “деструктуроване присвоєння”, оскільки воно “деструктурує” шляхом копіювання елементів у змінні. Однак, сам масив не змінюється.
Це просто коротший спосіб написати:
// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];
Небажані елементи масиву також можна викинути за допомогою додаткової коми:
// другий елемент не потрібен
let [firstName, , title] = ["Юлій", "Цезар", "Консул", "Римської республіки"];
alert( title ); // Консул
У наведеному вище коді другий елемент масиву пропускається, третій присвоюється title
, а решта елементів масиву також пропускаються (оскільки для них немає змінних).
…Насправді, ми можемо використовувати його з будь-якими даними, які перебираються, а не тільки з масивами:
let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);
Це працює, тому що внутрішньо деструктуроване присвоювання працює шляхом ітерації над правильним значенням. Це своєрідний синтаксичний цукор для виклику for..of
над значенням праворуч від =
і присвоювання значень.
Ми можемо використовувати будь-які “призначення” з лівого боку.
Наприклад, властивість об’єкта:
let user = {};
[user.name, user.surname] = "Іван Петренко".split(' ');
alert(user.name); // Іван
alert(user.surname); // Петренко
У попередньому розділі, ми бачили метод Object.entries(obj).
Ми можемо використовувати його з деструктуруванням для циклічного перебору ключів-та-значень об’єкта:
let user = {
name: "Іван",
age: 30
};
// перебрати циклом ключі-та-значення
for (let [key, value] of Object.entries(user)) {
alert(`${key}:${value}`); // name:Іван, потім age:30
}
Подібний код для Map
простіший, оскільки він є структурою даних, яка перебирається:
let user = new Map();
user.set("name", "Іван");
user.set("age", "30");
// Map ітерує як пари [key, value], що дуже зручно для деструктурування
for (let [key, value] of user) {
alert(`${key}:${value}`); // name:Іван, then age:30
}
Існує відомий трюк для обміну значень двох змінних за допомогою деструктурованого присвоєння:
let guest = "Джейн";
let admin = "Пітер";
// Давайте обміняємо значення: зробімо guest=Пітер, admin=Джейн
[guest, admin] = [admin, guest];
alert(`${guest} ${admin}`); // Пітер Джейн (успішно обмінялися!)
Тут ми створюємо тимчасовий масив з двох змінних і негайно деструктуруємо його в порядку обміну.
Таким методом ми можемо поміняти місцями більше двох змінних.
Залишкові параметри ‘…’
Зазвичай, якщо масив довший від списку зліва, “зайві” елементи опускаються.
Наприклад, тут береться лише два елементи, а решта просто ігнорується:
let [name1, name2] = ["Юлій", "Цезар", "Консул", "Римської Республіки"];
alert(name1); // Юлій
alert(name2); // Цезар
// Інші пункти ніде не присвоєні
Якщо ми хочемо також зібрати все наступне – ми можемо додати ще один параметр, який отримує “решту”, використовуючи три крапки "..."
:
let [name1, name2, ...rest] = ["Юлій", "Цезар", "Консул", "Римської Республіки"];
// rest -- це масив елементів, починаючи з 3-го
alert(rest[0]); // Консул
alert(rest[1]); // Римської Республіки
alert(rest.length); // 2
Значення rest
– це масив елементів, що залишилися.
Ми можемо використовувати будь-яке інше ім’я змінної замість rest
, просто переконайтеся, що воно має три крапки перед ним і йде останнім у присвоєнні деструктурування.
let [name1, name2, ...titles] = ["Юлій", "Цезар", "Консул", "Римської Республіки"];
// тепер titles = ["Консул", "Римської Республіки"]
Типові значення
Якщо масив коротший за список змінних зліва, помилок не буде. Відсутні значення вважаються невизначеними:
let [firstName, surname] = [];
alert(firstName); // undefined
alert(surname); // undefined
Якщо ми хочемо, щоб “типове” значення замінило б відсутнє, ми можемо надати його за допомогою =
:
// типове значення
let [name = "Гість", surname = "Анонім"] = ["Юлій"];
alert(name); // Юлій (з масиву)
alert(surname); // Анонім (використовується типове значення)
Початкові значення можуть бути більш складними виразами або навіть викликами функцій. Вони визначаються, лише якщо значення не надано.
Наприклад, тут ми використовуємо функцію prompt
для двох типових значень:
// запускає prompt тільки для surname
let [name = prompt("Ім'я?"), surname = prompt('Прізвище?')] = ["Юлій"];
alert(name); // Юлій (визначено з масиву)
alert(surname); // значення, отримане з prompt
Зверніть увагу: prompt
буде спрацьовувати лише для відсутнього значення (surname
).
Деструктурування об’єктів
Деструктуроване присвоєння також працює з об’єктами.
Основний синтаксис такий:
let {var1, var2} = {var1:…, var2:…}
Ми повинні мати існуючий об’єкт праворуч, який ми хочемо розділити на змінні. Ліва частина містить об’єктоподібний “шаблон” для відповідних властивостей. У найпростішому випадку це список імен змінних у {...}
.
Наприклад:
let options = {
title: "Меню",
width: 100,
height: 200
};
let {title, width, height} = options;
alert(title); // Меню
alert(width); // 100
alert(height); // 200
Властивості options.title
, options.width
та options.height
призначені відповідним змінним.
Порядок не має значення. Це теж працює:
// змінили порядок у let {...}
let {height, width, title} = { title: "Меню", height: 200, width: 100 }
Шаблон з лівого боку може бути більш складним і визначати зіставлення властивостей та змінних.
Якщо ми хочемо присвоїти властивість змінній з іншим іменем, наприклад, зробити так, щоб options.width
переходив до змінної з назвою w
, то ми можемо встановити ім’я змінної за допомогою двокрапки:
let options = {
title: "Меню",
width: 100,
height: 200
};
// { sourceProperty: targetVariable }
let {width: w, height: h, title} = options;
// width -> w
// height -> h
// title -> title
alert(title); // Меню
alert(w); // 100
alert(h); // 200
Двокрапка показує “що: куди йде”. У наведеному вище прикладі властивість width
переходить у w
, властивість height
переходить у h
, а title
присвоюється тому самому імені.
Для потенційно відсутніх властивостей ми можемо встановити типові значення за допомогою "="
, наприклад:
let options = {
title: "Меню"
};
let {width = 100, height = 200, title} = options;
alert(title); // Меню
alert(width); // 100
alert(height); // 200
Так само, як і з масивами або параметрами функцій, типові значення можуть бути будь-якими виразами або навіть викликами функцій. Вони будуть оцінені, якщо значення не надано.
У коді нижче prompt
запитує width
, але не title
:
let options = {
title: "Меню"
};
let {width = prompt("Ширина?"), title = prompt("Заголовок?")} = options;
alert(title); // Меню
alert(width); // (будь-який результат з prompt)
Ми також можемо поєднати двокрапку та рівність:
let options = {
title: "Меню"
};
let {width: w = 100, height: h = 200, title} = options;
alert(title); // Меню
alert(w); // 100
alert(h); // 200
Якщо у нас є складний об’єкт з багатьма властивостями, ми можемо витягти лише те, що нам потрібно:
let options = {
title: "Меню",
width: 100,
height: 200
};
// вибирає тільки title як змінну
let { title } = options;
alert(title); // Меню
Залишок об’єкту “…”
Що робити, якщо об’єкт має більше властивостей, ніж ми маємо змінних? Чи можемо ми взяти частину, а потім призначити кудись «залишок»?
Ми можемо використовувати шаблон залишкового оператору, так само, як ми робили з масивами. Він не підтримується деякими старішими браузерами (IE, використовуйте Babel для поліфілу), але працює в сучасних.
Це виглядає наступним чином:
let options = {
title: "Меню",
height: 200,
width: 100
};
// title = властивість з назвою title
// rest = об’єкт з залишковими властивостями
let {title, ...rest} = options;
// тепер title="Меню", rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100
let
відсутнійУ наведених вище прикладах змінні були оголошені прямо в присвоєнні: let {…} = {…}
. Звичайно, ми також можемо використовувати існуючі змінні без let
. Але тут може бути підступ.
Це не спрацює:
let title, width, height;
// помилка в цьому рядку
{title, width, height} = {title: "Меню", width: 200, height: 100};
Проблема в тому, що JavaScript розглядає {...}
в основному потоці коду (а не всередині іншого виразу) як блок коду. Такі блоки коду можна використовувати для групування операторів, наприклад:
{
// блок коду
let message = "Привіт";
// ...
alert( message );
}
Отже, тут JavaScript припускає, що у нас є блок коду, тому і виникає помилка. Натомість ми хочемо деструктурування.
Щоб показати JavaScript, що це не блок коду, ми можемо загорнути вираз у дужки (...)
:
let title, width, height;
// тепер працює
({title, width, height} = {title: "Menu", width: 200, height: 100});
alert( title ); // Меню
Вкладене деструктурування
Якщо об’єкт або масив містять інші вкладені об’єкти та масиви, ми можемо використовувати складніші шаблони з лівого боку для вилучення глибших частин.
У наведеному нижче коді options
містить інший об’єкт у властивості size
та масив у властивості items
. Шаблон у лівій частині присвоєння має ту саму структуру для вилучення з них значень:
let options = {
size: {
width: 100,
height: 200
},
items: ["Торт", "Пончик"],
extra: true
};
// деструктурування розподілене на кілька рядків для наочності
let {
size: { // помістимо тут size
width,
height
},
items: [item1, item2], // тут призначимо items
title = "Меню" // немає в об’єкті (використовується типове значення)
} = options;
alert(title); // Меню
alert(width); // 100
alert(height); // 200
alert(item1); // Торт
alert(item2); // Пончик
Усі властивості об’єкта 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 = default, ...rest} = object
Це означає, що властивість
prop
має входити до змінноїvarName
і, якщо такої властивості не існує, слід використовуватитипове
значення.Властивості об’єкта, які не мають зіставлення, копіюються в об’єкт
rest
. -
Повний синтаксис для масиву:
let [item1 = default, item2, ...rest] = array
Перший елемент переходить до
item1
; другий переходить доitem2
, усі інші утворюють масивrest
. -
Можна витягувати дані з вкладених масивів/об’єктів, для цього ліва сторона повинна мати ту ж структуру, що й права.