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), які додають ще й обробку помилок і скасування.
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'));.
Усі обходять масив і викликають функцію для кожного елемента:
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(...).
Об'єкти й масиви в змінних зберігаються за посиланням. 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 змінений стан зазвичай будують поверхневими копіями лише тих гілок, що змінилися, - це й дешевше, і дозволяє швидко порівнювати посилання.
Обидва зберігають пари «ключ - значення», але 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.
Спершу почитати
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.