Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

JavaScript: масиви й об'єкти

20 питань · ~20 хв · Версія v3.0

Увійдіть, щоб продовжити

Методи масивів, що змінюють і не змінюють дані, копіювання, Map і Set, ітератори, порядок ключів і JSON - питання всіх рівнів, від junior до senior.

За спробу
20
У пулі
95
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

19 питань

forEach викликає колбек і ігнорує те, що він повертає. async-колбек повертає Promise, який ніхто не чекає: цикл завершується одразу, помилки губляться.

items.forEach(async (item) => { await save(item); });
console.log('готово'); // виведеться ДО того, як щось збережеться

Правильні варіанти залежно від задачі:

// Послідовно, по одному
for (const item of items) {
  await save(item);
}

// Усі паралельно
await Promise.all(items.map((item) => save(item)));

Проблема «всіх паралельно»: тисяча елементів - тисяча одночасних запитів. API почне відповідати 429, браузер обмежить кількість з'єднань, база отримає піку навантаження.

Обмежена паралельність - не більше N одночасно:

async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;

  async function worker() {
    while (next < items.length) {
      const index = next++;
      results[index] = await fn(items[index], index);
    }
  }

  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}

const saved = await mapLimit(items, 5, save);

Тут next++ безпечний, бо JavaScript однопотоковий: між читанням і збільшенням лічильника інший «воркер» виконатися не може.

На практиці часто беруть готові бібліотеки (p-limit, p-map), які додають ще й обробку помилок і скасування.

Докладніше в документації: Array.prototype.forEach

for await...of перебирає послідовність значень, що надходять асинхронно: кожен крок циклу чекає на наступне значення.

Асинхронний генератор - найпростіший спосіб таку послідовність створити. Наприклад, обхід API з пагінацією:

async function* fetchAllPages(url) {
  let next = url;
  while (next) {
    const response = await fetch(next);
    const page = await response.json();
    yield* page.data;          // віддати елементи сторінки по одному
    next = page.links.next;    // колекція API Resource у Laravel кладе сюди URL наступної сторінки
  }
}

for await (const user of fetchAllPages('/api/users')) {
  console.log(user.name);
  if (user.id === 42) break;   // решта сторінок навіть не буде завантажена
}

Переваги перед «завантажити все в масив»:

  • лінивість - наступна сторінка запитується лише тоді, коли попередню оброблено;
  • пам'ять - не треба тримати весь набір одночасно;
  • break чи return з циклу зупиняє генератор, і зайві запити не робляться.

Де ще зустрічається:

  • потоки даних: ReadableStream у Node.js, Chrome і Firefox можна перебирати через for await (Safari поки ні - там читають через getReader()):
const response = await fetch('/export.csv');
const decoder = new TextDecoder();
for await (const chunk of response.body) {
  process(decoder.decode(chunk, { stream: true }));
}
  • читання файлів построково в Node.js (readline), події, черги повідомлень.

for await над масивом Promise теж працює, але виконує їх у порядку масиву, а не в порядку завершення, і помилки обробляє незручно: for await (const r of [p1, p2, p3]). Якщо Promise вже запущені, краще Promise.all чи Promise.allSettled.

Array.fromAsync() (ES2024) збирає асинхронну послідовність у масив: const users = await Array.fromAsync(fetchAllPages('/api/users'));.

Докладніше в документації: for await...of

Усі обходять масив і викликають функцію для кожного елемента:

  • map - новий масив тієї ж довжини з перетвореними елементами.
  • filter - новий масив лише з тими елементами, для яких функція повернула істинне значення.
  • reduce - згортає масив в одне значення: суму, об'єкт, інший масив.
  • forEach - нічого не повертає (undefined), лише виконує дію для кожного елемента.
const orders = [{ total: 100, paid: true }, { total: 250, paid: false }, { total: 40, paid: true }];

orders.map((o) => o.total);                       // [100, 250, 40]
orders.filter((o) => o.paid);                     // два оплачені
orders.reduce((sum, o) => sum + o.total, 0);      // 390
orders.forEach((o) => console.log(o.total));      // лише побічна дія

Типові помилки:

  • map заради побічної дії, без використання результату. Для цього є forEach чи for...of.
  • reduce без початкового значення на порожньому масиві кидає TypeError.
  • Перервати forEach неможливо (break не працює). Якщо треба зупинитися, - for...of, some() чи find().

Усі чотири не змінюють початковий масив (хіба що сам колбек зробить це навмисно), тож їх зручно з'єднувати в ланцюжок: orders.filter(...).map(...).

Докладніше в документації: Array.prototype.map

Об'єкти й масиви в змінних зберігаються за посиланням. const b = a - це не копія, а друге ім'я для того самого об'єкта.

Поверхнева копія копіює лише перший рівень. Вкладені об'єкти лишаються спільними:

const user = { name: 'Оля', address: { city: 'Київ' } };
const copy = { ...user };       // або Object.assign({}, user)

copy.name = 'Іра';              // user.name не змінився
copy.address.city = 'Львів';    // user.address.city теж став 'Львів'!

Глибока копія копіює всі рівні:

const deep = structuredClone(user);
deep.address.city = 'Одеса';    // user не зачеплено

structuredClone (вбудований у браузери й Node.js 17+) коректно копіює Date, Map, Set, вкладені масиви й навіть циклічні посилання. Не копіює функції, DOM-вузли й прототипи класів: екземпляр класу стане звичайним об'єктом.

Старий прийом JSON.parse(JSON.stringify(obj)) має купу обмежень: Date перетворюється на рядок, undefined, функції й Map зникають, циклічні посилання дають помилку.

На практиці глибока копія потрібна рідше, ніж здається: у React, Vue чи Redux змінений стан зазвичай будують поверхневими копіями лише тих гілок, що змінилися, - це й дешевше, і дозволяє швидко порівнювати посилання.

Докладніше в документації: structuredClone()

Обидва зберігають пари «ключ - значення», але Map створений саме як словник, а об'єкт - як запис з полями.

Відмінності:

  • Ключі. У об'єкта ключі - лише рядки й символи (число 1 стане рядком '1'). У Map ключем може бути будь-що: об'єкт, функція, NaN.
  • Розмір. map.size - одразу; для об'єкта - Object.keys(obj).length.
  • Порядок. Map зберігає порядок вставки. У об'єкта порядок теж визначений, але з винятком: ключі, схожі на цілі числа, йдуть першими за зростанням.
  • Немає успадкованих ключів. Звичайний об'єкт має прототип, і ключі на кшталт constructor чи __proto__ можуть дати сюрпризи. Map порожній по-справжньому.
  • Швидкодія на частих додаваннях і видаленнях у Map зазвичай краща.
  • JSON. JSON.stringify(map) дає {} - його треба перетворювати: Object.fromEntries(map).
const visits = new Map();
const button = document.querySelector('#buy');

visits.set(button, 1);                       // ключ - DOM-елемент
visits.set(button, visits.get(button) + 1);
visits.has(button);                          // true
for (const [element, count] of visits) { /* ... */ }

Коли що: об'єкт - для структур з відомими полями ({ id, name, email }), JSON і конфігурацій. Map - для словників з динамічними ключами, частих змін і ключів-не-рядків. Для набору унікальних значень - Set.

Докладніше в документації: Map

Прочитати - ще не значить знати

20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.