Prog Academy
RU UA
Вопросы на собеседовании Front-End разработчика: 37 вопросов с ответами
Карьера в IT

Вопросы на собеседовании Front-End разработчика: 37 вопросов с ответами

Короткий ответ: на собеседовании Junior Front-End разработчика проверяют не знание редких фактов, а понимание основ — семантики HTML, блочной модели и раскладок CSS, типов, замыканий и this в JavaScript, цикла событий, работы браузера и базовых вещей в TypeScript и React. Ниже — 37 вопросов в формулировках, в которых их задают, с короткими ответами: сначала то, что стоит сказать вслух, затем детали и код. Вопросы сгруппированы в семь блоков; после них — практические задачи с решениями, типичные ошибки и план подготовки на две недели.

Я во фронтенд-разработке с 2014 года и больше девяти лет преподаю. По моим наблюдениям, начинающие чаще спотыкаются не на экзотике, а на базе: могут собрать приложение на React, но не могут объяснить, почему setTimeout с нулевой задержкой срабатывает позже промиса. Поэтому в подборке больше всего места отдано JavaScript, а каждый пример кода можно запустить и проверить самому.

Актуально на октябрь 2026 года

Все примеры на 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 VitalsJunior — 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, а в очередь попадает только колбэк.

Один оборот цикла в браузере выглядит так:

  1. взять из очереди одну задачу (скрипт, колбэк таймера, обработчик события) и выполнить её до конца;
  2. выполнить все накопившиеся микрозадачи, включая те, что добавились в процессе;
  3. при необходимости перерисовать страницу;
  4. перейти к следующей задаче.

Что хотят услышать: долгая синхронная операция блокирует всё — и обработку кликов, и отрисовку, потому что цикл не может перейти к следующему шагу. Модель описана в стандарте 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.

FE

Те же темы — с тренером и 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 имеют срок жизни и, в отличие от хранилищ, автоматически отправляются на сервер с каждым запросом.

СвойствоlocalStoragesessionStorageCookie
Срок жизниПока не удалятДо закрытия вкладкиЗадаётся через 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. Дальше начинается отрисовка:

  1. из HTML строится дерево DOM, из CSS — дерево стилей CSSOM;
  2. из них собирается дерево рендеринга — только видимые элементы с вычисленными стилями;
  3. layout — вычисляются размеры и положение каждого элемента;
  4. paint — элементы отрисовываются в слои;
  5. 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–2HTML и доступностьСверстать форму с подписями и проверкой, пройти её с клавиатуры, ответить вслух на вопросы блока
3–4CSSСобрать макет на Grid и Flexbox с одной контрольной точкой, объяснить вес трёх своих селекторов
5–7Основы JavaScriptЗапустить каждый пример из блока, изменить его и предсказать вывод до запуска
8–9АсинхронностьНарисовать схему цикла событий, решить пять задач «что выведет код»
10DOM и браузерНаписать список с делегированием событий, рассказать путь от URL до отрисовки
11TypeScriptТипизировать функции из своего проекта, заменить каждый any на точный тип или unknown
12ReactОбъяснить вслух каждый 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.

Что рассказывать о себе, если коммерческого опыта нет?

Расскажите о проектах: какую задачу решали, какие технологии выбрали и почему, что было самым трудным и что вы переделали бы сейчас. Двух-трёх минут достаточно. Не выдавайте учебные проекты за коммерческие — это выясняется за пару уточняющих вопросов.

Источники

Курс Front-End в Prog Academy →

Фото автора: Максим Сластен

Об авторе

Front-End разработчик в DataScope Systems, тренер Prog Academy

Front-End разработчик в DataScope Systems, во фронтенд-разработке с 2014 года. Преподаёт более 9 лет, автор всех курсов Prog Academy по Front-End; в блоге пишет о фронтенде, JavaScript и TypeScript.

Контакт

Записаться на консультацию

Укажите актуальный номер, мы позвоним в любую страну :)