Питання на співбесіді з JavaScript
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
114 питань
Три крапки ... означають протилежні речі залежно від місця:
- spread (розгортання) - «розкласти» масив чи об'єкт на окремі елементи. Стоїть там, де використовують значення;
- rest (решта) - «зібрати» кілька елементів в один масив чи об'єкт. Стоїть там, де оголошують змінні чи параметри.
Spread:
// масиви
const all = [...frontend, ...backend]; // об'єднання
const copy = [...items]; // поверхнева копія
Math.max(...prices); // масив як аргументи функції
const unique = [...new Set(tags)]; // будь-який ітерований об'єкт у масив
// об'єкти
const defaults = { perPage: 20, sort: 'name' };
const options = { ...defaults, ...userOptions }; // пізніші властивості перекривають ранніші
const updated = { ...user, name: 'Іра' }; // копія зі зміненим полем
Rest:
function log(level, ...messages) {} // решта аргументів у масив
const [first, ...others] = list; // решта елементів
const { password, ...safeUser } = user; // усе, крім password
Останній приклад - зручний спосіб прибрати поле з об'єкта без мутації.
Що варто знати про spread:
- копія поверхнева. Вкладені об'єкти й масиви лишаються спільними:
const copy = { ...settings };
copy.limits.upload = 1; // змінило й settings.limits
Для глибокої копії - structuredClone(settings).
- порядок важливий:
{ ...user, role: 'admin' }перезапише роль, а{ role: 'admin', ...user }- ні, якщо вuserвона є; - spread об'єкта копіює лише власні перелічувані властивості - без прототипу, гетери обчислюються в значення. Тому
{ ...dateInstance }чи{ ...classInstance }дасть простий об'єкт без методів; - spread
nullчиundefinedв об'єкті нічого не додає (зручно для умовних полів:{ ...(isAdmin && { role: 'admin' }) }), а в масиві - кидаєTypeError; - дуже великі масиви як аргументи (
Math.max(...hugeArray)на сотні тисяч елементів) можуть перевищити ліміт аргументів - для нихreduce.
Spread - основа незмінних оновлень стану у React, Pinia, Redux: замість зміни об'єкта створюється новий з потрібними змінами.
У PHP ті самі три крапки: ...$args у параметрах (rest) і [...$a, ...$b] чи f(...$args) (spread).
Шаблонні рядки записують у зворотних лапках `. Вони підтримують підстановку виразів і багаторядковість:
const name = 'Оля';
const count = 3;
const message = `Привіт, ${name}! У вас ${count} нових ${count === 1 ? 'повідомлення' : 'повідомлень'}.`;
const html = `
<div class="card">
<h3>${title}</h3>
</div>
`;
Усередині ${...} - будь-який вираз: виклик функції, тернарний оператор, арифметика. Результат перетворюється на рядок.
Чим кращі за склеювання через +:
- читається як готовий текст, без нагромадження лапок і плюсів;
- переноси рядків зберігаються як є;
- не треба екранувати одинарні й подвійні лапки всередині.
Пастки:
- HTML з даними користувача через шаблонний рядок і
innerHTML- XSS. Шаблонний рядок нічого не екранує; - відступи багаторядкового шаблону потрапляють у рядок;
undefinedіnullпідставляться як текст'undefined'/'null'- видно в інтерфейсі, якщо дані не прийшли.
Теговані шаблони - функція перед шаблоном отримує окремо статичні частини й підставлені значення:
function highlight(strings, ...values) {
return strings.reduce(
(result, part, i) => result + part + (i < values.length ? `<mark>${escapeHtml(values[i])}</mark>` : ''),
'',
);
}
highlight`Знайдено ${count} результатів для «${query}»`;
Функція бачить, де закінчується довірений текст розробника і починаються дані, - і може їх обробити: екранувати, параметризувати.
Де теговані шаблони використовують:
- безпечний SQL: бібліотеки на кшталт
postgres(sql`SELECT * FROM users WHERE id = ${id}`) перетворюють значення на параметри запиту, а не вставляють у текст - захист від SQL-ін'єкцій; - HTML-шаблони з екрануванням:
lit-html(html`<p>${text}</p>`); - CSS-in-JS: styled-components;
- GraphQL-запити:
gql`...`; String.raw- вбудований тег, що не обробляє екранування:String.raw`C:\new\path`лишає\nяк два символи. Зручно для регулярних виразів і шляхів Windows.
Масив статичних частин має властивість raw - ті самі частини без обробки екранування.
Фронтенд-код ламається так само, як серверний: форма перестає відправлятися, фільтр показує не ті дані, кнопка «Оплатити» неактивна. Без тестів про це дізнаються користувачі, а кожен рефакторинг стає ризиком.
Рівні тестів:
| Рівень | Що перевіряє | Інструменти | Швидкість |
|---|---|---|---|
| unit | одна функція чи модуль окремо: форматування ціни, валідація, розрахунок кошика | Vitest, Jest | мілісекунди |
| компонентні / інтеграційні | кілька частин разом у DOM: компонент з формою, відправка, повідомлення про помилку | Vitest + Testing Library, jsdom чи справжній браузер | десятки мілісекунд |
| end-to-end (E2E) | увесь застосунок у справжньому браузері з бекендом: вхід, оформлення замовлення | Playwright, Cypress, Pest browser tests | секунди |
Піраміда радить багато дешевих unit-тестів, менше інтеграційних і кілька E2E: що вище рівень, то тест повільніший, крихкіший і дорожчий у підтримці.
«Тестовий трофей» (Kent C. Dodds) - альтернативний погляд для фронтенду: найбільше користі дають інтеграційні тести, бо unit-тести окремих функцій інтерфейсу часто перевіряють деталі реалізації, а не те, що бачить користувач. Основа трофея - статичний аналіз: TypeScript і ESLint ловлять цілий клас помилок без жодного тесту.
Що тестувати насамперед:
- чисту логіку без DOM: розрахунки, перетворення даних, парсинг - найдешевші й найстабільніші тести;
- критичні сценарії користувача (вхід, оплата, відправка форми) - кількома E2E-тестами;
- місця, де вже були баги: тест на баг гарантує, що він не повернеться.
Що не тестувати:
- бібліотеки й фреймворк - вони протестовані авторами;
- точну розмітку й стилі - такі тести падають від кожної зміни дизайну;
- приватні деталі реалізації, які користувач не бачить.
Для Laravel-проєкту: бекенд покривають Pest-тестами, а для фронтенду часто достатньо unit-тестів складної логіки в Vitest і браузерних тестів основних сторінок - вони перевіряють і JavaScript, і серверну частину одночасно.
Докладніше в документації: Martin Fowler: The Practical Test Pyramid
Vitest - тестовий фреймворк, побудований на Vite. Він використовує ту саму конфігурацію, плагіни й псевдоніми шляхів, що й збирання застосунку, тож тести бачать код так само, як браузер.
npm install -D vitest
// resources/js/utils/price.js
export function formatPrice(cents) {
return `${(cents / 100).toFixed(2)} грн`;
}
// resources/js/utils/price.test.js
import { describe, it, expect } from 'vitest';
import { formatPrice } from './price';
describe('formatPrice', () => {
it('перетворює копійки на гривні', () => {
expect(formatPrice(12345)).toBe('123.45 грн');
});
it('показує нуль', () => {
expect(formatPrice(0)).toBe('0.00 грн');
});
});
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
Запуск:
npx vitest- режим спостереження: при зміні файлу перезапускаються лише пов'язані з ним тести;npx vitest run- один прогін, для CI;npx vitest price- лише файли, у назві яких єprice.
Основні перевірки:
| Матчер | Для чого |
|---|---|
toBe |
примітиви, порівняння за === |
toEqual |
об'єкти й масиви за вмістом |
toStrictEqual |
те саме, але враховує undefined-поля і класи |
toContain, toHaveLength |
масиви й рядки |
toThrow |
функція кидає помилку |
toMatchObject |
об'єкт містить указані поля |
Типова помилка - expect({ a: 1 }).toBe({ a: 1 }): це різні об'єкти, тест падає. Для об'єктів - toEqual.
Чому Vitest, а не Jest, у проєкті на Vite:
- одна конфігурація:
vite.config.jsз псевдонімом@/уже працює в тестах, не треба дублювати її для Jest; - ES-модулі й TypeScript без Babel і додаткових трансформерів;
- швидкий режим спостереження завдяки графу модулів Vite;
- API сумісний з Jest (
describe,it,expect, моки) - перехід зазвичай механічний.
Середовище: за замовчуванням тести виконуються в Node.js. Для коду, що працює з DOM, потрібне браузерне середовище (jsdom, happy-dom чи справжній браузер).
Головна небезпека в асинхронних тестах - тест завершується раніше, ніж перевірка. Тест, у якому жодна перевірка не виконалася, вважається пройденим.
Неправильно:
it('завантажує користувача', () => {
fetchUser(1).then((user) => {
expect(user.name).toBe('Олена'); // виконається після завершення тесту
});
});
Тест «зелений» навіть тоді, коли fetchUser повертає зовсім іншого користувача.
Правильно - async/await:
it('завантажує користувача', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Олена');
});
Тестовий фреймворк чекає на Promise, який повертає функція тесту.
Перевірка відхиленого Promise:
it('кидає помилку для неіснуючого користувача', async () => {
await expect(fetchUser(999)).rejects.toThrow('Not found');
});
it('повертає дані', async () => {
await expect(fetchUser(1)).resolves.toMatchObject({ id: 1 });
});
await перед expect(...).rejects обов'язковий - без нього перевірка знову не встигне виконатися.
Варіант з try/catch потребує захисту:
it('кидає помилку', async () => {
expect.assertions(1); // тест упаде, якщо перевірок буде не рівно одна
try {
await fetchUser(999);
} catch (error) {
expect(error.message).toBe('Not found');
}
});
Без expect.assertions(1) тест пройде, якщо fetchUser помилково не кине винятку - блок catch просто не виконається.
Таймери й затримки не варто чекати по-справжньому (await new Promise(r => setTimeout(r, 3000))): тест стане повільним і нестабільним. Для цього є фейкові таймери.
Перевірка, що все дочекалося:
expect.hasAssertions()- хоча б одна перевірка виконалася;vi.waitFor(() => expect(...))- повторює перевірку, доки вона не пройде чи не мине час, коли результат з'являється не одразу.
Порада: напишіть тест, переконайтеся, що він падає, якщо зламати код. Тест, який ніколи не падав, міг нічого не перевіряти.
Покриття (coverage) - частка коду, яка виконалася під час тестів.
npm install -D @vitest/coverage-v8
npx vitest run --coverage
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
price.js | 100 | 75 | 100 | 100 | 12
cart.js | 62.50 | 50 | 66.66 | 62.50 | 18-24,31
Метрики:
| Метрика | Що рахує |
|---|---|
| Statements / Lines | виконані інструкції чи рядки |
| Functions | функції, які хоч раз викликали |
| Branches | гілки умов: обидві частини if/else, ? :, ??, && |
Branches - найкорисніша: 100% рядків може бути навіть тоді, коли гілка else жодного разу не перевірялася.
Провайдери у Vitest: v8 (за замовчуванням, використовує вбудоване покриття рушія, швидкий) і istanbul (інструментує код, працює в будь-якому середовищі).
Що покриття НЕ показує:
- чи є перевірки: тест, що викликає функцію без жодного
expect, дає 100% покриття і нічого не перевіряє; - чи перевірки правильні: код виконано, але очікуваний результат у тесті міг бути скопійований з помилкової реалізації;
- граничні випадки: рядок
price * qtyпокрито, але ніхто не перевірив від'ємну кількість, нуль, дробові значення; - поєднання умов: обидві гілки двох
ifпокрито окремо, але не всі чотири комбінації; - інтеграцію: кожен модуль покрито, а разом вони не працюють.
Як користуватися з розумом:
- дивитися на непокриті рядки, а не на відсоток: звіт показує, яку логіку забули перевірити;
- поріг у CI (
coverage.thresholds) - захист від падіння покриття, але не ціль. Вимога «90% будь-якою ціною» породжує тести без сенсу; - виключити згенерований код, конфігурації, типи (
coverage.exclude), щоб вони не спотворювали картину.
Що краще оцінює якість тестів - мутаційне тестування (Stryker для JavaScript, Pest Mutate для PHP): інструмент навмисно змінює код (> на >=, + на -) і перевіряє, чи впаде хоч один тест. Якщо мутація «вижила» - тести цей код насправді не перевіряють.
Клас - синтаксис для створення об'єктів з однаковою структурою й поведінкою. Під капотом це звичайна функція-конструктор з прототипом, але запис читабельніший.
class Product {
static count = 0; // статичне поле: належить класу, а не екземпляру
currency = 'UAH'; // поле екземпляра зі значенням за замовчуванням
#discount = 0; // приватне поле
constructor(name, price) {
this.name = name;
this.price = price;
Product.count++;
}
get finalPrice() { // гетер: читається як властивість
return this.price - this.#discount;
}
applyDiscount(amount) { // метод: спільний для всіх екземплярів через прототип
this.#discount = amount;
return this;
}
static fromJson(data) { // статичний метод: фабрика
return new Product(data.name, data.price);
}
}
const p = new Product('Книга', 500).applyDiscount(50);
p.finalPrice; // 450
Product.count; // 1
Успадкування:
class DigitalProduct extends Product {
constructor(name, price, url) {
super(name, price); // обов'язково до звернення до this
this.url = url;
}
toString() {
return `${this.name} (${this.url})`;
}
}
Правила, на яких часто помиляються:
super()передthisу конструкторі класу-нащадка - інакшеReferenceError: Must call super constructor ... before accessing 'this';- клас без
newвикликати не можна:Product('x')даєTypeError: Class constructor Product cannot be invoked without 'new'; - класи не піднімаються як функції: використати клас до його оголошення -
ReferenceError(тимчасова мертва зона, як уlet); - тіло класу завжди в суворому режимі;
- методи не прив'язані до екземпляра:
const fn = p.applyDiscount; fn(10)втратитьthis. Для обробників подій використовують стрілкову функцію в полі класу чиbind.
Поля чи присвоєння в конструкторі: поля класу оголошують структуру об'єкта в одному місці й створюються до виконання тіла конструктора (для нащадка - одразу після super()).
Статичний блок static { ... } виконується один раз при оголошенні класу - для складної ініціалізації статичних полів.
Коли клас доречний: об'єкти з власним станом і поведінкою, ієрархії, з якими працює бібліотека (веб-компоненти, класи помилок). Для простих даних достатньо об'єктного літерала, а для поведінки без стану - звичайних функцій.
JavaScript виконує код в одному потоці. Асинхронні дії (таймери, мережа, події) виконує середовище - браузер чи Node.js, а колбеки, що мають спрацювати після них, стають у черги. Event loop бере з черг наступне завдання, коли стек викликів порожній.
Дві черги з різним пріоритетом:
- Макрозадачі (tasks):
setTimeout,setInterval, події користувача, повідомлення з мережі. - Мікрозадачі (microtasks): колбеки Promise (
then, продовження післяawait),queueMicrotask,MutationObserver.
Порядок: виконати одну макрозадачу → виконати всі мікрозадачі, що накопичилися (включно з тими, що додалися під час виконання) → (у браузері) відмалювати сторінку → наступна макрозадача.
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
// 1, 4, 3, 2
setTimeout(fn, 0) не означає «одразу»: це макрозадача, і вона чекає, поки виконаються всі мікрозадачі.
Практичні наслідки:
- Довгий синхронний код (великий цикл, важкий
JSON.parse) блокує все: кліки, анімацію, рендер. Важку роботу ділять на частини або виносять у Web Worker. - Нескінченний ланцюжок мікрозадач теж заморожує сторінку: рендер чекає, поки черга мікрозадач спорожніє.
Усі чотири приймають масив Promise і запускаються паралельно, але по-різному вирішують, коли й чим завершитися:
Promise.all- чекає всі. Результат - масив значень у тому ж порядку. Перша ж помилка відхиляє весь результат (інші Promise продовжують виконуватися, але їхні результати губляться).Promise.allSettled- чекає всі, ніколи не відхиляється. Результат - масив об'єктів{status: 'fulfilled', value}або{status: 'rejected', reason}.Promise.race- результат першого, що завершився, хай успіхом, хай помилкою.Promise.any- перший успішний. Відхиляється зAggregateError, лише якщо відхилено всі.
// Усе потрібне для сторінки - інакше показати помилку
const [user, settings] = await Promise.all([getUser(), getSettings()]);
// Розіслати сповіщення й порахувати, скільки не дійшло
const results = await Promise.allSettled(recipients.map(send));
const failed = results.filter((r) => r.status === 'rejected').length;
// Тайм-аут
const data = await Promise.race([fetchData(), sleep(5000).then(() => { throw new Error('timeout'); })]);
// Перше доступне дзеркало
const file = await Promise.any(mirrors.map((url) => fetch(url)));
Нюанс race для тайм-ауту: «програлий» запит не скасовується, він продовжує виконуватися. Для справжнього скасування потрібен AbortController (у fetch - AbortSignal.timeout(5000)).
Старі API (події, таймери, бібліотеки з колбеками) зручно «загорнути» в Promise, щоб використовувати з await. Це називають промісифікацією.
Класичний спосіб - конструктор new Promise:
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error(`Не вдалося завантажити ${src}`));
image.src = src;
});
}
await sleep(500);
const image = await loadImage('/logo.png');
Правила:
resolveіrejectдіють лише перший раз - повторні виклики ігноруються;- виняток, кинутий синхронно всередині функції-виконавця, автоматично відхиляє Promise;
- не загортайте в
new Promiseте, що вже повертає Promise, - це зайвий шар і місце для втрачених помилок.
Promise.withResolvers() (ES2024) повертає Promise разом з його resolve і reject назовні:
const { promise, resolve, reject } = Promise.withResolvers();
Це зручно, коли виконання відбувається не в одному колбеку, а десь пізніше - наприклад, чекаємо на відповідь у WebSocket за ідентифікатором запиту:
const pending = new Map();
function request(socket, payload) {
const id = crypto.randomUUID();
const { promise, resolve, reject } = Promise.withResolvers();
pending.set(id, { resolve, reject });
socket.send(JSON.stringify({ id, ...payload }));
return promise;
}
socket.onmessage = (event) => {
const { id, result, error } = JSON.parse(event.data);
const handlers = pending.get(id);
pending.delete(id);
error ? handlers?.reject(new Error(error)) : handlers?.resolve(result);
};
Раніше для цього писали той самий шаблон вручну, зберігаючи resolve у зовнішні змінні з конструктора.
У Node.js для функцій зі стилем (error, result) є готовий util.promisify, а більшість модулів мають Promise-версії (node:fs/promises).
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 створений саме як словник, а об'єкт - як запис з полями.
Відмінності:
- Ключі. У об'єкта ключі - лише рядки й символи (число
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.
Змінюють масив на місці (мутують): 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').
Групування масиву за ознакою довго писали вручну через 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.
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+.
Питання з реальних технічних співбесід - 114 питань у 10 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
- Теми
- Браузерні API й мережа 12 Сучасний синтаксис 12 Асинхронність 12 Типи й приведення 12 DOM і події 12 Масиви й об'єкти 12 Функції й замикання 12 Тестування 10
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії