Питання на співбесіді з 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).
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 такі обробники блокує.
Події:
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 у цьому немає потреби.
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, підвантаження) замінює елементи - збережені посилання вказують на вузли, яких уже немає на сторінці. Делегування подій чи повторний пошук після оновлення.
Це два незалежні механізми, які часто плутають.
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.
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 без збирача).
Іменований експорт - модуль може мати їх скільки завгодно, імпортують їх за точною назвою у фігурних дужках:
// 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 змушує збирач обробити всі реекспортовані модулі, а в режимі розробки - ще й завантажити їх.
Браузер уміє виконувати 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-файлів.
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 для прибирання: зняти індикатор завантаження, розблокувати кнопку, закрити з'єднання - те, що має статися незалежно від результату.
Усі вбудовані помилки - нащадки 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 по ньому можна клікнути й перейти до коду.
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 в режимі розробки є завжди.
Усі три зберігають дані в браузері, але з різним терміном життя, обсягом і, головне, - різною поведінкою щодо сервера.
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 (асинхронна база в браузері).
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, що використовує його всередині.
Збирати адреси склеюванням рядків - джерело помилок: забуті ? і &, неекрановані пробіли, кирилиця й спецсимволи. Для цього є вбудовані класи.
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) - без них збережеться лише останнє значення.
Деструктуризація - короткий запис, щоб витягти значення з об'єкта чи масиву в окремі змінні.
Об'єкти - за назвами властивостей:
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 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
- Теми
- Браузерні API й мережа 12 Сучасний синтаксис 12 Асинхронність 12 Типи й приведення 12 DOM і події 12 Масиви й об'єкти 12 Функції й замикання 12 Тестування 10
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії