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

Питання на співбесіді з JavaScript

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

114 питань

Значення з полів форми, URL і dataset - завжди рядки. Перетворити їх на число можна кількома способами, і вони поводяться по-різному.

Number(value) і унарний плюс +value - однаково строгі: весь рядок має бути числом.

Number('42');      // 42
Number(' 42 ');    // 42 - пробіли по краях ігноруються
Number('42px');    // NaN
Number('3.14');    // 3.14
Number('');        // 0 - пастка!
Number(null);      // 0
Number(undefined); // NaN
+'42';             // 42

parseInt(value, radix) читає ціле число з початку рядка й зупиняється на першому недопустимому символі:

parseInt('42px');       // 42
parseInt('3.99');       // 3 - дробова частина відкидається
parseInt('px42');       // NaN
parseInt('ff', 16);     // 255

Завжди передавайте основу числення другим аргументом (parseInt(value, 10)) - так намір очевидний, а лінтери цього вимагають.

parseFloat(value) - те саме для дробових: parseFloat('3.14abc') → 3.14.

Що обрати:

  • дані з форми, де очікується саме число - Number(), і перевіряти результат. Він не «пропустить» '12abc';
  • значення з одиницями CSS ('16px') - parseInt/parseFloat;
  • +value - коротко, але менш помітно в коді.

Пастки:

  • порожній рядок дає 0, а не NaN. Порожнє поле «кількість» перетвориться на нуль. Порожнечу треба перевіряти окремо;
  • parseInt з числом, а не рядком: parseInt(0.0000005) → 5, бо число спершу перетворюється на рядок '5e-7'. Для округлення чисел є Math.trunc, Math.round;
  • parseInt як колбек map: ['1', '2', '3'].map(parseInt) дає [1, NaN, NaN], бо map передає індекс другим аргументом, і він стає основою числення. Правильно: .map(Number) чи .map((s) => parseInt(s, 10));
  • кома як десятковий роздільник: українські користувачі пишуть '12,5', а Number('12,5') - це NaN. Для введення сум потрібна нормалізація (value.replace(',', '.')) або поле type="number", значення якого браузер віддає з крапкою.

Перевірка результату: Number.isNaN(n) чи Number.isFinite(n) (відкидає і NaN, і Infinity).

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

element.onclick = fn - лише один обробник: наступне присвоєння перезаписує попередній. addEventListener додає скільки завгодно обробників на одну подію й має налаштування.

button.onclick = () => track('click');
button.onclick = () => openModal();   // track більше не спрацює

button.addEventListener('click', () => track('click'));
button.addEventListener('click', () => openModal()); // спрацюють обидва

Опції addEventListener:

  • once: true - обробник автоматично знімається після першого спрацювання.
  • passive: true - обіцянка не викликати preventDefault(). Для touchstart/wheel це дозволяє браузеру прокручувати сторінку плавно, не чекаючи обробника.
  • capture: true - обробник спрацює на фазі перехоплення, ще до цільового елемента.
  • signal - зняти обробник через AbortController, зручно для кількох обробників одразу.

Зняти обробник можна лише тим самим посиланням на функцію:

function onResize() { /* ... */ }
window.addEventListener('resize', onResize);
window.removeEventListener('resize', onResize);   // працює

window.addEventListener('resize', () => {});
window.removeEventListener('resize', () => {});   // нічого не зніме: інша функція

Атрибути в HTML (<button onclick="...">) - найгірший варіант: код змішано з розміткою, а суворий Content Security Policy такі обробники блокує.

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

Події:

  • DOMContentLoaded - HTML повністю розібрано, DOM готовий. Зображення, стилі (крім тих, що блокують скрипти) й iframe ще можуть вантажитися. Тут зазвичай ініціалізують скрипти.
  • load (на window) - завантажено все: зображення, шрифти, iframe. Відбувається пізніше, часто значно.

Звичайний <script src> у <head> зупиняє розбір HTML, доки скрипт не завантажиться й не виконається. Сторінка «біла», а скрипт ще й не бачить елементи нижче.

  • defer - завантажити паралельно з розбором, а виконати після розбору HTML, перед DOMContentLoaded, у порядку появи в документі.
  • async - завантажити паралельно й виконати, щойно завантажився, у довільному порядку, перериваючи розбір.
<script defer src="/app.js"></script>        <!-- основний код, залежить від DOM -->
<script async src="/analytics.js"></script>  <!-- незалежний, порядок не важливий -->
<script type="module" src="/main.js"></script> <!-- модулі поводяться як defer -->

Правило: код застосунку - defer (або type="module"), незалежні сторонні скрипти (аналітика, віджети) - async. Розміщувати скрипти в кінці <body> - старий спосіб досягти схожого ефекту; з defer у цьому немає потреби.

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

querySelector(selector) повертає перший елемент, що відповідає CSS-селектору, або null. querySelectorAll(selector) - усі відповідні елементи.

const form = document.querySelector('#order-form');
const firstError = document.querySelector('.field.is-invalid input');
const buttons = document.querySelectorAll('[data-action="delete"]');

buttons.forEach((button) => button.addEventListener('click', onDelete));

Селектор - будь-який валідний CSS: класи, атрибути, псевдокласи (:checked, :not(...)), комбінатори.

querySelectorAll повертає статичний NodeList - знімок на момент виклику. Елементи, додані пізніше, у ньому не з'являться. У NodeList є forEach, але немає map і filter - для них [...nodes] чи Array.from(nodes).

Старші методи:

  • getElementById('id') - найшвидший пошук за id, повертає елемент або null;
  • getElementsByClassName, getElementsByTagName - повертають живу HTMLCollection, яка оновлюється разом з DOM. Це може дивувати: видалення елементів у циклі по такій колекції пропускає кожен другий.

Пошук не лише від document. Усі методи працюють і на елементі - пошук тоді обмежується його нащадками:

const card = document.querySelector('.product-card');
const price = card.querySelector('.price');

closest(selector) шукає вгору - найближчого предка (або сам елемент), що відповідає селектору. Незамінний при делегуванні подій:

document.addEventListener('click', (event) => {
  const row = event.target.closest('tr[data-id]');
  if (row) openOrder(row.dataset.id);
});

matches(selector) - чи відповідає сам елемент селектору.

Пастки:

  • перевіряти на null: document.querySelector('.missing').classList кине TypeError. Або ?., або явна перевірка;
  • id, що починаються з цифри чи містять спецсимволи, треба екранувати в селекторі: document.querySelector(`#${CSS.escape(id)}`);
  • скрипт виконується до розмітки: якщо <script> у <head> без defer, елементів ще немає. Розв'язується атрибутом defer чи type="module";
  • динамічний вміст (Livewire, Alpine, підвантаження) замінює елементи - збережені посилання вказують на вузли, яких уже немає на сторінці. Делегування подій чи повторний пошук після оновлення.

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

Це два незалежні механізми, які часто плутають.

preventDefault() скасовує стандартну дію браузера для події:

form.addEventListener('submit', async (event) => {
  event.preventDefault();             // не перезавантажувати сторінку
  await fetch(form.action, { method: 'POST', body: new FormData(form) });
});

link.addEventListener('click', (event) => {
  event.preventDefault();             // не переходити за посиланням
  openModal(link.href);
});

Типові стандартні дії: відправка форми, перехід за посиланням, встановлення галочки чекбокса, введення символу в поле при keydown, контекстне меню при правому кліку, прокрутка колесом.

stopPropagation() зупиняє поширення події DOM-деревом: батьківські елементи її не отримають.

dropdown.addEventListener('click', (event) => {
  event.stopPropagation();   // клік усередині меню не дійде до document
});

document.addEventListener('click', () => closeAllDropdowns());

Стандартна дія при цьому не скасовується: посилання всередині все одно спрацює.

stopImmediatePropagation() - ще й не викликати інші обробники цього самого елемента, зареєстровані пізніше.

Чому stopPropagation варто уникати:

  • він ламає код, що спирається на спливання: делегування подій, аналітику, закриття випадних меню кліком поза ними, обробники фреймворків на document;
  • проблему зазвичай краще вирішити перевіркою в батьківському обробнику: if (dropdown.contains(event.target)) return;.

return false в обробнику, призначеному через onclick="...", скасовує стандартну дію. В addEventListener повернене значення ігнорується - потрібен явний preventDefault().

Пасивні обробники ({ passive: true }) обіцяють браузеру, що preventDefault() не буде викликано. Тоді браузер може почати прокрутку, не чекаючи на обробник. Виклик preventDefault() у пасивному обробнику ігнорується (з попередженням у консолі).

Перевірка: event.defaultPrevented показує, чи хтось уже скасував стандартну дію - корисно, щоб не обробляти подію двічі.

У фреймворках: Alpine @submit.prevent, @click.stop; Livewire wire:submit сам скасовує стандартну відправку форми; Vue - ті самі модифікатори .prevent і .stop.

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

ES-модуль - файл JavaScript з власною областю видимості, який явно експортує те, що дає назовні, й імпортує те, що йому потрібно.

// money.js
export function formatPrice(amount) {
  return `${amount.toFixed(2)} грн`;
}

// app.js
import { formatPrice } from './money.js';
console.log(formatPrice(100));
<script type="module" src="/app.js"></script>

Як було до модулів: усі <script> на сторінці ділили одну глобальну область видимості. Функція formatPrice з одного файлу ставала глобальною і могла перезаписати однойменну з іншого. Порядок підключення скриптів доводилося вибудовувати вручну, а залежності між файлами ніде не були записані.

Чим модулі відрізняються:

  • власна область видимості: змінні верхнього рівня модуля не потрапляють у window;
  • явні залежності: з import видно, звідки що береться, - це читає і людина, і збирач;
  • строгий режим завжди ('use strict' не потрібен);
  • виконуються один раз: скільки б модулів не імпортували money.js, він завантажиться й виконається лише раз, а всі отримають той самий екземпляр;
  • відкладене виконання: <script type="module"> поводиться як defer - виконується після розбору HTML і не блокує його;
  • top-level await дозволений;
  • this на верхньому рівні - undefined, а не window;
  • завантаження з іншого домену підпорядковується CORS.

Імпорти статичні: import - лише на верхньому рівні модуля, шлях - рядок-константа. Завдяки цьому залежності відомі ще до виконання коду - звідси можливість tree shaking. Для завантаження «за потреби» є динамічний import().

Модулі в Laravel-проєкті: resources/js/app.js - точка входу ES-модуля. Vite збирає граф імпортів, а директива @vite в Blade підключає результат як <script type="module">. Пакети з npm імпортуються за назвою (import axios from 'axios') - браузер такого шляху не розуміє, тому розв'язує його збирач (або import map без збирача).

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

Іменований експорт - модуль може мати їх скільки завгодно, імпортують їх за точною назвою у фігурних дужках:

// validation.js
export function isEmail(value) { /* ... */ }
export const MAX_LENGTH = 255;

// інший файл
import { isEmail, MAX_LENGTH } from './validation.js';
import { isEmail as checkEmail } from './validation.js';   // перейменування
import * as validation from './validation.js';              // усе як об'єкт-простір імен

Експорт за замовчуванням - один на модуль, імпортується без дужок і під будь-якою назвою:

// Modal.vue, api.js
export default function createApi(baseUrl) { /* ... */ }

import createApi from './api.js';
import makeClient from './api.js';   // теж працює - та сама функція

Модуль може мати й те, й інше: import axios, { AxiosError } from 'axios'.

Аргументи на користь іменованих експортів:

  • однакові назви всюди: не буває ситуації, коли в одному файлі функцію імпортували як createApi, в іншому як apiFactory, - пошук і рефакторинг простіші;
  • помилка в назві помітна одразу: import { isEmial } - збирач чи редактор скаже, що такого експорту немає. З експортом за замовчуванням будь-яка назва «правильна»;
  • автоімпорт у редакторі працює точніше;
  • tree shaking простіше розібратися з окремими експортами, ніж з одним великим об'єктом за замовчуванням.

Через це багато команд мають правило лінтера «лише іменовані експорти» (import/no-default-export).

Коли експорт за замовчуванням природний:

  • файл = одна сутність: компонент Vue (.vue-файли експортують компонент за замовчуванням), сторінка в Next.js, конфіг (vite.config.js - export default defineConfig(...));
  • вимога інструмента, що шукає саме default.

Реекспорт - для «бочок» (index.js, що збирає експорти папки):

export { isEmail, MAX_LENGTH } from './validation.js';
export { default as Modal } from './Modal.vue';
export * from './formatters.js';

Великі «бочки» мають ціну: імпорт однієї функції з index.js змушує збирач обробити всі реекспортовані модулі, а в режимі розробки - ще й завантажити їх.

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

Браузер уміє виконувати ES-модулі, тож чому не віддати йому файли як є? Тому що код, який зручно писати, і код, який швидко завантажується, - різні.

Що робить збирач під час розробки (Vite npm run dev):

  • розв'язує імпорти пакетів: import axios from 'axios' браузер не розуміє - Vite перетворює на шлях до файлу в node_modules;
  • перетворює те, що браузер не вміє: TypeScript, .vue-файли, JSX, Tailwind і PostCSS;
  • гаряча заміна модулів (HMR): змінили компонент - оновився лише він, без перезавантаження сторінки й зі збереженням стану;
  • віддає модулі браузеру майже без обробки, тож сервер стартує за мілісекунди незалежно від розміру проєкту - у цьому головна перевага Vite над старими збирачами.

Що робить збирач для продакшену (npm run build):

  • об'єднує модулі в невелику кількість файлів - сотні дрібних запитів повільніші за кілька великих;
  • мініфікує: прибирає пробіли й коментарі, скорочує імена змінних;
  • tree shaking: викидає код, який ніде не імпортується;
  • розділення коду: спільні залежності - в окремий файл, сторінки й важкі компоненти - в окремі частини, що завантажуються за потреби;
  • хешовані імена файлів (app-3f9a1c.js): вміст змінився - змінилася назва. Тоді файли можна кешувати в браузері «назавжди», а після деплою користувачі гарантовано отримають нову версію;
  • маніфест - відповідність вихідних файлів і зібраних. Саме з нього директива @vite('resources/js/app.js') у Laravel дізнається, який файл підключити.

У Laravel-проєкті:

  • npm run dev - сервер розробки, @vite підключає файли з нього (адресу записано у public/hot);
  • npm run build - збірка в public/build, @vite бере шляхи з public/build/manifest.json;
  • помилка «Unable to locate file in Vite manifest» означає, що збірку не виконано або файл не вказано в input конфігурації laravel-vite-plugin.

Альтернативи: webpack (старіший, гнучкіший, повільніший), esbuild і Rollup (на яких побудовано Vite), Rolldown (новий швидкий збирач для майбутніх версій Vite). Можна й зовсім без збирача - import maps і нативні модулі, - для невеликих проєктів без TypeScript чи .vue-файлів.

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

try - блок коду, у якому може статися помилка. catch - що робити, якщо вона сталася. finally - що виконати в будь-якому разі.

function loadSettings() {
  try {
    const raw = localStorage.getItem('settings');
    return JSON.parse(raw) ?? {};
  } catch (error) {
    console.warn('Пошкоджені налаштування, використовуємо типові', error);
    return {};
  } finally {
    hideLoader();   // виконається і при успіху, і при помилці
  }
}

Як це працює:

  • коли в try кидається виняток, решта блоку пропускається і керування переходить у catch;
  • у catch доступний об'єкт помилки: error.name, error.message, error.stack;
  • якщо помилки не було, catch не виконується;
  • finally виконується завжди - навіть якщо в try чи catch був return чи нова помилка.

Змінна в catch необов'язкова:

try {
  JSON.parse(input);
} catch {
  showError('Некоректний JSON');
}

Кинути власну помилку - throw:

if (!email.includes('@')) {
  throw new Error('Некоректна електронна адреса');
}

Кидати варто об'єкти Error (або нащадки), а не рядки: throw 'помилка' не має стеку викликів, і налагоджувати таке набагато важче.

Типові помилки новачків:

  • ловити все й мовчати: catch (e) {} - помилка зникла, а програма працює з неправильними даними. Як мінімум - записати в лог;
  • занадто великий try: обгорнути всю функцію, а потім не розуміти, який рядок кинув виняток. Краще обгортати лише те, що справді може впасти і що ви знаєте, як обробити;
  • очікувати, що try зловить помилку в колбеку setTimeout чи обробнику події - не зловить, бо колбек виконується пізніше, коли try уже завершено;
  • асинхронний код без await: try { fetchData() } catch не зловить відхилений Promise. Потрібно try { await fetchData() } catch.

finally для прибирання: зняти індикатор завантаження, розблокувати кнопку, закрити з'єднання - те, що має статися незалежно від результату.

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

Усі вбудовані помилки - нащадки Error. За типом зазвичай одразу зрозуміло, що пішло не так.

TypeError - операція над значенням не того типу. Найчастіша помилка у фронтенді:

const user = null;
user.name;              // TypeError: Cannot read properties of null (reading 'name')
undefined();            // TypeError: undefined is not a function
const PI = 3.14; PI = 3; // TypeError: Assignment to constant variable

Зазвичай означає, що дані прийшли не такі, як очікувалося: API повернув null, елемент не знайдено на сторінці, змінну не ініціалізовано.

ReferenceError - звернення до змінної, якої не існує або яка ще не ініціалізована:

console.log(totla);     // ReferenceError: totla is not defined (друкарська помилка)
console.log(x); let x = 1;   // ReferenceError: Cannot access 'x' before initialization

SyntaxError - код чи дані неможливо розібрати:

JSON.parse('{ name: "Оля" }');   // SyntaxError: ключі в JSON мають бути в лапках

Синтаксична помилка в самому файлі скрипта не дає йому виконатися взагалі - жоден рядок файлу не спрацює.

RangeError - значення поза допустимими межами:

new Array(-1);              // RangeError: Invalid array length
(1.5).toFixed(200);         // RangeError: toFixed() digits argument must be between 0 and 100
function f() { f(); } f();  // RangeError: Maximum call stack size exceeded

URIError - некоректний URI: decodeURIComponent('%').

AggregateError - кілька помилок разом, наприклад від Promise.any, коли відхилилися всі Promise (error.errors - масив причин).

Як розрізняти в коді:

try {
  data = JSON.parse(text);
} catch (error) {
  if (error instanceof SyntaxError) {
    showError('Сервер повернув некоректні дані');
  } else {
    throw error;   // невідому помилку - далі, а не ковтати
  }
}

Читати повідомлення уважно: Cannot read properties of undefined (reading 'map') каже не про map, а про те, що об'єкт перед .map - undefined. Шукати треба, звідки взялося це undefined.

Стек викликів (error.stack) показує, через які функції пройшов виконання до помилки, - з номерами рядків. У DevTools по ньому можна клікнути й перейти до коду.

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

console.log - найпростіший спосіб, але для складних помилок значно ефективніше зупинити виконання й подивитися на стан програми зсередини.

Оператор debugger у коді зупиняє виконання в цьому місці, якщо відкрито DevTools:

function calculateTotal(items) {
  debugger;   // виконання зупиниться тут
  return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}

Без відкритих DevTools оператор нічого не робить. Але в репозиторій його не комітять - лінтери на це скаржаться.

Точки зупинки в DevTools (вкладка Sources) - те саме без зміни коду: клік по номеру рядка. Види:

  • умовна - зупиняється лише якщо вираз істинний (item.price < 0). Незамінно в циклі на тисячу ітерацій;
  • logpoint - нічого не зупиняє, а друкує вираз у консоль. Як console.log, але без зміни коду й перезбирання;
  • на подію - зупинитися на будь-якому click, submit, таймері (панель Event Listener Breakpoints);
  • на зміну DOM - зупинитися, коли елемент змінюється чи видаляється (контекстне меню елемента у вкладці Elements). Відповідає на питання «хто змінює цей елемент?»;
  • на запит - коли URL запиту містить рядок (XHR/fetch Breakpoints);
  • на винятки - «Pause on exceptions», включно з перехопленими.

Після зупинки:

  • Scope - значення всіх змінних у поточній функції й замиканнях;
  • Call Stack - як ми сюди потрапили; клік на попередню функцію показує її стан;
  • Watch - вирази, що перераховуються на кожному кроці;
  • консоль працює в контексті зупиненої функції - можна викликати функції й змінювати змінні;
  • кроки: Step over (наступний рядок), Step into (увійти у функцію), Step out (вийти з неї), Resume.

Корисні дрібниці:

  • $0 у консолі - поточний виділений у Elements елемент;
  • «Blackbox script» / «Add to ignore list» - не заходити в код бібліотек (Vue, Alpine) при покроковому виконанні;
  • вкладка Network - запити, відповіді, статуси; «Copy as fetch» для повторення запиту в консолі;
  • у Livewire-проєктах корисно дивитися запити livewire/update у Network - там видно, що компонент відправив і що отримав.

Мініфікований код налагоджувати неможливо - для цього потрібні source maps, які у Vite в режимі розробки є завжди.

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

Усі три зберігають дані в браузері, але з різним терміном життя, обсягом і, головне, - різною поведінкою щодо сервера.

localStorage sessionStorage cookies
живе доки не видалять доки відкрита вкладка до Expires/Max-Age або до закриття браузера
видимість усі вкладки домену лише поточна вкладка усі вкладки домену
обсяг ~5 МБ на домен ~5 МБ ~4 КБ на cookie
іде на сервер ні ні з кожним запитом
доступ з JS так так так, якщо не HttpOnly

Web Storage (localStorage, sessionStorage):

localStorage.setItem('theme', 'dark');
localStorage.getItem('theme');              // 'dark'
localStorage.removeItem('theme');

// лише рядки - об'єкти через JSON
localStorage.setItem('filters', JSON.stringify({ city: 'Київ' }));
const filters = JSON.parse(localStorage.getItem('filters') ?? '{}');
  • зберігаються лише рядки: setItem('count', 5) збереже '5';
  • API синхронний - великі записи блокують головний потік;
  • зміни в одній вкладці викликають подію storage в інших вкладках того самого домену - спосіб синхронізувати вкладки;
  • у приватному режимі, при заповненому сховищі чи заблокованих cookies доступ може кидати виняток - звертання варто обгортати в try.

Cookies створені для сервера: браузер автоматично додає їх до кожного запиту на домен. Тому в них зберігають ідентифікатор сесії, а не налаштування інтерфейсу - інакше кожен запит, включно з картинками, нестиме зайві байти.

Безпека - головне:

  • не зберігайте токени автентифікації в localStorage. Будь-який скрипт на сторінці (включно з XSS чи скомпрометованою бібліотекою) прочитає їх і відправить зловмиснику;
  • сесійна cookie з прапорцем HttpOnly недоступна JavaScript взагалі - XSS не зможе її вкрасти. Так працює автентифікація Laravel (і Sanctum для SPA на тому ж домені);
  • Secure - лише через HTTPS, SameSite - захист від підробки запитів з інших сайтів.

Що куди класти:

  • тема, згорнуті панелі, чернетка форми - localStorage;
  • стан майстра, що не повинен «перетікати» в іншу вкладку, - sessionStorage;
  • сесія, автентифікація - HttpOnly-cookie від сервера;
  • великі обсяги чи бінарні дані - IndexedDB (асинхронна база в браузері).

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

FormData збирає дані форми у формат, який браузер відправляє так само, як звичайна HTML-форма, - зокрема з файлами.

const form = document.querySelector('#profile-form');

form.addEventListener('submit', async (event) => {
  event.preventDefault();

  const data = new FormData(form);          // усі поля з атрибутом name, включно з файлами
  data.append('source', 'web');             // додаткове поле

  const response = await fetch(form.action, {
    method: 'POST',
    body: data,
    headers: { Accept: 'application/json' },
  });
});

Найчастіша помилка - вручну задати Content-Type:

headers: { 'Content-Type': 'multipart/form-data' }   // зламає запит

Для multipart/form-data заголовок обов'язково містить межу (boundary), що розділяє частини: multipart/form-data; boundary=----WebKitFormBoundary.... Браузер генерує її сам, якщо тіло - FormData. Вказаний вручну заголовок межі не має - сервер не зможе розібрати запит.

Корисні методи:

data.get('email');                    // значення поля
data.getAll('tags[]');                // усі значення поля з однаковою назвою
data.set('email', 'new@example.com'); // замінити
data.delete('password_confirmation');
Object.fromEntries(data);             // в об'єкт (для полів без повторів)

Файли: <input type="file" name="avatar"> потрапляє у FormData автоматично. Додати файл чи Blob вручну - data.append('avatar', file, 'avatar.png').

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

  • PUT/PATCH з файлами: PHP історично розбирає multipart/form-data лише для POST. Для оновлення з файлами відправляють POST з підміною методу: data.append('_method', 'PUT');
  • CSRF: для форм, що відправляються fetch-ем, токен з поля _token потрапить у FormData сам, якщо в Blade-формі є @csrf;
  • Accept: application/json - щоб Laravel при помилці валідації повернув JSON 422 з полем errors, а не редирект назад;
  • масиви називають як у HTML: tags[], items[0][qty] - Laravel розбере їх у вкладені масиви.

Коли не FormData: якщо файлів немає, а API приймає JSON, простіше body: JSON.stringify(payload) з Content-Type: application/json.

Прогрес завантаження великого файлу fetch наразі не показує. Для смуги прогресу використовують XMLHttpRequest (подія upload.onprogress) або бібліотеку на кшталт axios, що використовує його всередині.

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

Збирати адреси склеюванням рядків - джерело помилок: забуті ? і &, неекрановані пробіли, кирилиця й спецсимволи. Для цього є вбудовані класи.

URL - розбір і побудова адреси:

const url = new URL('https://laravelukraine.com/jobs?page=2#list');

url.hostname;     // 'laravelukraine.com'
url.pathname;     // '/jobs'
url.search;       // '?page=2'
url.hash;         // '#list'

// відносна адреса відносно базової
new URL('/api/users', window.location.origin).href;

URLSearchParams - параметри запиту:

const params = new URLSearchParams(window.location.search);
params.get('page');          // '2' - завжди рядок або null
params.has('q');
params.getAll('tags[]');     // для повторюваних параметрів

params.set('page', 3);
params.append('tags[]', 'php');
params.delete('page');
params.toString();           // 'tags%5B%5D=php'

Найзручніше - разом:

const url = new URL('/api/vacancies', location.origin);
url.searchParams.set('q', 'вакансії php');
url.searchParams.set('remote', '1');

await fetch(url);   // '/api/vacancies?q=%D0%B2%D0%B0...&remote=1'

Екранування робиться автоматично: кирилиця, пробіли, & і = у значеннях не зламають адресу.

Побудова з об'єкта:

const query = new URLSearchParams({ q: 'laravel ukraine', tag: 'php&js' });
query.toString();   // 'q=laravel+ukraine&tag=php%26js'

Пробіл кодується як + (формат форм), а не %20 - сервери розуміють обидва варіанти.

Корисні прийоми:

  • оновити адресу без перезавантаження (фільтри в каталозі):
const url = new URL(location.href);
url.searchParams.set('sort', 'price');
history.replaceState(null, '', url);
  • перевірка коректності: URL.canParse(value) - без try, бо конструктор new URL на некоректному рядку кидає TypeError;
  • безпека: перед переходом за адресою з даних користувача варто перевірити url.protocol - щоб не перейти на javascript:....

Пастки:

  • params.get повертає рядок: params.get('page') + 1 дасть '21'. Перетворюйте явно: Number(params.get('page') ?? 1);
  • URLSearchParams з об'єкта з масивом ({ ids: [1, 2] }) дасть ids=1%2C2 - для масивів потрібні окремі append.

Laravel читає повторювані параметри як масив лише з дужками (tags[]=php&tags[]=js) - без них збережеться лише останнє значення.

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

Деструктуризація - короткий запис, щоб витягти значення з об'єкта чи масиву в окремі змінні.

Об'єкти - за назвами властивостей:

const user = { id: 7, name: 'Оля', city: 'Львів', role: 'admin' };

const { name, city } = user;               // name = 'Оля', city = 'Львів'
const { name: userName } = user;           // перейменування
const { phone = 'не вказано' } = user;     // значення за замовчуванням
const { id, ...profile } = user;           // решта властивостей в об'єкт

Масиви - за позицією:

const [first, second] = ['php', 'laravel', 'vue'];
const [, , third] = ['php', 'laravel', 'vue'];   // пропуск елементів
const [head, ...tail] = [1, 2, 3, 4];           // tail = [2, 3, 4]

let a = 1, b = 2;
[a, b] = [b, a];                                // обмін значеннями

Вкладена деструктуризація:

const { address: { city } } = order;   // city з order.address

Якщо order.address - undefined, буде TypeError. Захист - значення за замовчуванням: const { address: { city } = {} } = order;.

Де це використовується найчастіше:

  • параметри функцій - іменовані опції:
function createUser({ name, email, role = 'user' }) { /* ... */ }
createUser({ email: 'a@b.ua', name: 'Оля' });
  • імпорти: import { ref, computed } from 'vue' - схожий синтаксис;
  • результат функції з кількома значеннями: const [count, setCount] = useState(0) у React, const { data, error } = await request();
  • перебір пар: for (const [key, value] of Object.entries(prices));
  • відповідь API: const { data, meta: { total } } = await response.json();.

Що варто знати:

  • значення за замовчуванням спрацьовує лише для undefined, не для null: const { limit = 10 } = { limit: null } дасть null;
  • деструктуризувати null чи undefined неможливо - TypeError;
  • деструктуризація масиву працює з будь-яким ітерованим значенням: рядком, Set, Map (const [[key, value]] = map);
  • деструктуризація створює поверхневі копії значень: вкладені об'єкти лишаються спільними з оригіналом.

Аналог у PHP - [$first, $second] = $array і ['name' => $name] = $user.

Докладніше в документації: Деструктуризація

Питання з реальних технічних співбесід - 114 питань у 10 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 37 Middle 41 Senior 36

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії