20 лютого 2024 р.

Деструктуроване присвоєння

Двома найбільш вживаними структурами даних у 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); // Петренко
Цикл з .entries()

У попередньому розділі, ми бачили метод 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.

  • Можна витягувати дані з вкладених масивів/об’єктів, для цього ліва сторона повинна мати ту ж структуру, що й права.

Завдання

важливість: 5

У нас є об’єкт:

let user = {
  name: "Іван",
  years: 30
};

Напишіть деструктуроване присвоєння, яке зчитує:

  • властивість name у змінну name.
  • властивість years у змінну age.
  • властивість isAdmin у змінну isAdmin (false, якщо така властивість відсутня)

Ось приклад значень після вашого присвоєння:

let user = { name: "Іван", years: 30 };

// ваш код зліва:
// ... = user

alert( name ); // Іван
alert( age ); // 30
alert( isAdmin ); // false
let user = {
  name: "Іван",
  years: 30
};

let {name, years: age, isAdmin = false} = user;

alert( name ); // Іван
alert( age ); // 30
alert( isAdmin ); // false
важливість: 5

Є об’єкт salaries:

let salaries = {
  "Іван": 100,
  "Петро": 300,
  "Марія": 250
};

Створіть функцію topSalary(salaries) яка повертає ім’я найбільш високооплачуваної особи.

  • Якщо об’єкт salaries пустий, функція повинна повернути null.
  • Якщо є кілька високооплачуваних осіб, поверніть будь-якого з них.

P.S. Використовуйте Object.entries і деструктурування для перебору пар ключ/значення.

Відкрити пісочницю з тестами.

function topSalary(salaries) {

  let maxSalary = 0;
  let maxName = null;

  for(const [name, salary] of Object.entries(salaries)) {
    if (maxSalary < salary) {
      maxSalary = salary;
      maxName = name;
    }
  }

  return maxName;
}

Відкрити рішення із тестами в пісочниці.

Навчальна карта