Вопросы на собеседовании Front-End разработчика: 37 вопросов с ответами
Короткий ответ: на собеседовании Junior Front-End разработчика проверяют не знание редких фактов, а понимание основ — семантики HTML, блочной модели и раскладок CSS, типов, замыканий и this в JavaScript, цикла событий, работы браузера и базовых вещей в TypeScript и React. Ниже — 37 вопросов в формулировках, в которых их задают, с короткими ответами: сначала то, что стоит сказать вслух, затем детали и код. Вопросы сгруппированы в семь блоков; после них — практические задачи с решениями, типичные ошибки и план подготовки на две недели.
Я во фронтенд-разработке с 2014 года и больше девяти лет преподаю. По моим наблюдениям, начинающие чаще спотыкаются не на экзотике, а на базе: могут собрать приложение на React, но не могут объяснить, почему setTimeout с нулевой задержкой срабатывает позже промиса. Поэтому в подборке больше всего места отдано JavaScript, а каждый пример кода можно запустить и проверить самому.
Все примеры на JavaScript запущены в Node.js v22.14.0, и вывод в комментариях — реальный. Примеры на TypeScript проверены компилятором TypeScript 5.9.3 в режиме strict. Фрагменты HTML, CSS, React и работы с DOM — минимальные стандартные примеры, в рамках статьи они не запускались. Подбор вопросов — выбор автора по опыту преподавания, а не статистика: в конкретной компании список будет другим.
Как проходит собеседование Front-End разработчика?
Обычно отбор состоит из четырёх этапов: короткий разговор с рекрутером, техническое интервью, практическое задание и финальная встреча с командой или руководителем. Порядок и количество этапов зависят от компании: где-то тестовое задание присылают до технического интервью, где-то его заменяет live coding. Спросите об этом рекрутера на первом же звонке.
| Этап | Что проверяют | Как готовиться |
|---|---|---|
| Скрининг с рекрутером | Мотивацию, ожидания, английский, соответствие резюме вакансии | Подготовьте рассказ о себе на 2–3 минуты и ответ на вопрос, почему фронтенд и почему эта компания |
| Техническое интервью | Теорию по HTML, CSS, JavaScript, браузеру и фреймворку из вакансии | Отвечайте на вопросы из этой статьи вслух: сначала короткий ответ, потом пример |
| Практическое задание или live coding | Умение писать код, читать чужой и объяснять свои решения | Решайте небольшие задачи с таймером и проговаривайте ход мысли |
| Финальная встреча | Как вы работаете в команде, реагируете на замечания и учитесь | Вспомните два-три примера из учебных проектов: что не получалось и как вы это решили |
Если вы только планируете путь в профессию, начните с материала Как стать Front-End разработчиком, а медианные зарплаты по уровням собраны на странице зарплаты Front-End разработчиков.
Какие темы проверяют на Junior и Middle?
От джуна ждут уверенной базы по HTML, CSS и JavaScript и рабочего знания одного фреймворка; от Middle — понимания того, почему всё устроено именно так, и умения выбирать между решениями. Таблица показывает, где проходит граница, без оценок частоты вопросов.
| Тема | Что проверяют | Уровень |
|---|---|---|
| HTML и доступность | Семантические теги, формы, управление с клавиатуры, текстовые альтернативы | Junior |
| CSS | Блочная модель, специфичность, Flexbox и Grid, позиционирование, адаптивность | Junior |
| Основы JavaScript | Типы и приведение, область видимости, замыкания, this, прототипы | Junior |
| Асинхронность | Промисы, async/await, порядок выполнения в цикле событий | Junior; детали очередей — Middle |
| DOM и браузер | События, делегирование, хранилища, CORS, путь от URL до пикселей | Junior; рендеринг в деталях — Middle |
| TypeScript | Типы и интерфейсы, unknown, сужение типов, дженерики | Junior — основы; Middle — дженерики и служебные типы |
| React | Компоненты, props и state, хуки, ключи, эффекты, повторный рендер | Junior; оптимизация рендера — Middle |
| Инструменты и производительность | Git, сборщик, Core Web Vitals | Junior — Git и общее понимание; Middle — настройка и измерения |
Вопросы по HTML и доступности
Что такое семантическая вёрстка и зачем она нужна?
Семантическая вёрстка — это выбор тегов по смыслу содержимого, а не по внешнему виду: <header>, <nav>, <main>, <article>, <section>, <aside>, <footer>, заголовки <h1>–<h6>, <button> для действий и <a> для переходов.
Она нужна по трём причинам. Программы экранного доступа строят по таким тегам карту страницы и позволяют прыгать между областями и заголовками. Поисковые системы лучше понимают структуру документа. И код читается проще: <nav> понятнее, чем <div class="menu-wrapper">. Список элементов с назначением каждого есть в справочнике MDN.
Типичная ошибка: выбирать уровень заголовка по размеру шрифта. Уровни задают структуру, а размер настраивается в CSS.
Чем кнопка button отличается от div с обработчиком клика?
<button> из коробки получает фокус с клавиатуры, срабатывает по Enter и пробелу, объявляется скринридером как кнопка и умеет быть отключённой через disabled. <div> ничего из этого не умеет: придётся вручную добавлять tabindex="0", role="button" и обработчики клавиш.
Правило простое: действие на странице — <button>, переход на другой адрес — <a href>. Отдельная деталь, которую любят уточнять: внутри формы кнопка без атрибута type имеет тип submit и отправляет форму, поэтому обычным кнопкам пишут type="button".
Как связать подпись с полем формы и какую проверку даёт сам HTML?
Подпись связывают с полем через пару for и id либо вкладывают поле внутрь <label>. Тогда клик по подписи ставит фокус в поле, а скринридер зачитывает подпись при переходе к нему. Атрибут placeholder подпись не заменяет: он исчезает при вводе.
<form action="/subscribe" method="post">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<button type="submit">Subscribe</button>
</form>
Встроенную проверку дают атрибуты required, type (email, url, number), min, max, minlength, maxlength и pattern; тип поля ещё и подсказывает мобильному браузеру нужную клавиатуру. Подробности — в руководстве MDN по валидации форм.
Что хотят услышать: проверка в браузере — это удобство для пользователя, а не защита. Данные всё равно проверяются на сервере.
Что такое доступность и что вы проверяете в первую очередь?
Доступность (accessibility, a11y) — это возможность пользоваться сайтом людям с разными ограничениями: без мыши, со скринридером, при слабом зрении, на маленьком экране. Требования описаны в стандарте WCAG 2.2.
Минимальная проверка, которую можно сделать самому:
- весь сценарий проходится с клавиатуры, и фокус всегда виден;
- у содержательных изображений есть
alt, у декоративных он пустой (alt=""); - у каждого поля формы есть подпись;
- контраст обычного текста и фона не ниже 4,5:1 — это уровень AA в WCAG;
- заголовки идут по порядку и отражают структуру страницы.
Об ARIA достаточно знать главное правило из руководства W3C: сначала нативный элемент HTML, а атрибуты role и aria-* — только когда подходящего элемента нет. Неправильная ARIA-разметка хуже, чем её отсутствие.
Вопросы по CSS
Что такое блочная модель и что меняет box-sizing: border-box?
Каждый элемент — это прямоугольник из четырёх слоёв: содержимое, внутренний отступ (padding), рамка (border) и внешний отступ (margin). По умолчанию действует box-sizing: content-box: свойство width задаёт ширину только содержимого, а padding и border прибавляются сверху. При border-box они входят в заданную ширину.
*,
*::before,
*::after {
box-sizing: border-box;
}
.card {
width: 300px;
padding: 20px;
border: 2px solid currentColor;
}
/* border-box: the card is 300px wide; content-box: 300 + 40 + 4 = 344px */
Стоит упомянуть и схлопывание отступов: вертикальные margin соседних блочных элементов не складываются, а берётся больший из них. Описание модели — в MDN: box-sizing.
Как считается специфичность селекторов?
Специфичность — это вес селектора из трёх чисел: количество идентификаторов, затем классов, атрибутов и псевдоклассов, затем тегов и псевдоэлементов. Сравнение идёт слева направо, поэтому один #id сильнее любого количества классов. При равном весе побеждает правило, которое объявлено ниже.
p { color: black; } /* 0-0-1 */
.note { color: blue; } /* 0-1-0 */
p.note:hover { color: green; } /* 0-2-1 */
#intro { color: red; } /* 1-0-0 */
Инлайновый атрибут style сильнее любого селектора, а !important сравнивается отдельно и выигрывает у обычных объявлений. Универсальный селектор и комбинаторы веса не добавляют, :where() всегда имеет нулевой вес, а :is() и :not() берут вес самого специфичного аргумента. Правила — в MDN: Specificity.
Что хотят услышать: войну специфичности не выигрывают через !important; стили держат плоскими — на классах, без идентификаторов и глубокой вложенности.
Чем Flexbox отличается от Grid и когда что выбирать?
Flexbox раскладывает элементы вдоль одной оси — в строку или в колонку, а Grid управляет строками и колонками одновременно. Flexbox удобен, когда размеры диктует содержимое: меню, панель кнопок, выравнивание иконки с текстом. Grid — когда сначала есть сетка, а содержимое в неё вписывается: макет страницы, галерея карточек.
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 24px;
}
Это не конкуренты: в одном макете Grid обычно задаёт каркас страницы, а Flexbox выравнивает содержимое внутри ячеек. Справочные материалы — MDN: Flexible box layout и MDN: Grid layout.
Какие значения есть у position и чем они отличаются?
Значений пять: static, relative, absolute, fixed и sticky.
static— значение по умолчанию: элемент в обычном потоке, свойстваtop,leftиz-indexна него не действуют.relative— элемент сдвигается относительно своего обычного места, а место за ним сохраняется.absolute— элемент выпадает из потока и отсчитывает координаты от ближайшего предка сposition, отличным отstatic.fixed— элемент привязан к окну браузера и не прокручивается вместе со страницей.sticky— элемент ведёт себя как обычный, пока не дойдёт до заданного отступа (например,top: 0), после чего «прилипает» в пределах своего контейнера прокрутки.
Типичная ошибка: ставить absolute и удивляться, что элемент улетел в угол страницы, — у родителя забыли указать position: relative. Полное описание — в MDN: position.
Как вы делаете адаптивную вёрстку?
Я начинаю со стилей для узкого экрана и расширяю их медиазапросами min-width (подход mobile-first), использую относительные единицы и раскладки, которые перестраиваются сами. В <head> обязательно стоит <meta name="viewport" content="width=device-width, initial-scale=1">, иначе мобильный браузер отрисует страницу в ширину настольного экрана и уменьшит её.
.layout {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) {
.layout {
grid-template-columns: 240px 1fr;
}
}
h1 {
font-size: clamp(1.75rem, 4vw, 3rem);
}
img {
max-width: 100%;
height: auto;
}
Дополнительные инструменты: rem вместо пикселей для шрифтов, функция clamp() для плавных размеров, контейнерные запросы (@container), когда компонент должен реагировать на ширину родителя, а не окна. Основа — MDN: Using media queries.
Вопросы по основам JavaScript
Какие типы данных есть в JavaScript?
В языке семь примитивных типов — string, number, bigint, boolean, undefined, null, symbol — и один тип для всего остального, object. Массивы, функции, даты — это объекты. Примитивы неизменяемы и сравниваются по значению, объекты — по ссылке.
console.log(typeof 42); // number
console.log(typeof 'text'); // string
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof 10n); // bigint
console.log(typeof Symbol('id')); // symbol
console.log(typeof null); // object
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof function () {}); // function
console.log(Array.isArray([])); // true
Что хотят услышать: typeof null возвращает 'object' — это историческая особенность языка, а не признак того, что null является объектом; массив проверяют через Array.isArray(). Таблица значений — в MDN: typeof.
Чем отличается == от ===?
Оператор === сравнивает без преобразований: если типы разные, результат сразу false. Оператор == сначала приводит операнды к общему типу, из-за чего результат бывает неочевидным. В рабочем коде используют ===.
console.log(1 == '1'); // true
console.log(1 === '1'); // false
console.log(0 == ''); // true
console.log(null == undefined); // true
console.log(null === undefined); // false
console.log(null == 0); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log('5' + 2); // 52
console.log('5' - 2); // 3
Две последние строки — про неявное приведение: + со строкой склеивает строки, а - работает только с числами и превращает строку в число. Единственное общепринятое исключение для == — проверка value == null, которая ловит сразу null и undefined. Алгоритмы сравнения описаны в MDN: Equality comparisons and sameness.
Чем отличаются var, let и const?
var видна во всей функции и её можно объявить повторно; let и const видны только внутри блока {}, где объявлены. const запрещает присваивать переменной новое значение, но не делает объект неизменяемым.
function demo() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1
console.log(typeof b); // undefined
}
demo();
const user = { name: 'Olena' };
user.name = 'Ira'; // allowed: the binding is constant, not the object
console.log(user.name); // Ira
try {
user = {};
} catch (e) {
console.log(e.name + ': ' + e.message); // TypeError: Assignment to constant variable.
}
Что хотят услышать: по умолчанию — const, let — когда значение действительно меняется, var в новом коде не используют.
Что такое hoisting (всплытие)?
Всплытие — это то, что объявления обрабатываются до выполнения кода в своей области видимости. Объявление функции доступно целиком ещё до строки, где оно написано. Переменная var существует с начала функции, но до присваивания равна undefined. Переменные let, const и классы тоже «подняты», но до строки объявления находятся во временной мёртвой зоне, и обращение к ним бросает ReferenceError.
console.log(a); // undefined
var a = 1;
console.log(sum(2, 3)); // 5
function sum(x, y) {
return x + y;
}
try {
console.log(b);
} catch (e) {
console.log(e.name + ': ' + e.message); // ReferenceError: Cannot access 'b' before initialization
}
let b = 2;
Функциональные выражения и стрелочные функции подчиняются правилам переменной, в которую записаны: вызвать их до объявления нельзя. Термин разобран в глоссарии MDN.
Что такое замыкание?
Замыкание — это функция вместе с переменными внешней области видимости, в которой она была создана. Функция сохраняет доступ к этим переменным, даже когда внешняя функция уже завершилась.
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const next = createCounter();
console.log(next()); // 1
console.log(next()); // 2
const other = createCounter();
console.log(other()); // 1
Переменная count недоступна снаружи, но живёт, пока жива функция next; у other — собственная копия. На замыканиях построены приватное состояние, обработчики событий, debounce и хуки React.
Классический уточняющий вопрос — что выведет цикл с таймером:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let:', j), 0);
}
var: 3
var: 3
var: 3
let: 0
let: 1
let: 2
С var переменная одна на весь цикл, и к моменту срабатывания таймеров она уже равна 3. С let на каждой итерации создаётся новая привязка, и каждая функция замыкает свою. Подробнее — в MDN: Closures.
Как определяется значение this?
У обычной функции this определяется тем, как она вызвана, а не где объявлена. Вызов как метода (user.hello()) — this равен объекту перед точкой. Вызов через call, apply или bind — тому, что передали явно. Вызов с new — новому объекту. Простой вызов fn() в строгом режиме — undefined. У стрелочной функции своего this нет: она берёт его из окружающего кода.
'use strict';
const user = {
name: 'Olena',
tags: ['js', 'css'],
hello() {
return 'Hi, ' + this.name;
},
describe() {
return this.tags.map((tag) => this.name + ':' + tag);
},
};
console.log(user.hello()); // Hi, Olena
const fn = user.hello;
try {
fn();
} catch (e) {
console.log(e.name); // TypeError
}
const bound = user.hello.bind(user);
console.log(bound()); // Hi, Olena
console.log(user.hello.call({ name: 'Ira' })); // Hi, Ira
console.log(user.describe()); // [ 'Olena:js', 'Olena:css' ]
Типичная ошибка: передать метод как колбэк (button.addEventListener('click', user.hello)) и потерять контекст. Решения — bind или стрелочная функция-обёртка. Все случаи перечислены в MDN: this.
Как работает прототипное наследование и что такое классы?
У каждого объекта есть скрытая ссылка на другой объект — прототип. Если свойства нет в самом объекте, движок ищет его в прототипе, затем в прототипе прототипа — и так до null. Классы — это более удобный синтаксис поверх того же механизма: методы класса лежат в prototype, а extends связывает прототипы в цепочку.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name + ' makes a sound';
}
}
class Dog extends Animal {
speak() {
return super.speak() + ': woof';
}
}
const dog = new Dog('Rex');
console.log(dog.speak()); // Rex makes a sound: woof
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.hasOwn(dog, 'name')); // true
console.log(Object.hasOwn(dog, 'speak')); // false
console.log(typeof Dog); // function
Свойство name принадлежит самому объекту, а метод speak найден по цепочке прототипов. Подробно — в MDN: Inheritance and the prototype chain.
Чем поверхностная копия объекта отличается от глубокой?
Поверхностная копия ({ ...obj }, Object.assign, [...arr]) копирует только верхний уровень: вложенные объекты остаются общими с оригиналом. Глубокая копия дублирует всю структуру; встроенный способ — structuredClone().
const original = { name: 'Olena', tags: ['js'], address: { city: 'Kyiv' } };
const shallow = { ...original };
shallow.address.city = 'Lviv';
console.log(original.address.city); // Lviv
const deep = structuredClone(original);
deep.address.city = 'Odesa';
deep.tags.push('ts');
console.log(original.address.city); // Lviv
console.log(original.tags); // [ 'js' ]
try {
structuredClone({ run() {} });
} catch (e) {
console.log(e.name); // DataCloneError
}
structuredClone понимает даты, Map, Set и циклические ссылки, но не копирует функции — на них он бросает ошибку. Вопрос важен для React: состояние нельзя менять на месте, его заменяют копией. Описание — в MDN: structuredClone().
Вопросы по асинхронному JavaScript
Что такое event loop (цикл событий)?
JavaScript выполняет код в одном потоке: в каждый момент работает одна функция из стека вызовов. Цикл событий — это механизм, который, когда стек пуст, берёт следующую задачу из очереди и запускает её. Благодаря ему таймеры, сетевые запросы и события пользователя не блокируют страницу: ожидание происходит вне потока JavaScript, а в очередь попадает только колбэк.
Один оборот цикла в браузере выглядит так:
- взять из очереди одну задачу (скрипт, колбэк таймера, обработчик события) и выполнить её до конца;
- выполнить все накопившиеся микрозадачи, включая те, что добавились в процессе;
- при необходимости перерисовать страницу;
- перейти к следующей задаче.
Что хотят услышать: долгая синхронная операция блокирует всё — и обработку кликов, и отрисовку, потому что цикл не может перейти к следующему шагу. Модель описана в стандарте HTML.
Чем микрозадачи отличаются от макрозадач?
Макрозадачи (в стандарте — просто задачи) — это колбэки setTimeout и setInterval, обработчики событий, сетевые колбэки. Микрозадачи — обработчики промисов (then, catch, finally), продолжение функции после await и queueMicrotask(). После каждой макрозадачи очередь микрозадач выполняется целиком, поэтому промис всегда обгоняет setTimeout(fn, 0).
console.log('1: sync start');
setTimeout(() => console.log('2: timeout (macrotask)'), 0);
Promise.resolve().then(() => console.log('3: promise (microtask)'));
queueMicrotask(() => console.log('4: queueMicrotask (microtask)'));
console.log('5: sync end');
1: sync start
5: sync end
3: promise (microtask)
4: queueMicrotask (microtask)
2: timeout (macrotask)
Второй пример показывает, что микрозадачи выполняются между двумя таймерами, а не после всех:
setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => console.log('microtask inside timeout 1'));
}, 0);
setTimeout(() => console.log('timeout 2'), 0);
Promise.resolve()
.then(() => console.log('microtask 1'))
.then(() => console.log('microtask 2'));
console.log('sync');
sync
microtask 1
microtask 2
timeout 1
microtask inside timeout 1
timeout 2
Разбор очередей — в MDN: Using microtasks.
Что такое промис и какие у него состояния?
Промис — это объект, который представляет результат асинхронной операции. У него три состояния: ожидание (pending), выполнен (fulfilled) и отклонён (rejected). Перейти из ожидания можно только один раз, после этого состояние и значение не меняются.
Для нескольких промисов есть четыре статических метода, и разницу между ними спрашивают часто:
const ok = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (message, ms) => new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
async function main() {
console.log(await Promise.all([ok('a', 20), ok('b', 10)])); // [ 'a', 'b' ]
try {
await Promise.all([ok('a', 20), fail('boom', 10)]);
} catch (e) {
console.log('all rejected:', e.message); // all rejected: boom
}
const settled = await Promise.allSettled([ok('a', 20), fail('boom', 10)]);
console.log(settled.map((r) => r.status)); // [ 'fulfilled', 'rejected' ]
console.log(await Promise.race([ok('slow', 30), ok('fast', 10)])); // fast
console.log(await Promise.any([fail('boom', 10), ok('first success', 30)])); // first success
}
main();
Promise.all— ждёт все и падает при первой ошибке; результаты идут в порядке исходного массива, а не в порядке завершения.Promise.allSettled— ждёт все и никогда не падает, возвращает статус каждого.Promise.race— возвращает первый завершившийся, успешно или с ошибкой.Promise.any— возвращает первый успешный.
Справочник — MDN: Promise.
Как работает async/await и как обрабатывать в нём ошибки?
Функция с async всегда возвращает промис, а await приостанавливает её до завершения промиса, не блокируя поток: остальной код продолжает выполняться. Ошибку отклонённого промиса ловят обычным try/catch.
const load = (name, ms) => new Promise((resolve) => setTimeout(() => resolve(name), ms));
async function sequential() {
const start = Date.now();
const user = await load('user', 100);
const posts = await load('posts', 100);
return [user, posts, Math.round((Date.now() - start) / 100) * 100];
}
async function parallel() {
const start = Date.now();
const [user, posts] = await Promise.all([load('user', 100), load('posts', 100)]);
return [user, posts, Math.round((Date.now() - start) / 100) * 100];
}
async function withError() {
try {
await Promise.reject(new Error('network error'));
} catch (e) {
return 'caught: ' + e.message;
}
}
async function main() {
console.log(await sequential()); // [ 'user', 'posts', 200 ]
console.log(await parallel()); // [ 'user', 'posts', 100 ]
console.log(await withError()); // caught: network error
}
main();
Третье число — время выполнения в миллисекундах, округлённое до сотен.
Типичная ошибка: ставить await подряд для независимых запросов. Они выполняются по очереди и занимают вдвое больше времени; независимые операции запускают вместе через Promise.all. Справочник — MDN: async function.
Те же темы — с тренером и code review
Курс Front-End в Prog Academy состоит из модулей AI Start, «Верстка HTML/CSS» и JavaScript и длится 4,5 месяца. В программе — Flexbox и Grid, прототипы, события и DOM, Promise и Fetch, React и/или Vue.js, сборка проектов и дипломный проект для портфолио с code review от преподавателя.
Программа курса Front-End →Вопросы по DOM и браузеру
Что такое всплытие и перехват событий?
Событие в DOM проходит три фазы: перехват (от window вниз к элементу), цель (сам элемент) и всплытие (от элемента обратно вверх по предкам). По умолчанию обработчики, добавленные через addEventListener, срабатывают на фазе всплытия; чтобы поймать событие на перехвате, передают третьим аргументом { capture: true }.
Что обычно уточняют:
event.target— элемент, на котором событие произошло;event.currentTarget— элемент, на котором висит выполняющийся обработчик;event.stopPropagation()останавливает дальнейшее распространение, аevent.preventDefault()отменяет действие браузера по умолчанию — переход по ссылке или отправку формы. Это разные вещи;- всплывают не все события:
focusиblurне всплывают, для них есть всплывающие аналогиfocusinиfocusout.
Наглядное объяснение — в MDN: Event bubbling.
Что такое делегирование событий?
Делегирование — это один обработчик на общем предке вместо отдельного обработчика на каждом дочернем элементе. Работает благодаря всплытию: клик по кнопке внутри списка дойдёт до самого списка, а нужный элемент находят через event.target.
const list = document.querySelector('#todo-list');
list.addEventListener('click', (event) => {
const button = event.target.closest('button[data-action="remove"]');
if (!button || !list.contains(button)) return;
button.closest('li').remove();
});
Плюсов два: меньше обработчиков в памяти и автоматическая работа с элементами, которые добавили в список позже. Метод closest() нужен, потому что клик может прийтись на иконку внутри кнопки, а не на саму кнопку.
Чем отличаются localStorage, sessionStorage и cookie?
Все три хранят строки в браузере, но живут и передаются по-разному. localStorage хранит данные без срока, пока их не удалят. sessionStorage живёт до закрытия вкладки и у каждой вкладки свой. Cookie имеют срок жизни и, в отличие от хранилищ, автоматически отправляются на сервер с каждым запросом.
| Свойство | localStorage | sessionStorage | Cookie |
|---|---|---|---|
| Срок жизни | Пока не удалят | До закрытия вкладки | Задаётся через Expires или Max-Age; без них — до конца сессии браузера |
| Отправка на сервер | Нет | Нет | Автоматически с каждым запросом |
| Доступ из JavaScript | Да | Да | Да, если не выставлен флаг HttpOnly |
| Объём | Несколько мегабайт на источник, зависит от браузера | Несколько мегабайт на источник, зависит от браузера | Около 4 КБ на одну cookie |
Что хотят услышать: всё, что лежит в localStorage, может прочитать любой скрипт на странице, поэтому при XSS-уязвимости токен оттуда украдут. Cookie с флагами HttpOnly, Secure и SameSite из JavaScript недоступна. Описание API — MDN: Web Storage API.
Что такое CORS и почему браузер блокирует запрос?
По правилу одного источника (same-origin policy) скрипт страницы не может читать ответы с другого источника, а источник — это сочетание протокола, домена и порта. CORS — механизм, которым сервер разрешает такие запросы: он добавляет в ответ заголовок Access-Control-Allow-Origin с адресом страницы, и браузер отдаёт ответ скрипту.
Для «непростых» запросов — например, с методом PUT или DELETE, с телом в формате JSON или с нестандартными заголовками — браузер сначала отправляет предварительный запрос OPTIONS (preflight) и только при разрешающем ответе делает основной.
Что хотят услышать: CORS — это ограничение браузера, а не сервера: тот же запрос из curl или с другого сервера пройдёт. Поэтому ошибку CORS исправляют настройкой сервера или прокси, а не кодом на фронтенде. Детали — в MDN: Cross-Origin Resource Sharing.
Что происходит после того, как пользователь ввёл адрес и нажал Enter?
Браузер находит IP-адрес сервера через DNS, устанавливает соединение (TCP и TLS для HTTPS), отправляет HTTP-запрос и получает HTML. Дальше начинается отрисовка:
- из HTML строится дерево DOM, из CSS — дерево стилей CSSOM;
- из них собирается дерево рендеринга — только видимые элементы с вычисленными стилями;
- layout — вычисляются размеры и положение каждого элемента;
- paint — элементы отрисовываются в слои;
- composite — слои собираются в итоговую картинку на экране.
CSS блокирует отрисовку: пока стили не загружены, страницу не показывают. Обычный <script> блокирует разбор HTML, поэтому скрипты подключают с defer (выполняются после разбора документа, по порядку) или async (выполняются сразу после загрузки, в произвольном порядке); скрипты с type="module" отложены по умолчанию. Подробнее — web.dev: Critical rendering path.
Вопросы по TypeScript и React
Чем type отличается от interface в TypeScript?
Оба способа описывают форму объекта, и в большинстве случаев они взаимозаменяемы. Разница в возможностях: interface расширяется через extends и допускает слияние объявлений с одним именем, а type умеет то, чего интерфейс не может, — объединения, пересечения, псевдонимы примитивов и кортежей.
interface User {
id: number;
name: string;
email?: string;
}
interface Admin extends User {
permissions: string[];
}
type Status = 'idle' | 'loading' | 'error';
type WithTimestamps<T> = T & { createdAt: Date };
const admin: Admin = { id: 1, name: 'Olena', permissions: ['users:edit'] };
const status: Status = 'loading';
const stored: WithTimestamps<User> = { id: 2, name: 'Ira', createdAt: new Date(0) };
console.log(admin.permissions.length, status, stored.createdAt.getTime()); // 1 loading 0
Что хотят услышать: главное — единообразие в проекте. Рекомендация из TypeScript Handbook — использовать interface, пока не понадобятся возможности type. И стоит добавить, что типы существуют только на этапе компиляции: в итоговом JavaScript их нет, поэтому данные с сервера нужно проверять отдельно.
Чем any отличается от unknown и зачем нужен never?
any отключает проверку типов: со значением можно делать что угодно, и ошибка проявится только при выполнении. unknown тоже принимает любое значение, но не даёт ничего с ним сделать, пока вы не сузите тип проверкой. never — тип, у которого нет значений: его возвращают функции, которые всегда бросают ошибку, и им проверяют, что в switch разобраны все варианты.
function parse(json: string): unknown {
return JSON.parse(json);
}
const data = parse('{"name":"Olena"}');
// console.log(data.name); // error TS18046: 'data' is of type 'unknown'.
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log(data.name); // Olena
}
function assertNever(value: never): never {
throw new Error('Unexpected value: ' + String(value));
}
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
default:
return assertNever(shape);
}
}
console.log(area({ kind: 'square', side: 3 })); // 9
Если в Shape добавить третий вариант и забыть про него в switch, компилятор укажет на строку с assertNever. Сужение типов описано в TypeScript Handbook: Narrowing.
Что такое дженерики и зачем они нужны?
Дженерик — это параметр типа: функция или тип описываются один раз, а конкретный тип подставляется при использовании. Так код остаётся универсальным и при этом типобезопасным, без any.
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(['a', 'b']); // string | undefined
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const users = [
{ id: 1, name: 'Olena' },
{ id: 2, name: 'Ira' },
];
console.log(n, s, pluck(users, 'name')); // 1 a [ 'Olena', 'Ira' ]
// pluck(users, 'age'); // error TS2345: Argument of type '"age"' is not assignable to parameter of type '"name" | "id"'.
Запись K extends keyof T — ограничение: вторым аргументом можно передать только имя существующего свойства. От джуна обычно достаточно умения читать такие сигнатуры; писать сложные — уровень Middle. Раздел справочника — TypeScript Handbook: Generics.
Чем props отличаются от state в React?
Props — это данные, которые компонент получает от родителя; менять их внутри компонента нельзя. State — это собственные данные компонента, которые сохраняются между рендерами; их меняют только функцией-сеттером, и это вызывает новый рендер.
import { useState } from 'react';
function Counter({ step = 1 }) {
const [count, setCount] = useState(0);
return (
<button type="button" onClick={() => setCount((current) => current + step)}>
Clicked: {count}
</button>
);
}
Здесь step — props, count — state. Два уточнения, которые стоит назвать самому. Состояние не меняют на месте (items.push(...)), а заменяют новым значением, иначе React не увидит изменения. И значение состояния внутри одного рендера не меняется сразу после вызова сеттера, поэтому, когда новое значение зависит от предыдущего, в сеттер передают функцию. Объяснение — в react.dev: State: A Component's Memory.
Зачем нужны ключи (key) в списках?
Ключ позволяет React понять, какой элемент списка какому соответствует между рендерами: что добавили, что удалили, что переставили. Ключ должен быть стабильным и уникальным среди соседей — обычно это идентификатор из данных.
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Типичная ошибка: использовать индекс массива как ключ. Если элементы вставляются, удаляются или сортируются, индексы сдвигаются, и состояние компонента — например, текст в поле ввода — оказывается у другого элемента. Ключ из Math.random() ещё хуже: элементы пересоздаются при каждом рендере. Правила — в react.dev: Rendering Lists.
Как работает useEffect и зачем нужна функция очистки?
useEffect синхронизирует компонент с чем-то внешним по отношению к React: подпиской, таймером, сетевым соединением, сторонним виджетом. Эффект запускается после того, как React обновил экран. Массив зависимостей определяет, когда запускать его снова: без массива — после каждого рендера, с пустым массивом — один раз после появления компонента, с перечнем значений — когда хотя бы одно из них изменилось.
import { useEffect, useState } from 'react';
function OnlineStatus() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
const update = () => setOnline(navigator.onLine);
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return <p>{online ? 'Online' : 'Offline'}</p>;
}
Функция, которую возвращает эффект, — очистка: React вызывает её перед повторным запуском эффекта и при удалении компонента. Без неё остаются «висящие» подписки и таймеры. В режиме разработки со Strict Mode React один раз дополнительно запускает эффект и очистку, чтобы такие ошибки были заметны.
Что хотят услышать: эффект не нужен, чтобы вычислить что-то из props или state, — это считают прямо во время рендера. Справочник — react.dev: useEffect.
Когда компонент React рендерится заново?
Причин три: изменилось состояние самого компонента, заново отрендерился его родитель или изменилось значение контекста, который компонент читает. Важно: при рендере родителя дочерние компоненты рендерятся по умолчанию, даже если их props не изменились.
Рендер — это вызов функции компонента, а не обновление страницы. После него React сравнивает результат с предыдущим и вносит в DOM только различия, поэтому «лишний» рендер не всегда проблема. Если он стал дорогим, есть инструменты: memo пропускает рендер компонента при тех же props, useMemo запоминает результат вычисления, useCallback — саму функцию. Проекты с React Compiler получают такую мемоизацию автоматически на этапе сборки.
Что хотят услышать: сначала измерить в React DevTools Profiler, потом оптимизировать. Процесс описан в react.dev: Render and Commit.
Какие правила есть у хуков и почему?
Правил два: хуки вызывают только на верхнем уровне компонента или собственного хука — не в условиях, циклах и вложенных функциях, — и только из React-функций, а не из обычных. Причина в том, что React сопоставляет состояние с хуком по порядку вызовов: если на одном рендере хук вызван, а на другом пропущен, значения сдвинутся.
Исключение в современном React — функция use: её можно вызывать внутри условий и циклов. Собственный хук — это обычная функция с именем на use, которая вызывает другие хуки; так выносят повторяющуюся логику вроде подписки из примера выше. Соблюдение правил проверяет линтер — плагин eslint-plugin-react-hooks. Источник — react.dev: Rules of Hooks.
Вопросы по инструментам и производительности
Зачем нужен сборщик и что он делает?
Сборщик превращает исходный код проекта в файлы, которые быстро загружаются в браузере. Он проходит по графу импортов, преобразует TypeScript и JSX в JavaScript, объединяет модули, удаляет неиспользуемый код (tree shaking), минифицирует результат и добавляет к именам файлов хеш содержимого, чтобы браузер мог долго их кешировать.
Ещё две задачи: разделение кода (code splitting) — страница загружает только то, что ей нужно сейчас, а остальное подгружается по требованию через динамический import(), — и сервер разработки с мгновенным обновлением модулей без перезагрузки страницы. Самые известные сборщики — Vite и webpack. Мотивация хорошо описана в документации Vite.
Что такое Core Web Vitals?
Core Web Vitals — это три метрики Google, которые описывают, как пользователь ощущает скорость страницы:
| Метрика | Что измеряет | Хорошее значение |
|---|---|---|
| LCP (Largest Contentful Paint) | Когда отрисован самый крупный элемент в видимой области — скорость загрузки | До 2,5 секунды |
| INP (Interaction to Next Paint) | Сколько проходит от действия пользователя до следующей отрисовки — отзывчивость | До 200 миллисекунд |
| CLS (Cumulative Layout Shift) | Насколько элементы сдвигаются во время загрузки — визуальная стабильность | До 0,1 |
Пороги оцениваются по 75-му перцентилю загрузок страницы. Что помогает: сжатые изображения в современных форматах и приоритет для главного изображения (LCP), короткие обработчики и разбиение долгих задач (INP), атрибуты width и height у изображений и зарезервированное место под баннеры (CLS). Определения и пороги — на web.dev: Web Vitals.
Чем git merge отличается от git rebase?
git merge соединяет две ветки и, если истории разошлись, создаёт отдельный коммит слияния: история сохраняется такой, какой была. git rebase переносит ваши коммиты поверх другой ветки, создавая их заново с новыми хешами: история получается линейной, но переписанной.
Что хотят услышать: правило из книги Pro Git — не делать rebase коммитов, которые уже отправлены в общий репозиторий и на которых кто-то мог основать работу. Свою локальную ветку перед pull request перебазировать можно, общую — нет. Объяснение с рисунками — в Pro Git: Rebasing, а базовые команды разобраны в статье Git для начинающих.
Практические задачи с решениями
На практической части джуну обычно дают небольшие задачи на функции, массивы и асинхронность — такие, которые решаются за 10–15 минут. Ниже четыре типовые задачи; все решения запущены в Node.js v22.14.0.
Задача 1. Напишите функцию debounce
Нужно вернуть функцию, которая вызывает исходную только после того, как вызовы прекратились на заданное время. Так делают поиск по мере ввода: запрос уходит, когда пользователь перестал печатать.
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce((query) => console.log('search:', query), 100);
search('r');
search('re');
search('react'); // only this call reaches the function
search: react
Что стоит проговорить: timerId живёт в замыкании, каждый новый вызов отменяет предыдущий таймер, а fn.apply(this, args) сохраняет контекст и аргументы.
Задача 2. Разверните вложенный массив без метода flat
function flatten(items) {
const result = [];
for (const item of items) {
if (Array.isArray(item)) {
result.push(...flatten(item));
} else {
result.push(item);
}
}
return result;
}
console.log(flatten([1, [2, [3, [4]], 5]])); // [ 1, 2, 3, 4, 5 ]
console.log([1, [2, [3, [4]], 5]].flat(Infinity)); // [ 1, 2, 3, 4, 5 ]
Решение рекурсивное: если элемент — массив, функция вызывает сама себя. После ручного решения назовите встроенный способ — flat(Infinity): интервьюеру важно, что вы знаете оба.
Задача 3. Сгруппируйте массив объектов по полю
function groupBy(items, getKey) {
return items.reduce((groups, item) => {
const key = getKey(item);
(groups[key] ??= []).push(item);
return groups;
}, {});
}
const users = [
{ name: 'Olena', role: 'dev' },
{ name: 'Ira', role: 'qa' },
{ name: 'Taras', role: 'dev' },
];
console.log(groupBy(users, (user) => user.role));
{
dev: [ { name: 'Olena', role: 'dev' }, { name: 'Taras', role: 'dev' } ],
qa: [ { name: 'Ira', role: 'qa' } ]
}
Оператор ??= создаёт массив для ключа, только если его ещё нет. В современных средах есть встроенный Object.groupBy() из ES2024 — упомяните его после своего решения.
Задача 4. Что выведет этот код и в каком порядке?
async function first() {
console.log('A');
await second();
console.log('B');
}
async function second() {
console.log('C');
}
console.log('D');
setTimeout(() => console.log('E'), 0);
first();
Promise.resolve().then(() => console.log('F'));
console.log('G');
D
A
C
G
B
F
E
Объяснение по шагам. D — синхронный код. Вызов first() выполняется синхронно до первого await, поэтому сразу печатаются A и C. Остаток first после await уходит в очередь микрозадач, следом туда же попадает обработчик then. Синхронный код заканчивается строкой G, затем по порядку выполняются микрозадачи — B и F, и только потом макрозадача таймера — E.
Типичные ошибки кандидатов
Большинство отказов на уровне Junior связано не с пробелом в одной теме, а с тем, как кандидат отвечает и рассуждает. Это наблюдения автора, а не статистика:
- Заученные определения без примера. «Замыкание — это функция и её лексическое окружение» ничего не доказывает, пока вы не покажете счётчик или
debounce. - Фреймворк вместо языка. Знание хуков React не спасает, если непонятно, как работает
thisили цикл событий. - Молчание во время live coding. Интервьюер оценивает рассуждение; тишину он оценить не может.
- Решение без уточнений. Что на входе? Могут ли быть пустые значения? Задать два вопроса перед началом — признак опыта, а не слабости.
- Выдуманный ответ вместо «не знаю». Следующий уточняющий вопрос это покажет.
- Проект, который кандидат не может объяснить. Особенно если код писал AI-ассистент, а вы его не разбирали.
- Игнорирование HTML и доступности. «Я всё делаю на div» — быстрый способ потерять очки на первых же вопросах.
- Отсутствие вопросов к интервьюеру. Это читается как отсутствие интереса к работе.
Какие вопросы задать интервьюеру?
Собеседование — двусторонний разговор: вам тоже нужно понять, как здесь работают и будете ли вы расти. Выберите три-четыре вопроса из списка:
- Над каким продуктом или проектом я буду работать и какой у него стек?
- Как устроена команда: сколько фронтенд-разработчиков, кто будет моим наставником?
- Как проходит code review и сколько времени обычно занимает?
- Какими будут мои задачи в первый месяц?
- Как выглядит процесс от задачи до релиза: ветки, тесты, деплой?
- Как в команде относятся к использованию AI-ассистентов в работе?
- По каким критериям оценивают результат испытательного срока?
- Каким будет следующий этап отбора и когда ждать ответа?
План подготовки на две недели
План рассчитан на тех, кто уже изучил основы и имеет хотя бы один свой проект: по полтора-два часа в день на повторение и тренировку ответов вслух. Если какая-то тема для вас новая, две недели на неё не хватит — сначала выучите её по плану из статьи Как стать Front-End разработчиком.
| Дни | Тема | Что сделать |
|---|---|---|
| 1–2 | HTML и доступность | Сверстать форму с подписями и проверкой, пройти её с клавиатуры, ответить вслух на вопросы блока |
| 3–4 | CSS | Собрать макет на Grid и Flexbox с одной контрольной точкой, объяснить вес трёх своих селекторов |
| 5–7 | Основы JavaScript | Запустить каждый пример из блока, изменить его и предсказать вывод до запуска |
| 8–9 | Асинхронность | Нарисовать схему цикла событий, решить пять задач «что выведет код» |
| 10 | DOM и браузер | Написать список с делегированием событий, рассказать путь от URL до отрисовки |
| 11 | TypeScript | Типизировать функции из своего проекта, заменить каждый any на точный тип или unknown |
| 12 | React | Объяснить вслух каждый useEffect в своём проекте: зачем он, какие зависимости, нужна ли очистка |
| 13 | Практика и инструменты | Решить задачи из раздела выше с таймером; повторить Git: ветки, конфликт, rebase |
| 14 | Пробное собеседование | Попросить знакомого задать вопросы или записать ответы на диктофон; подготовить рассказ о себе и проекте |
Для ежедневной практики с автоматической проверкой заданий подойдут бесплатный курс по JavaScript и бесплатный курс по Git. Если вы ещё сомневаетесь в направлении, посмотрите сравнение Frontend или Backend: что выбрать.
Частые вопросы
Сколько времени нужно на подготовку к собеседованию на Junior Front-End?
Если основы уже изучены и есть свои проекты, на повторение теории и тренировку ответов вслух обычно хватает двух недель по полтора-два часа в день. Это ориентир автора, а не норма: если тема из списка вопросов для вас новая, её сначала нужно выучить и закрепить практикой, и на это уходят недели, а не вечера.
Будет ли на собеседовании live coding?
Скорее всего, да, в том или ином виде: написать небольшую функцию, предсказать вывод кода или поправить вёрстку, комментируя свои действия. Формат зависит от компании, поэтому спросите о нём рекрутера заранее. Оценивают не скорость набора, а ход рассуждений: проговаривайте, что делаете и почему.
Нужен ли английский на собеседовании Front-End разработчика?
Читать по-английски нужно в любом случае: документация, сообщения об ошибках и названия в коде написаны на нём. Разговорный английский проверяют, если команда или заказчик общаются на нём, и требуемый уровень обычно указан в вакансии. Часть собеседования могут провести на английском, поэтому подготовьте рассказ о себе и о своём проекте на двух языках.
Можно ли пользоваться AI-ассистентами на собеседовании и в тестовом задании?
На собеседовании — только если интервьюер прямо разрешил; если правила не названы, спросите. В тестовом задании ассистенты обычно допустимы, но за каждую строку отвечаете вы: на разборе попросят объяснить решение и изменить его на ходу. Код, который вы не можете объяснить, вредит сильнее, чем его отсутствие.
Что делать, если не знаешь ответа на вопрос?
Сказать об этом прямо и показать, как вы рассуждаете: что знаете по соседней теме, как проверили бы предположение, где посмотрели бы в документации. Выдумывать ответ хуже, чем признаться: следующий уточняющий вопрос это покажет. После собеседования запишите вопрос и разберите его — он встретится снова.
Спрашивают ли алгоритмы на собеседовании Junior Front-End?
Сложные алгоритмы от джуна во фронтенде требуют редко, но базовые задачи на массивы, строки и объекты встречаются часто: сгруппировать, отфильтровать, развернуть вложенный массив, убрать дубликаты. Нужно уверенно владеть методами массивов и понимать, сколько раз ваш код проходит по данным.
Как выполнять тестовое задание?
Сначала сделайте то, что написано в условии, и только потом улучшения. Добавьте README с инструкцией запуска и списком принятых решений, проверьте вёрстку на мобильной ширине и управление с клавиатуры, делайте осмысленные коммиты. Если срок или условие непонятны, задайте вопрос до начала работы — это нормальная рабочая практика.
Нужно ли знать React, чтобы пройти собеседование на Junior?
Смотрите на вакансию: если в ней указан React, вопросы о компонентах, состоянии и хуках будут обязательно. Но фреймворк не заменяет основ: кандидата, который пишет на React и не может объяснить замыкание или цикл событий, отсеивают на блоке по JavaScript.
Что рассказывать о себе, если коммерческого опыта нет?
Расскажите о проектах: какую задачу решали, какие технологии выбрали и почему, что было самым трудным и что вы переделали бы сейчас. Двух-трёх минут достаточно. Не выдавайте учебные проекты за коммерческие — это выясняется за пару уточняющих вопросов.
Источники
- WHATWG: HTML Standard — Event loops
- MDN: HTML elements reference
- MDN: Client-side form validation
- W3C: Web Content Accessibility Guidelines (WCAG) 2.2
- W3C WAI: ARIA Authoring Practices Guide — Read Me First
- MDN: Specificity
- MDN: position
- MDN: Equality comparisons and sameness
- MDN: Closures
- MDN: this
- MDN: Inheritance and the prototype chain
- MDN: Using microtasks in JavaScript with queueMicrotask()
- MDN: Promise
- MDN: Web Storage API
- MDN: Cross-Origin Resource Sharing (CORS)
- web.dev: Critical rendering path
- web.dev: Web Vitals
- TypeScript Handbook: Everyday Types
- TypeScript Handbook: Narrowing
- TypeScript Handbook: Generics
- react.dev: Rendering Lists
- react.dev: useEffect
- react.dev: Render and Commit
- react.dev: Rules of Hooks
- Vite: Why Vite
- Scott Chacon, Ben Straub: Pro Git — Rebasing
