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

Middle: питання на співбесіді з теми «Масиви й об'єкти»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

Обидва зберігають пари «ключ - значення», але 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

Змінюють масив на місці (мутують): push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin.

Повертають новий, не чіпаючи початковий: map, filter, slice, concat, flat, flatMap, а також нові (ES2023) toSorted, toReversed, toSpliced і with.

const scores = [30, 10, 20];

const sorted = scores.sort((a, b) => a - b);
// scores теж став [10, 20, 30]: sort змінює масив і повертає його ж

const safe = [30, 10, 20];
const sortedCopy = safe.toSorted((a, b) => a - b); // safe лишився [30, 10, 20]
const replaced = safe.with(0, 99);                 // [99, 10, 20], safe не змінився

Чому це важливо:

  • У React, Vue, Redux мутація стану на місці ламає виявлення змін: посилання на масив те саме, і компонент може не оновитися. Тому toSorted() чи [...items].sort() замість items.sort().
  • Масив з аргументу функції належить коду, який його передав. Відсортувати його на місці - змінити чужі дані непомітно.

Ще пастка sort: без функції порівняння елементи порівнюються як рядки, тож [10, 9, 1].sort() дає [1, 10, 9]. Для чисел потрібен (a, b) => a - b, для українського тексту - (a, b) => a.localeCompare(b, 'uk').

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

Групування масиву за ознакою довго писали вручну через reduce. З ES2024 для цього є вбудовані функції.

Object.groupBy(items, callback) - результат у звичайному об'єкті, ключі - те, що повернув колбек:

const orders = [
  { id: 1, status: 'paid', total: 100 },
  { id: 2, status: 'new', total: 50 },
  { id: 3, status: 'paid', total: 70 },
];

const byStatus = Object.groupBy(orders, (order) => order.status);
// { paid: [{ id: 1, ... }, { id: 3, ... }], new: [{ id: 2, ... }] }

const bySize = Object.groupBy(orders, ({ total }) => (total >= 70 ? 'large' : 'small'));

Map.groupBy(items, callback) - результат у Map. Потрібен, коли ключ - не рядок: об'єкт, дата, число, яке не має перетворюватися на рядок:

const byCustomer = Map.groupBy(orders, (order) => customersById.get(order.customerId));
byCustomer.get(someCustomer);   // замовлення конкретного об'єкта-клієнта

Особливості:

  • це статичні функції, а не методи масиву: Object.groupBy(arr, fn), а не arr.groupBy(fn). Метод масиву планували, але він конфліктував зі старими бібліотеками, що розширювали Array.prototype;
  • об'єкт від Object.groupBy має прототип null: у нього немає hasOwnProperty, toString тощо. result.hasOwnProperty('paid') кине помилку - перевіряйте через Object.hasOwn(result, 'paid') чи 'paid' in result;
  • групи відсутніх значень просто не створюються - порожніх масивів для «ненайдених» статусів не буде;
  • елементи в групах ідуть у порядку вихідного масиву.

Як це робили раніше (і досі доводиться в старих оточеннях):

const byStatus = orders.reduce((groups, order) => {
  (groups[order.status] ??= []).push(order);
  return groups;
}, {});

Порівняння з Laravel: це аналог collect($orders)->groupBy('status'). Агрегати по групах далі рахують звичайними методами: Object.entries(byStatus).map(([status, list]) => [status, list.length]).

Підтримка: усі сучасні браузери з 2024 року і Node.js 21+. Для старших середовищ - поліфіл (core-js) чи reduce.

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

Set - колекція унікальних значень. Повторне додавання того самого значення нічого не змінює.

const tags = new Set(['php', 'laravel', 'php']);
tags.size;            // 2
tags.add('vue');
tags.has('laravel');  // true, за O(1)
tags.delete('php');
[...tags];            // ['laravel', 'vue'] - порядок додавання зберігається

Найчастіший прийом - прибрати дублікати:

const unique = [...new Set(ids)];

Унікальність визначається як === (з винятком: NaN дорівнює NaN). Тому об'єкти порівнюються за посиланням - два різні об'єкти { id: 1 } обидва потраплять у множину. Для унікальності об'єктів за полем використовують Map з ключем-полем.

Методи множин (ES2025) - нарешті вбудовані операції теорії множин. Кожен повертає новий Set:

const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);

a.union(b);                // {1, 2, 3, 4}
a.intersection(b);         // {2, 3}
a.difference(b);           // {1}      - є в a, немає в b
a.symmetricDifference(b);  // {1, 4}   - є лише в одній з множин

new Set([2]).isSubsetOf(a);   // true
a.isSupersetOf(new Set([1])); // true
a.isDisjointFrom(new Set([9])); // true - спільних елементів немає

Практичні застосування:

  • синхронізація зв'язків: які теги додати й які прибрати при збереженні форми:
const toAttach = selected.difference(current);
const toDetach = current.difference(selected);

Це те, що в Laravel робить sync() для зв'язків «багато-до-багатьох».

  • права: required.isSubsetOf(userPermissions) - чи має користувач усі потрібні права;
  • швидкі перевірки належності у фільтрах замість array.includes у циклі.

Аргументом нових методів може бути не лише Set, а будь-який об'єкт із size, has() і keys() - наприклад, Map (порівнюються ключі). Масив напряму не підходить: a.union([4]) кине помилку, потрібно a.union(new Set([4])).

Підтримка: усі сучасні браузери з 2024 року, Node.js 22+.

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