Перейти к основному содержимому

РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ НА ДЖУНА + LIVE CODING | FRONTEND РАЗРАБОТЧИК

· 44 мин. чтения

Сегодня мы разберём реальное собеседование на позицию младшего фронтенд-разработчика с кандидатом Михаилом — молодым специалистом из Казани, который уже прошёл стажировку в аутсорсинговой компании и месяц работает как junior, но пришёл проверить свои силы и получить обратную связь. Интервьюеры Евгений и Максим проверяют его знания по HTML/CSS, JavaScript и React, а затем дают практическую задачу с debounce и фильтрацией списка на чистом JS. В итоге кандидат демонстрирует уверенную коммуникацию и базовое понимание технологий, однако глубина знаний и точность формулировок оставляют желать лучшего, что даёт ему конкретные точки роста для дальнейшего развития.

Вопрос 1. Какова цель участия в собеседовании и текущий статус трудоустройства?

Таймкод: 00:01:53

Ответ собеседника: Правильный. Цель — проверить свои навыки после года учёбы и прохождения стажировки. Работу уже нашёл, трудоустроен уже месяц как фронтенд-разработчик.

Правильный ответ:

Вопрос является организационным и не имеет технически правильного или неправильного ответа. Кандидат честно описал свою ситуацию: он проходит собеседование для самопроверки навыков, а на данный момент уже работает фронтенд-разработчиком. Ответ адекватный и полный.

Вопрос 2. Как давно работаешь и где проходил стажировку?

Таймкод: 00:02:27

Ответ собеседника: Правильный. Работает месяц. Три месяца назад попал на стажировку в аутсорсинговую компанию Outff, прошёл её и был трудоустроен.

Правильный ответ:

Вопрос является организационным и уточняющим. Кандидат предоставил полную информацию о своём опыте: стажировка в аутсорсинговой компании, длительность три месяца, после чего последовало трудоустройство. Ответ адекватный и исчерпывающий для организационного вопроса.

Вопрос 3. Какой грейд и примерная зарплата?

Таймкод: 00:03:01

Ответ собеседника: Правильный. Считает себя младшим фронтенд-разработчиком (джун). Зарплата плюс-минус 50 тысяч рублей, то есть до 100 тысяч.

Правильный ответ:

Вопрос является организационным. Кандидат адекватно оценивает свой уровень как junior-разработчика, что соответствует его опыту — месяц работы после стажировки. Указанный уровень зарплаты соответствует рынку для junior frontend-разработчика. Ответ честный и соответствующий ситуации.

Вопрос 4. Где обучался разработке до стажировки?

Таймкод: 00:04:03

Ответ собеседника: Правильный. Учился в колледже на разработчика веб-интерактивных приложений, но особо ничему там не научился. После колледжа полгода учился самостоятельно — YouTube-курсы, статьи, ничего не покупал.

Правильный ответ:

Вопрос является организационным и уточняющим. Кандидат честно описал свой путь обучения: формальное образование в колледже не дало практических навыков, после чего последовало полгода самостоятельного обучения по бесплатным материалам. Ответ полный и адекватный, демонстрирует самостоятельность кандидата.

Вопрос 5. Как прошло первое собеседование на стажировку?

Таймкод: 00:04:47

Ответ собеседника: Правильный. Ответил очень мало на вопросы, но его всё равно одобрили, потому что он хорошо говорил, и решили «дотянуть» его до собеседования.

Правильный ответ:

Вопрос является организационным и уточняющим. Кандидат честно признал, что технически был слабо подготовлен, но его коммуникативные навыки и мотивация позволили получить шанс на стажировку. Это типичная ситуация для стажировок — компании часто берут кандидатов с потенциалом роста, даже если технические знания пока недостаточны. Ответ адекватный.

Вопрос 6. В каком городе живёшь и работаешь?

Таймкод: 00:05:28

Ответ собеседника: Правильный. Живёт и работает в Казани.

Правильный ответ:

Вопрос является организационным. Кандидат проживает и работает в Казани. Ответ полный и исчерпывающий для данного вопроса.

Вопрос 7. Какие ключевые навыки указаны в резюме и где работал с Git?

Таймкод: 00:07:45

Ответ собеседника: Неполный. В резюме указаны: разработка SPA, TypeScript, Redux, Next.js SSR, работа с хуками, оптимизация компонентов (callback, memo, useMemo). На вопрос про Git ответил нечётко, упомянув работу с ним на стажировке, но без подробностей.

Правильный ответ:

Перечень навыков из резюме:

Кандидат указал следующие ключевые компетенции:

  • Разработка SPA (Single Page Applications)
  • TypeScript как основной язык
  • Redux для управления состоянием
  • Next.js с поддержкой SSR (Server-Side Rendering)
  • Работа с React-хуками
  • Оптимизация компонентов через useCallback, React.memo, useMemo

Работа с Git:

Для junior-разработчика ожидается знание базовых операций Git:

  • Клонирование репозитория (git clone)
  • Создание и переключение веток (git branch, git checkout, git switch)
  • Фиксация изменений (git add, git commit, git push, git pull)
  • Работа с Pull/Merge Requests
  • Разрешение конфликтов слияния
  • Понимание разницы между merge и rebase
  • Использование .gitignore

На стажировке кандидат, вероятно, работал с Git в командном процессе: создание feature-веток, коммит изменений, создание merge request и код-ревью. Для более полного ответа стоило бы конкретизировать, какие команды Git использовались и как был организован workflow в команде (Git Flow, GitHub Flow или другой подход).

Вопрос 8. Где работал с Git — на стажировке или самостоятельно?

Таймкод: 00:08:27

Ответ собеседника: Правильный. Работал с Git и на стажировке (стажировочный проект в скрам-команде с деликами, мерж-реквестами, пуллами), и сейчас на текущем проекте.

Правильный ответ:

Кандидат подтвердил опыт работы с Git в профессиональной среде: участие в Scrum-команде, работа с деликами (делегирование задач), создание merge request и выполнение pull-запросов. Это свидетельствует о знакомстве с командным рабочим процессом и code review. Ответ полный и адекватный для данного грейда.

Вопрос 9. Почему в резюме нет достижений с конкретными результатами и цифрами?

Таймкод: 00:09:03

Ответ собеседника: Неполный. Резюме было создано под конкретную задачу (трудоустройство в аутсорс), там нарисовали 18 лет опыта. Понял замечание, но не смог привести примеров реальных достижений с измеримыми результатами.

Правильный ответ:

Проблема с резюме:

Кандидат признал, что в резюме был указан недостоверный опыт (18 лет при фактическом месяце работы). Это серьёзная проблема, так как заведомо ложная информация в резюме подрывает доверие и может привести к немедленному отказу при проверке.

Как правильно оформлять достижения:

Для junior-разработчика допустимо указывать учебные и стажировочные проекты с измеримыми результатами. Формула: Действие + Результат + Метрика.

Примеры формулировок для junior-уровня:

  • «Реализовал компонент авторизации с валидацией форм, что сократило количество ошибок ввода на 40%»
  • «Оптимизировал рендеринг списка через React.memo, время отрисовки уменьшилось с 200ms до 50ms»
  • «Написал unit-тесты для модуля корзины, покрытие кода увеличилось с 0% до 75%»
  • «Интегрировал Sentry для отслеживания ошибок, что позволило сократить время обнаружения багов с 2 дней до 2 часов»

Рекомендации:

  1. Никогда не указывайте заведомо ложный опыт
  2. Для junior-позиции акцент делайте на учебных проектах, pet-проектах, open-source контрибуциях
  3. Даже небольшие достижения можно оформить с метриками
  4. Если опыта мало — это нормально для junior, лучше честно указать реальный уровень

Вопрос 10. Оцени себя по 10-балльной шкале по HTML/CSS, JavaScript, React и TypeScript

Таймкод: 00:10:39

Ответ собеседника: Правильный. По всем направлениям — HTML/CSS, JavaScript, React, TypeScript — оценил себя на 6-7 из 10.

Правильный ответ:

Самооценка 6-7 из 10 по всем ключевым фронтенд-технологиям является адекватной для junior-разработчика с месяцем коммерческого опыта и стажировкой. Такая оценка демонстрирует:

  • Понимание основ каждой технологии
  • Осознание зоны роста
  • Отсутствие завышенных ожиданий

Для сравнения, типичное распределение по грейдам:

  • Junior: 4-6 из 10 — знание основ, способность решать типовые задачи с помощью документации
  • Middle: 7-8 из 10 — уверенное владение, способность проектировать решения, знание паттернов
  • Senior: 9-10 из 10 — экспертное знание, способность обучать других, глубокое понимание внутренней работы

Кандидат оценивает себя чуть выше среднего для junior-уровня, что приемлемо и свидетельствует об уверенности в своих силах при адекватном самовосприятии.

Вопрос 11. Что делает тег <label>, зачем нужен атрибут for?

Таймкод: 00:11:13

Ответ собеседника: Правильный. Label — это описание для инпута, чекбокса и других элементов формы, чтобы пользователь понимал, для чего они. Атрибут for связывает label с конкретным инпутом по id, и при клике на label фокус переходит на связанный инпут.

Правильный ответ:

Ответ кандидата полный и правильный. Дополним деталями.

Назначение тега <label>:

<label> — элемент HTML-формы, который предоставляет текстовое описание для элемента управления (input, select, textarea). Решает две ключевые задачи:

1. Улучшение пользовательского опыта

При клике на <label> фокус автоматически переходит на связанный элемент формы. Для чекбоксов и радиокнопок это особенно важно, так как увеличивает область клика.

2. Доступность (Accessibility / a11y)

Скринридеры используют <label> для озучивания назначения элемента формы пользователям с ограниченными возможностями.

Способы связывания:

А. Через атрибут for и id:

<label for="email">Email:</label>
<input type="email" id="email" name="email" />

Б. Через вложенность:

<label>
Email:
<input type="email" name="email" />
</label>

Оба способа валидны, но первый предпочтительнее при сложной вёрстке, когда label и input не могут быть вложены друг в друга.

Пример с чекбоксом:

<label>
<input type="checkbox" name="agree" />
Я согласен с условиями
</label>

В этом случае клик по тексту «Я согласен с условиями» переключает состояние чекбокса.

Важность для доступности:

<!-- Плохо: скринридер не поймёт назначение поля -->
<div>Email</div>
<input type="email" name="email" />

<!-- Хорошо: скринридер озучит "Email, edit text" -->
<label for="email">Email</label>
<input type="email" id="email" name="email" />

Ответ кандидата полностью покрывает суть вопроса.

Вопрос 12. Что делает мета-тег <meta name="viewport"> и какую информацию он сообщает браузеру?

Таймкод: 00:11:52

Ответ собеседника: Неправильный. Не смог ответить на вопрос, сказал что метаданные точно не помнит для чего конкретно нужен viewport.

Правильный ответ:

Назначение viewport meta-тега:

Тег <meta name="viewport"> управляет тем, как страница отображается на мобильных устройствах. Без него мобильный браузер рендерит страницу как десктопную (обычно шириной 980px) и масштабирует её, что делает контент нечитаемым.

Стандартное объявление:

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

Ключевые параметры:

width=device-width — ширина viewport равна ширине экрана устройства. Без этого браузер использует фиксированную ширину (обычно 980px на iOS).

initial-scale=1.0 — начальный масштаб страницы. Значение 1.0 означает без масштабирования.

Дополнительные параметры:

<!-- Запрет масштабирования пользователем -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

<!-- Минимальный и максимальный масштаб -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=0.5, maximum-scale=3.0" />

<!-- Ориентация экрана (не рекомендуется использовать принудительно) -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />

Почему это критически важно:

<!-- Без viewport: мобильный браузер показывает "уменьшенный" сайт -->
<head>
<title>Без viewport</title>
</head>

<!-- С viewport: контент адаптируется под ширину устройства -->
<head>
<title>С viewport</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>

Связь с адаптивной вёрсткой:

Viewport meta-тег работает совместно с CSS-медиазапросами:

/* Стили для мобильных (до 768px) */
.container {
width: 100%;
padding: 16px;
}

/* Стили для планшетов и десктопов */
@media (min-width: 768px) {
.container {
width: 750px;
margin: 0 auto;
}
}

Без viewport-тега медиазапрос @media (min-width: 768px) на телефоне с физической шириной 375px никогда не сработает, так как viewport будет 980px.

Итог: Это базовый вопрос по HTML, и знание viewport обязательно для любого фронтенд-разработчика, работающего с адаптивной вёрсткой.

Вопрос 13. Что такое <!DOCTYPE> и что произойдёт, если его убрать?

Таймкод: 00:12:32

Ответ собеседова: Неполный. DOCTYPE указывает, что тип документа — HTML. Если его убрать, страница в целом будет работать нормально, но браузер может интерпретировать её иначе. Не смог точно объяснить разницу между стандартным и quirks-режимом.

Правильный ответ:

Назначение <!DOCTYPE>:

<!DOCTYPE> — декларация типа документа, которая должна быть первой строкой HTML-файла. Она информирует браузер о версии HTML, используемой в документе, и определяет режим рендеринга.

Объявление для HTML5:

<!DOCTYPE html>

Это единственное объявление, которое нужно запомнить для современной разработки.

Режимы рендеринга браузера:

1. Standards Mode (стандартный режим)

Активируется при наличии корректного <!DOCTYPE>. Браузер рендерит страницу строго по спецификациям W3C и WHATWG.

<!DOCTYPE html>
<html>
<body>
<!-- Рендеринг по стандартам -->
</body>
</html>

2. Quirks Mode (режим совместимости)

Активируется при отсутствии <!DOCTYPE> или при использовании устаревших объявлений. Браузер эмулирует поведение старых версий (IE5, Netscape 4) для совместимости с унаследованным кодом.

<!-- Нет DOCTYPE — quirks mode -->
<html>
<body>
<!-- Рендеринг с багами старых браузеров -->
</body>
</html>

3. Almost Standards Mode (почти стандартный режим)

Режим, при котором большинство вещей рендерится по стандартам, но есть небольшие отличия (например, в рендеринге таблиц). Активируется некоторыми старыми DOCTYPE.

Ключевые различия между Standards и Quirks Mode:

АспектStandards ModeQuirks Mode
Box modelwidth = content + padding + border (стандартный W3C)width включает padding и border (как в IE5)
Высота строкиВычисляется по спецификацииМожет наследоваться некорректно
Отступы и поляСтандартное поведениеМогут игнорироваться для некоторых элементов
Размер шрифтаСтандартное наследованиеНелинейное масштабирование вложенных таблиц
overflowСтандартное поведениеМожет работать иначе для body

Практический пример — box model:

<!DOCTYPE html>
<style>
.box {
width: 200px;
padding: 20px;
border: 5px solid black;
}
</style>
<div class="box">Content</div>

В Standards Mode общая ширина элемента: 200 + 20*2 + 5*2 = 250px. В Quirks Mode общая ширина: 200px (padding и border включены в width).

Современная альтернатива через CSS:

/* Принуждение к border-box модели для всех элементов */
*, *::before, *::after {
box-sizing: border-box;
}

Вывод: Отсутствие <!DOCTYPE> — серьёзная проблема, приводящая к непредсказуемому рендерингу. Декларация <!DOCTYPE html> должна быть в каждом HTML-документе. Кандидат верно указал на изменение интерпретации браузером, но не смог детализировать конкретные различия между режимами.

Вопрос 14. В чём разница между <button>, <input type="submit"> и <input type="button">?

Таймкод: 00:12:56

Ответ собеседника: Неполный. Работают одинаково, разница в семантике. Считал, что сабмит сработает только при нажатии на input, а не на кнопку, но ему пояснили, что это не так.

Правильный ответ:

1. <button>

Универсальный элемент кнопки. По умолчанию имеет type="submit", но поддерживает три типа:

<!-- Сабмитит форму (по умолчанию) -->
<button type="submit">Отправить</button>

<!-- Просто кнопка без встроенного поведения -->
<button type="button">Нажми меня</button>

<!-- Сбрасывает форму -->
<button type="reset">Сбросить</button>

Преимущества <button>:

  • Может содержать вложенный HTML (иконки, span, картинки)
  • Более гибкая стилизация
  • Лучшая семантика и доступность
<button type="submit">
<svg><!-- иконка --></svg>
<span>Отправить</span>
</button>

2. <input type="submit">

Создаёт кнопку отправки формы. Значение задаётся через атрибут value:

<input type="submit" value="Отправить" />

Ограничения:

  • Не может содержать дочерних элементов
  • Менее гибкая стилизация
  • Текст задаётся только через value

3. <input type="button">

Создаёт кнопку без встроенного поведения. По умолчанию ничего не делает — требует JavaScript-обработчика:

<input type="button" value="Кликни" onclick="handleClick()" />

Поведение внутри формы:

Все три элемента внутри <form> ведут себя по-разному:

<form action="/submit" method="POST">
<input name="email" type="email" />

<!-- Сабмитит форму (type="submit" по умолчанию) -->
<button>Отправить через button</button>

<!-- Сабмитит форму -->
<input type="submit" value="Отправить через input submit" />

<!-- НЕ сабмитит форму — просто кнопка -->
<input type="button" value="Просто кнопка" />
</form>

Ключевые различия:

Характеристика<button><input type="submit"><input type="button">
Сабмит формуДа (по умолчанию)ДаНет
Вложенный HTMLДаНетНет
Гибкость стилизацииВысокаяОграниченнаяОграниченная
ДоступностьЛучшаяХорошаяХорошая

Рекомендация: Используйте <button> как основной элемент кнопки — он наиболее гибкий и семантически правильный. <input type="submit"> и <input type="button"> считаются устаревшими подходами для создания кнопок.

Кандидат допустил ошибку, полагая, что submit работает только для input. На самом деле <button> без явного type="button" также сабмитит форму, так как его тип по умолчанию — submit.

Вопрос 15. Как работает z-index и что такое контекст наложения?

Таймкод: 00:13:50

Ответ собеседова: Неполный. Z-index управляет наложением элементов по оси Z: чем больше значение, тем выше элемент. Работает для элементов с position, отличным от static (absolute, relative, fixed). Упомянул, что что-то мог забыть. Не рассказал про контекст наложения как таковой.

Правильный ответ:

Базовый принцип z-index:

z-index определяет порядок наложения элементов по оси Z. Элемент с большим значением z-index перекрывает элемент с меньшим значением.

.box1 {
position: absolute;
z-index: 1;
}

.box2 {
position: absolute;
z-index: 10; /* Перекроет .box1 */
}

Условия работы z-index:

z-index работает только для элементов с position, отличным от static:

  • position: relative
  • position: absolute
  • position: fixed
  • position: sticky

Для position: static значение z-index игнорируется.

Контекст наложения (Stacking Context):

Это ключевое понятие, которое кандидат не раскрыл. Контекст наложения — это трёхмерная концептуализация элементов HTML вдоль оси Z относительно пользователя.

Когда создаётся контекст наложения:

  1. Корневой элемент <html> всегда создаёт контекст наложения
  2. Элемент с position: absolute/fixed и z-index не auto
  3. Элемент с position: relative/sticky и z-index не auto
  4. Элемент с opacity меньше 1
  5. Элемент с transform не none
  6. Элемент с filter не none
  7. Элемент с isolation: isolate
  8. Элемент — will-change с определёнными значениями
  9. Элемент с mix-blend-mode, отличным от normal
  10. Flex- или Grid-потомок с z-index не auto

Практическая проблема — изоляция z-index:

<div class="parent" style="position: relative; z-index: 1;">
<div class="child" style="position: absolute; z-index: 9999;">
Я с z-index: 9999
</div>
</div>

<div class="other" style="position: relative; z-index: 2;">
<!-- Этот элемент перекроет .child, несмотря на z-index: 9999 -->
</div>

Почему так происходит: .parent создаёт контекст наложения с z-index: 1. Его дочерний .child с z-index: 9999 находится внутри этого контекста и не может «выбраться» за его пределы. Элемент .other с z-index: 2 находится на том же уровне, что и .parent, и поскольку 2 > 1, весь контекст .parent (включая .child) оказывается под .other.

Визуализация порядка наложения внутри одного контекста:

  1. Фон и границы элемента, создающего контекст
  2. Дети с отрицательным z-index
  3. Непозиционированные блоки (в порядке появления в DOM)
  4. Непозиционированные float-элементы
  5. Непозиционированные inline-элементы
  6. Позиционированные элементы с z-index: auto или 0
  7. Дети с положительным z-index

Пример с opacity, создающим контекст:

.parent {
opacity: 0.99; /* Создаёт новый контекст наложения */
}

.child {
position: absolute;
z-index: 9999; /* Работает только внутри контекста .parent */
}

Рекомендации:

  1. Используйте z-index осмотрительно — завышенные значения (9999, 99999) — антипаттерн
  2. Рассмотрите использование isolation: isolate для явного создания контекста там, где это нужно
  3. При отладке проблем с наложением проверяйте, какой предок создаёт контекст наложения
  4. Используйте CSS-переменные для управления z-index:
:root {
--z-dropdown: 100;
--z-modal: 200;
--z-toast: 300;
--z-tooltip: 400;
}

Кандидат верно описал базовое поведение z-index, но не раскрыл контекст наложения — важную тему, которая часто вызывает трудности при отладке CSS.

Вопрос 16. Что делает свойство object-fit в CSS?

Таймкод: 00:14:39

Ответ собеседника: Неполный. Использовал object-fit: cover для растягивания изображения без потери качества. Не смог чётко объяснить, что оно управляет тем, как замещаемый элемент (изображение, видео) масштабируется внутри своего контейнера.

Правильный ответ:

Назначение object-fit:

object-fit определяет, как замещаемый содержимый (replaced element) — <img>, <video>, <picture>, <canvas> — масштабируется и позиционируется внутри своего контейнера, когда соотношение сторон содержимого не совпадает с соотношением сторон блока.

Значения object-fit:

fill (по умолчанию):

Содержимое растягивается, заполняя весь контейнер. Соотношение сторон не сохраняется — изображение может деформироваться.

img {
width: 300px;
height: 200px;
object-fit: fill; /* Растянется, игнорируя пропорции */
}

contain:

Содержимое масштабируется с сохранением пропорций так, чтобы полностью поместиться внутри контейнера. Могут появиться пустые области (letterboxing).

img {
width: 300px;
height: 200px;
object-fit: contain; /* Вписывается полностью, возможны полосы по бокам */
}

cover:

Содержимое масштабируется с сохранением пропорций так, чтобы полностью покрыть контейнер. Излишки обрезаются. Это самое популярное значение для фоновых изображений и карточек.

.card-image {
width: 300px;
height: 200px;
object-fit: cover; /* Заполняет контейнер, обрезая лишнее */
}

none:

Содержимое не масштабируется вообще. Оно сохраняет свой оригинальный размер и обрезается по границам контейнера.

img {
width: 300px;
height: 200px;
object-fit: none; /* Оригинальный размер, обрезается */
}

scale-down:

Действует как меньшее из none и contain. Если оригинальный размер меньше контейнера — отображается без масштабирования. Если больше — вписывается как contain.

img {
width: 300px;
height: 200px;
object-fit: scale-down; /* contain или none — что меньше */
}

Дополнительное свойство object-position:

Управляет позиционированием содержимого внутри контейнера, аналогично background-position:

img {
width: 300px;
height: 200px;
object-fit: cover;
object-position: top center; /* Фокус на верхней части изображения */
}

Практический пример — карточка с аватаркой:

<div class="card">
<img class="card__image" src="photo.jpg" alt="Фото" />
<h3 class="card__title">Имя пользователя</h3>
</div>
.card {
width: 250px;
}

.card__image {
width: 100%;
height: 250px;
object-fit: cover;
object-position: center top;
border-radius: 50%;
}

Сравнение с background-size:

object-fit часто сравнивают с background-size, но есть ключевое отличие:

/* Через background — изображение не индексируется поисковиками, нет alt */
.hero {
background-image: url('photo.jpg');
background-size: cover;
background-position: center;
}

/* Через img — семантически правильно, доступно для скринридеров */
.hero img {
width: 100%;
height: 100%;
object-fit: cover;
}

Итог: object-fit: cover — наиболее часто используемое значение в реальной разработке, особенно для карточек, аватаров и hero-секций. Кандидат верно указал его применение, но не раскрыл полную картину значений.

Вопрос 17. Что делает функция calc() в CSS и чем она полезна?

Таймкод: 00:16:22

Ответ собеседника: Неполный. calc() вычисляет размерности, позволяя комбинировать разные единицы измерения (например, 100vh - 100px). Переводит всё в пиксели и даёт окончательный результат. Ответ был близок к правильному, но сформулирован нечётко.

Правильный ответ:

Назначение calc():

calc() — CSS-функция, позволяющая выполнять математические вычисления при задании значений свойств. Главная ценность — возможность комбинировать разные единицы измерения в одном выражении.

Синтаксис:

property: calc(expression);

Поддерживаемые операторы:

  • + — сложение
  • - — вычитание
  • * — умножение (хотя бы один операнд должен быть числом без единиц)
  • / — деление (правый операнд должен быть числом без единиц)

Ключевые правила:

  1. Пробелы вокруг + и - обязательны (иначе парсер может интерпретировать как отрицательное число)
  2. * и / не требуют пробелов, но для читаемости лучше ставить
  3. Можно вкладывать calc() друг в друга

Практические примеры:

Вычитание фиксированного значения из процентов:

.sidebar {
width: calc(100% - 250px);
}

.content {
height: calc(100vh - 60px); /* Высота экрана минус шапка */
}

Комбинирование разных единиц:

.container {
padding: calc(1rem + 2vw); /* Адаптивный отступ */
font-size: calc(14px + 0.5vw); /* Плавное масштабирование шрифта */
margin-top: calc(50% - 150px); /* Центрирование с поправкой */
}

CSS Grid с calc():

.grid {
display: grid;
grid-template-columns: 250px 1fr;
/* Или с calc для более сложных случаев: */
grid-template-columns: calc(33.333% - 20px) calc(33.333% - 20px) calc(33.333% - 20px);
gap: 30px;
}

Адаптивная типографика (clamp + calc):

h1 {
/* Минимум 24px, предпочтение 4vw, максимум 48px */
font-size: clamp(24px, calc(24px + 2vw), 48px);
}

Типичный случай — sticky footer:

.page {
min-height: 100vh;
display: flex;
flex-direction: column;
}

.page__content {
flex: 1;
}

/* Альтернатива: */
.page__content {
min-height: calc(100vh - 80px); /* 80px — высота футера */
}

Отличие от препроцессорных вычислений:

/* SASS — вычисляется на этапе компиляции, не знает о viewport */
.sidebar {
width: 100% - 250px; /* Ошибка! SASS не может вычесть px из % */
}

/* CSS calc() — вычисляется браузером в runtime */
.sidebar {
width: calc(100% - 250px); /* Работает! */
}

Производительность:

calc() вычисляется браузером на этапе layout (reflow). Современные браузеры оптимизируют эти вычисления, поэтому влияние на производительность минимально. Однако чрезмерно сложные вложенные выражения могут незначительно замедлить рендеринг.

Итог: calc() — мощный инструмент для создания гибких макетов без JavaScript. Особенно полезен для комбинирования фиксированных и относительных единиц измерения. Кандидат правильно указал основное назначение, но формулировка про «перевод в пиксели» неточна — результат calc() может быть в любой единице измерения в зависимости от входных данных.

Вопрос 18. В чём разница между var, let и const в JavaScript?

Таймкод: 00:17:20

Ответ собеседника: Неполный. var — устаревший способ объявления переменных. var имеет функциональную область видимости и всплывает. let и const тоже всплывают, но попадают в «деф-зону» (TDZ — temporal dead zone), и к ним нельзя обратиться до объявления. Не вспомнил термин «temporal dead zone» и не объяснил разницу между let и const.

Правильный ответ:

1. Область видимости (Scope):

var — функциональная область видимости:

function example() {
if (true) {
var x = 10;
}
console.log(x); // 10 — переменная доступна вне блока
}

let и const — блочная область видимости:

function example() {
if (true) {
let y = 20;
const z = 30;
}
console.log(y); // ReferenceError: y is not defined
console.log(z); // ReferenceError: z is not defined
}

2. Всплытие (Hoisting):

var — всплывает с инициализацией undefined:

console.log(a); // undefined — ошибки нет
var a = 5;

Интерпретируется как:

var a; // Объявление всплывает
console.log(a); // undefined
a = 5; // Присваивание остаётся на месте

let и const — всплывают, но без инициализации (TDZ):

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;

Temporal Dead Zone (TDZ) — временная мёртвая зона:

Это период от начала блока до строки объявления переменной. В этой зоне переменная существует (память выделена), но доступ к ней вызывает ReferenceError.

// TDZ начинается здесь
console.log(typeof myVar); // ReferenceError (а не undefined!)
// TDZ продолжается здесь
let myVar = 42;
// TDZ заканчивается здесь
console.log(myVar); // 42

3. Переобъявление:

var допускает переобъявление:

var x = 1;
var x = 2; // Ошибки нет, перезапишет значение

let и const запрещают переобъявление:

let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared

const z = 1;
const z = 2; // SyntaxError: Identifier 'z' has already been declared

4. Присваивание и мутация — разница между let и const:

let — можно переприсваивать:

let count = 0;
count = 1; // OK
count = 42; // OK

const — нельзя переприсваивать, но можно мутировать:

const MAX_VALUE = 100;
MAX_VALUE = 200; // TypeError: Assignment to constant variable

// НО: мутация объектов и массивов разрешена
const user = { name: 'Alice' };
user.name = 'Bob'; // OK — мутация свойства
user.age = 30; // OK — добавление свойства

const arr = [1, 2, 3];
arr.push(4); // OK — [1, 2, 3, 4]
arr[0] = 99; // OK — [99, 2, 3, 4]

// Полная замена — ошибка
user = {}; // TypeError
arr = []; // TypeError

5. Глобальный объект:

var globalVar = 'test';
console.log(window.globalVar); // 'test' — попадает в глобальный объект

let globalLet = 'test';
console.log(window.globalLet); // undefined — не попадает

Сводная таблица:

Характеристикаvarletconst
Область видимостиФункциональнаяБлочнаяБлочная
HoistingДа, с undefinedДа, с TDZДа, с TDZ
ПереobjectявлениеДаНетНет
ПереприсваиваниеДаДаНет
Мутация объектаДаДаДа

Рекомендации:

  1. Используйте const по умолчанию
  2. Используйте let, когда значение будет меняться (счётчики, флаги)
  3. Не используйте var в новом коде
  4. Для «заморозки» объектов используйте Object.freeze():
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
});

config.apiUrl = 'https://evil.com'; // Тихо игнорируется (или ошибка в strict mode)

Кандидат хорошо описал var и TDZ, но не объяснил ключевое разницу между let и const — запрет переприсваивания при разрешённой мутации.

Вопрос 19. Что такое промис в JavaScript?

Таймкод: 00:23:38

Ответ собеседника: Правильный. Промис — это объект, который позволяет работать с асинхронностью в JavaScript.

Правильный ответ:

Ответ кандидата верен по сути, но слишком краток. Раскроем тему подробнее.

Определение:

Промис (Promise) — объект, представляющий результат асинхронной операции, который может быть доступен сейчас, в будущем или никогда. Промис является «заполнителем» для значения, которое будет получено позже.

Три состояния промиса:

  1. Pending — ожидание, начальное состояние, операция ещё не завершена
  2. Fulfilled — выполнен успешно, результат получен
  3. Rejected — выполнен с ошибкой, причина отказа доступна

Состояние меняется только один раз: из pending либо в fulfilled, либо в rejected. После смены состояния промис считается «settled» (завершённым).

Создание промиса:

const promise = new Promise((resolve, reject) => {
// Асинхронная операция
setTimeout(() => {
const success = true;
if (success) {
resolve('Данные получены'); // Переводит в fulfilled
} else {
reject(new Error('Ошибка сети')); // Переводит в rejected
}
}, 1000);
});

Потребление промиса:

promise
.then((result) => {
console.log(result); // 'Данные получены'
})
.catch((error) => {
console.error(error.message); // 'Ошибка сети'
})
.finally(() => {
console.log('Операция завершена'); // Выполняется всегда
});

Цепочки промисов (chaining):

fetch('/api/user')
.then((response) => response.json())
.then((user) => fetch(`/api/posts?userId=${user.id}`))
.then((response) => response.json())
.then((posts) => console.log(posts))
.catch((error) => console.error('Ошибка:', error));

Каждый .then() возвращает новый промис, что позволяет строить цепочки.

Статические методы:

// Параллельное выполнение, ждёт все промисы
Promise.all([fetch('/api/a'), fetch('/api/b'), fetch('/api/c')])
.then(([a, b, c]) => console.log(a, b, c));

// Первый успешный результат
Promise.race([fetch('/api/fast'), fetch('/api/slow')])
.then((result) => console.log('Первый:', result));

// Ждёт завершения всех (успех или ошибка)
Promise.allSettled([
Promise.resolve('ok'),
Promise.reject('fail'),
]).then((results) => {
console.log(results);
// [{ status: 'fulfilled', value: 'ok' },
// { status: 'rejected', reason: 'fail' }]
});

// Первый успешный (игнорирует ошибки)
Promise.any([Promise.reject('a'), Promise.resolve('b')])
.then((result) => console.log(result)); // 'b'

Практический пример — запрос с таймаутом:

function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
),
]);
}

fetchWithTimeout('/api/data', 3000)
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error(error.message));

Связь с async/await:

async function loadUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
} catch (error) {
console.error('Не удалось загрузить пользователя:', error);
throw error;
}
}

async/await — синтаксический сахар над промисами, который делает асинхронный код похожим на синхронный.

Итог: Промис — фундаментальная концепция JavaScript для работы с асинхронностью. Понимание промисов обязательно для любого разработчика, работающего с API, таймерами и другими асинхронными операциями.

Вопрос 20. Чем async/await отличается от then?

Таймкод: 00:23:51

Ответ собеседника: Правильный. async/await — это синтаксический сахар над промисом, который позволяет сделать функцию, возвращающую промис. then — это метод для обработки промиса. Они разные по назначению: async/await для объявления асинхронных функций, then для обработки результата.

Правильный ответ:

Ответ кандидата верен. Дополним деталями и примерами.

Ключевое отличие:

async/await и .then() работают с промисами, но представляют разные стили написания асинхронного кода. async/await — синтаксический сахар над промисами, который делает код более читаемым.

Сравнение на практике:

Через .then():

function loadUserData(userId) {
return fetch(`/api/users/${userId}`)
.then((response) => {
if (!response.ok) {
throw new Error('Network error');
}
return response.json();
})
.then((user) => {
return fetch(`/api/posts?userId=${user.id}`);
})
.then((response) => response.json())
.then((posts) => {
return { user: posts.user, posts };
})
.catch((error) => {
console.error('Error:', error);
throw error;
});
}

Через async/await:

async function loadUserData(userId) {
try {
const userResponse = await fetch(`/api/users/${userId}`);
if (!userResponse.ok) {
throw new Error('Network error');
}
const user = await userResponse.json();

const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsResponse.json();

return { user: posts.user, posts };
} catch (error) {
console.error('Error:', error);
throw error;
}
}

Преимущества async/await:

  1. Читаемость — код выглядит как синхронный, легче следовать логике
  2. Обработка ошибок — привычный try/catch вместо .catch()
  3. Отладка — стек вызовов сохраняется, проще находить ошибки
  4. Условная логика — легче писать if/else и циклы
// Условная логика — async/await
async function loadData(user) {
if (user.isAdmin) {
const adminData = await fetch('/api/admin');
return adminData.json();
} else {
const userData = await fetch('/api/user');
return userData.json();
}
}

// То же через .then — менее читаемо
function loadDataThen(user) {
if (user.isAdmin) {
return fetch('/api/admin').then((r) => r.json());
} else {
return fetch('/api/user').then((r) => r.json());
}
}

Преимущества .then():

  1. Композируемость — удобно для простых цепочек и параллельных операций
  2. Promise.all/race — нативная интеграция
  3. Не нужно оборачивать в async — можно использовать в любом контексте
// Параллельные запросы — оба подхода работают
// Через Promise.all
const [users, posts] = await Promise.all([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
]);

// Или через async/await
async function loadAll() {
const [users, posts] = await Promise.all([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
]);
return { users, posts };
}

Когда что использовать:

СитуацияРекомендация
Последовательные запросыasync/await
Параллельные запросыPromise.all + await
Простая трансформация данных.then() или await
Сложная условная логикаasync/await
Обработка ошибокasync/await + try/catch
Callback-based APIОбернуть в промис, затем async/await

Итог: Оба подхода работают с промисами и взаимозаменяемы. async/await предпочтителен для большинства случаев благодаря лучшей читаемости и удобству отладки. .then() остаётся полезным для простых цепочек и композиции промисов.

Вопрос 21. В чём разница между null, undefined и NaN?

Таймкод: 00:24:12

Ответ собеседника: Неполный. NaN (Not a Number) — результат неудачного приведения значения к числу. null — значение, заданное явно программистом («ничего»). undefined — значение не задано (переменная объявлена, но ей ничего не присвоено). Признал, что тонкая грань между null и undefined иногда путает.

Правильный ответ:

Ответ кандидата в целом верен. Раскроем тему глубже, особенно практические аспекты.

1. undefined — «значение не присвоено»

undefined означает, что переменная объявлена, но значение ей не присвоено, или свойство/параметр не существует.

let a;
console.log(a); // undefined

function foo(x) {
console.log(x);
}
foo(); // undefined — параметр не передан

const obj = {};
console.log(obj.nonExistent); // undefined — свойства нет

const arr = [1, 2, 3];
console.log(arr[10]); // undefined — индекс за пределами

// Явное присвоение undefined — антипаттерн
let b = undefined; // Лучше использовать let b; или let b = null;

2. null — «намеренное отсутствие значения»

null — примитивное значение, которое программист присваивает явно, чтобы обозначить «здесь ничего нет» или «значение отсутствует».

const user = null; // Пользователь не найден
const selectedItem = null; // Ничего не выбрано
const cache = null; // Кэш очищен

// Типичное использование в API
function findUser(id) {
const user = database.find((u) => u.id === id);
return user || null; // Явно возвращаем null, если не нашли
}

3. NaN — «не число»

NaN (Not a Number) — специальное числовое значение, представляющее результат невалидной математической операции.

console.log(Number('hello')); // NaN
console.log(undefined + 1); // NaN
console.log(0 / 0); // NaN
console.log(Math.sqrt(-1)); // NaN
console.log(parseFloat('abc')); // NaN

// Критическое свойство: NaN не равен ничему, даже самому себе
console.log(NaN === NaN); // false

// Проверка на NaN
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN('hello')); // false — строка, не NaN

// Устаревший глобальный isNaN делает приведение типов
console.log(isNaN('hello')); // true — сначала пытается преобразовать в число

Сводная таблица:

ХарактеристикаundefinednullNaN
Типundefinedobject (баг JS)number
ЗначениеНе присвоеноНамеренная пустотаНевалидное число
ИсточникАвтоматическиПрограммистОперация
typeof"undefined""object""number"

Проблема typeof null === 'object':

Это исторический баг JavaScript, сохранённый для обратной совместимости. В первых версиях JS значения хранились как теги типов и данные. Тег для объектов был 000, а null представлялся как нулевой указатель (000 + нулевой адрес), поэтому typeof возвращал "object".

Проверка на null и undefined:

let value;

// Проверка на undefined
console.log(value === undefined); // true
console.log(typeof value === 'undefined'); // true (безопасно для необъявленных)

// Проверка на null
value = null;
console.log(value === null); // true

// Проверка на оба (null == undefined — true!)
console.log(value == null); // true — ловит и null, и undefined

// Безопасная проверка «есть ли значение»
function hasValue(val) {
return val !== null && val !== undefined;
}

// Или через оператор нулевого слияния (??)
const result = value ?? 'default'; // 'default' — только для null/undefined

// Оператор || ловит все falsy значения
const result2 = value || 'default'; // 'default' — но 0, '', false тоже заменит

Практические рекомендации:

  1. Используйте null для явного обозначения отсутствия значения
  2. Не присваивайте undefined вручную
  3. Используйте Number.isNaN() вместо глобального isNaN()
  4. Оператор ?? (nullish coalescing) предпочтительнее ||, когда нужно различать null/undefined и 0/''/false

Кандидат верно определил все три значения, но не упомянул ключевые нюансы: typeof null === 'object', NaN !== NaN, и разницу между ?? и ||.

Вопрос 22. Что такое контекст в React и что происходит при его изменении?

Таймкод: 00:25:34

Ответ собеседника: Неполный. Контекст — это небольшой глобальный стейт, к которому можно обратиться из компонентов и изменить его. При изменении контекста перерисовываются все подписчики контекста (все компоненты внутри провайдера). Не смог чётко объяснить разницу между контекстом и Redux.

Правильный ответ:

Что такое React Context:

React Context — механизм передачи данных через дерево компонентов без необходимости пропсами (prop drilling). Позволяет «пробросить» значение от предка к любому потомку на любом уровне вложенности.

Базовое использование:

import { createContext, useContext, useState } from 'react';

// 1. Создание контекста
const ThemeContext = createContext('light');

// 2. Провайдер — оборачивает часть дерева
function App() {
const [theme, setTheme] = useState('light');

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<MainContent />
</ThemeContext.Provider>
);
}

// 3. Потребление в любом вложенном компоненте
function Header() {
const { theme, setTheme } = useContext(ThemeContext);

return (
<header className={theme}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Переключить тему
</button>
</header>
);
}

Что происходит при изменении контекста:

Когда значение в Provider меняется, React вызывает ре-рендер всех компонентов, которые вызывают useContext для данного контекста, независимо от того, используют ли они изменившееся значение или нет.

const UserContext = createContext(null);

function App() {
const [user, setUser] = useState({ name: 'Alice' });
const [count, setCount] = useState(0);

return (
<UserContext.Provider value={user}>
<Profile /> {/* Ре-рендерится при изменении user */}
<UserSettings /> {/* Ре-рендерится при изменении user */}
<Counter /> {/* НЕ ре-рендерится — не использует контекст */}
</UserContext.Provider>
);
}

Проблема производительности и решение:

// Плохо: при изменении user.email ре-рендерятся все потребители
<UserContext.Provider value={user}>
...
</UserContext.Provider>

// Лучше: разделяем на отдельные контексты
<UserContext.Provider value={user}>
<EmailContext.Provider value={user.email}>
...
</EmailContext.Provider>
</UserContext.Provider>

// Или используем мемоизацию
const value = useMemo(() => ({ user, setUser }), [user]);
<UserContext.Provider value={value}>
...
</UserContext.Provider>

Разница между Context и Redux:

ХарактеристикаContextRedux
НазначениеПередача данных через деревоГлобальное управление состоянием
DevToolsНетДа (Redux DevTools)
MiddlewareНетДа (thunk, saga, etc.)
СелекторыНет (ре-рендер всех)Да (точечные подписки)
ОтладкаСложнаяВременная машин времени
РазмерВстроен в React~2KB + зависимости
ОптимизацияРучнаяВстроенная

Когда использовать Context:

  • Тема приложения (theme)
  • Текущая локализация (i18n)
  • Аутентифицированный пользователь
  • Настройки UI (sidebar state, etc.)

Когда использовать Redux (или Zustand/Jotai):

  • Сложное бизнес-состояние
  • Нужны middleware для побочных эффектов
  • Важна производительность при частых обновлениях
  • Нужны DevTools для отладки состояния

Современные альтернативы:

// Zustand — минималистичный стейт-менеджер
import { create } from 'zustand';

const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));

// Jotai — атомарный подход
import { atom, useAtom } from 'jotai';

const userAtom = atom(null);
const [user, setUser] = useAtom(userAtom);

Итог: Context — это инструмент для передачи данных, а не полноценный стейт-менеджер. Кандидат верно описал базовое поведение, но не объяснил проблему ре-рендеров и разницу с Redux.

Вопрос 23. В чём разница между контекстом и Redux?

Таймкод: 00:26:07

Ответ собеседника: Неполный. Redux реализован не совсем через контекст, а через что-то вроде use/store (не вспомнил точно). Redux более оптимизирован — перерисовываются только компоненты, которые получают конкретные состояния, а не всё содержимое провайдера. Ответ был нечётким, признал, что «налил воды».

Правильный ответ:

Кандидат правильно уловил суть различий в оптимизации, но ответ был неструктурированным. Приведём полный разбор.

Внутренняя реализация:

Redux (через react-redux) использует React Context внутри своего Provider, но добавляет слой оптимизации поверх него. Хранение состояния происходит через createStore, а подписка на изменения — через useSelector или useStore.

// react-redux Provider внутри использует Context
import { Provider } from 'react-redux';
import { createStore } from 'redux';

const store = createStore(reducer);

<Provider store={store}>
<App />
</Provider>

Ключевые различия в механизме обновлений:

Context — широковещательное обновление:

При изменении значения в Provider React помечает как «грязные» все компоненты, вызывающие useContext для этого контекста, и ре-рендерит их всех.

// При изменении user.name — Header, Sidebar и Footer все ре-рендерятся
<UserContext.Provider value={user}>
<Header /> {/* Использует user.name */}
<Sidebar /> {/* Использует user.role */}
<Footer /> {/* Использует user.name */}
</UserContext.Provider>

Redux — точечное обновление:

useSelector сравнивает предыдущее и текущее выбранное значение. Ре-рендер происходит только если выбранная часть состояния изменилась.

// Redux store: { user: { name: 'Alice', role: 'admin' }, posts: [...] }

function Header() {
// Подписан только на user.name
const name = useSelector((state) => state.user.name);
return <h1>{name}</h1>;
}

function Sidebar() {
// Подписан только на user.role
const role = useSelector((state) => state.user.role);
return <div>{role}</div>;
}

// При изменении posts — Header и Sidebar НЕ ре-рендерятся

Полная таблица сравнения:

ХарактеристикаReact ContextRedux Toolkit
Внутренняя реализацияНативный React APIContext + подписка через store
Механизм обновленияРе-рендер всех потребителейРе-рендер только при изменении выбранного значения
СелекторыНетuseSelector с мемоизацией
MiddlewareНетredux-thunk, redux-saga, redux-observable
DevToolsНетRedux DevTools (time-travel, state diff)
ИммутабельностьНа усмотрение разработчикаcreateSlice через Immer
Размер бандла0 KB (встроен)~11 KB (Redux + React-Redux + RTK)
Кривая обученияНизкаяСредняя
ОптимизацияРучная (разделение контекстов)Встроенная

Пример оптимизации Context:

// Плохо: один большой контекст
const AppContext = createContext();

function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [locale, setLocale] = useState('en');

// Любое изменение → ре-рендер всех потребителей
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme, locale, setLocale }}>
<Header />
<Main />
</AppContext.Provider>
);
}

// Хорошо: разделённые контексты
const UserContext = createContext();
const ThemeContext = createContext();
const LocaleContext = createContext();

function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [locale, setLocale] = useState('en');

return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<LocaleContext.Provider value={{ locale, setLocale }}>
<Header /> {/* Ре-рендерится только при изменении theme/locale */}
<Main /> {/* Ре-рендерится только при изменении user */}
</LocaleContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}

Когда что выбирать:

  • Context — для статичных или редко меняющихся данных (тема, локаль, аутентификация)
  • Redux — для сложного, часто обновляемого бизнес-состояния (корзина, фильтры, кэш данных)
  • Оба — можно комбинировать: Context для UI-состояния, Redux для бизнес-логики

Кандидат правильно указал на ключевое отличие — точечные обновления в Redux против широковещательных в Context, но не смог чётко сформулировать механизм.

Вопрос 24. Зачем нужны ключи (keys) в React и как они работают при рендеринге списков?

Таймкод: 00:28:37

Ответ собеседника: Правильный. Ключи помогают React понять, что элемент — тот же самый, его не нужно заново монтировать/размонтировать, а достаточно обновить или изменить его положение в DOM. Используются при рендеринге списков. При фильтрации или реверсе списка React по ключам понимает, что это те же элементы, и просто переставляет их в DOM, а не пересоздаёт.

Правильный ответ:

Ответ кандидата полный и правильный. Дополним техническими деталями.

Зачем нужны keys:

Ключи — это подсказка для алгоритма согласования (reconciliation) React. Они позволяют сопоставить элементы виртуального DOM между рендерами и определить:

  • Какой элемент добавлен
  • Какой элемент удалён
  • Какой элемент остался тем же, но изменился

Алгоритм сравнения:

Без ключей React сравнивает элементы попарно по позиции:

// До: <li>A</li><li>B</li><li>C</li>
// После: <li>X</li><li>A</li><li>B</li><li>C</li>

// React без keys:
// Позиция 0: A → X (обновить текст)
// Позиция 1: B → A (обновить текст)
// Позиция 2: C → B (обновить текст)
// Позиция 3: нет → C (создать новый)
// Итого: 3 обновления + 1 создание

С ключами React понимает, что элемент X был добавлен в начало, а A, B, C просто сдвинулись:

// До: <li key="a">A</li><li key="b">B</li><li key="c">C</li>
// После: <li key="x">X</li><li key="a">A</li><li key="b">B</li><li key="c">C</li>

// React с keys:
// key="x": новый элемент → создать в начале
// key="a": существует → переместить
// key="b": существует → переместить
// key="c": существует → переместить
// Итого: 1 создание + перемещения (без обновления содержимого)

Правила выбора ключей:

// Хорошо: стабильный уникальный идентификатор
const todoItems = todos.map((todo) => <li key={todo.id}>{todo.text}</li>);

// Плохо: индекс массива — ломается при сортировке/фильтрации
const todoItems = todos.map((todo, index) => <li key={index}>{todo.text}</li>);

// Плохо: Math.random() — новый key каждый рендер, полный ре-монтаж
const todoItems = todos.map((todo) => <li key={Math.random()}>{todo.text}</li>);

Проблема с индексами как ключами:

// Список с инпутами
function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>
<input defaultValue={todo.text} />
<button onClick={() => onDelete(index)}>Удалить</button>
</li>
))}
</ul>
);
}

// Состояние: [{ text: 'A' }, { text: 'B' }, { text: 'C' }]
// Пользователь вводит в первый инпут: "Modified A"
// Удаляем первый элемент

// Без key (или с index): React думает, что key=0 всё ещё первый элемент
// Инпут с "Modified A" остаётся на месте, но теперь отображает "B"
// Это баг: введённый текст привязан к неправильному элементу

Ключи и жизненный цикл компонента:

function UserCard({ user }) {
console.log('Монтаж:', user.name);
return <div>{user.name}</div>;
}

// При смене порядка с [Alice, Bob] на [Bob, Alice]:
// С key={user.id}: компоненты перемещаются, НЕ размонтируются
// С key={index}: компонент на позиции 0 (Alice) обновляется на Bob
// компонент на позиции 1 (Bob) обновляется на Alice

Ключи за пределами списков:

Ключи можно использовать для принудительного ре-монтажа компонента:

// При смене key компонент полностью размонтируется и создаётся заново
<ChatWindow key={selectedChatId} chatId={selectedChatId} />

Это полезно, когда нужно сбросить внутреннее состояние компонента при смене данных.

Итог: Кандидат дал отличный ответ, покрывающий основную идею и практические последствия использования ключей.

Вопрос 25. Что такое DOM и как расшифровывается эта аббревиатура?

Таймкод: 00:29:53

Ответ собеседника: Правильный. DOM — это Document Object Model, объектная модель документа. Это дерево объектов, представляющее структуру HTML-страницы.

Правильный ответ:

Ответ кандидата правильный и полный. Дополним контекстом.

Определение:

DOM (Document Object Model) — программный интерфейс (API) для HTML и XML документов, представляющий страницу в виде дерева объектов. Каждый узел дерева — объект, представляющий часть документа.

Структура DOM-дерева:

Document
└── html
├── head
│ ├── title
│ │ └── "Page Title" (text node)
│ └── meta
└── body
├── div.container
│ ├── h1
│ │ └── "Heading" (text node)
│ └── p
│ └── "Paragraph text" (text node)
└── script

Типы узлов:

  • Document — корневой узел
  • Element — HTML-теги (div, p, span)
  • Text — текстовое содержимое внутри элементов
  • Comment — HTML-комментарии
  • Attribute — атрибуты элементов (в DOM4 — не отдельные узлы)

Основные операции с DOM:

// Навигация
element.parentElement // Родитель
element.children // Дочерние элементы
element.nextElementSibling // Следующий сосед
element.previousElementSibling // Предыдущий сосед

// Поиск
document.getElementById('app')
document.querySelector('.container > p')
document.querySelectorAll('li.active')

// Создание
const div = document.createElement('div');
const text = document.createTextNode('Hello');

// Манипуляция
parent.appendChild(child);
parent.removeChild(child);
parent.insertBefore(newNode, referenceNode);
element.remove(); // Современный способ

// Атрибуты
element.setAttribute('class', 'active');
element.getAttribute('class');
element.classList.add('highlight');
element.dataset.userId = '123'; // data-user-id="123"

DOM vs Virtual DOM:

React использует Virtual DOM — лёгкую копию DOM в памяти. При изменениях React:

  1. Создаёт новое виртуальное дерево
  2. Сравнивает с предыдущим (diffing)
  3. Вычисляет минимальный набор изменений
  4. Применяет только необходимые изменения к реальному DOM

Это значительно быстрее, чем прямые манипуляции с DOM, так как операции с реальным DOM — дорогие (вызывают reflow и repaint).

Ответ кандидата полностью покрывает вопрос.

Вопрос 26. Что такое Virtual DOM, чем он хорош и какую проблему решает?

Таймкод: 00:31:24

Ответ собеседника: Неполный. Virtual DOM — это легковесная копия реального DOM, дерево React-элементов. В коде это абстракция, которую нельзя напрямую увидеть. Не смог объяснить, что Virtual DOM — это JavaScript-объекты, и не смог внятно объяснить, чем именно он легче реального DOM (отсутствие тяжёлых свойств реальных DOM-узлов). Упомянул, что React теперь использует термин «дерево Fiber» вместо Virtual DOM.

Правильный ответ:

Что такое Virtual DOM:

Virtual DOM — это JavaScript-объекты, представляющие структуру реального DOM-дерева. Каждый элемент Virtual DOM — обычный JS-объект с минимальным набором свойств: тип элемента, пропсы, дети.

// JSX:
<div className="container">
<h1>Hello</h1>
<p>World</p>
</div>

// Компилируется в React.createElement, который возвращает:
{
type: 'div',
props: {
className: 'container',
children: [
{
type: 'h1',
props: { children: 'Hello' }
},
{
type: 'p',
props: { children: 'World' }
}
]
}
}

Почему Virtual DOM «лёгкий»:

Реальный DOM-узел — тяжёлый объект с сотнями свойств и методов, привязанный к нативному коду браузера:

// Реальный DOM-узел содержит:
const element = document.createElement('div');
console.log(Object.keys(element).length); // 200+ свойств

// Среди них: innerHTML, outerHTML, style, offsetWidth, offsetHeight,
// getBoundingClientRect, addEventListener, appendChild, cloneNode,
// contentEditable, dataset, tabIndex, textContent, ... и сотни других

Virtual DOM-узел содержит только необходимое:

// Virtual DOM-узел:
{
type: 'div',
props: { className: 'container', children: [...] }
}
// Всего 2-3 свойства вместо 200+

Проблема, которую решает Virtual DOM:

Прямые манипуляции с DOM — дорогая операция. Каждое изменение DOM может вызвать:

  1. Reflow (layout) — пересчёт позиций и размеров элементов
  2. Repaint — перерисовка пикселей
// Плохо: 1000 отдельных операций с DOM
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
list.appendChild(li); // Каждый вызов — потенциальный reflow!
}

// Лучше: одна операция через фрагмент
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li); // Работа в памяти, без reflow
}
list.appendChild(fragment); // Один reflow

Как работает Virtual DOM в React:

  1. Render — React создаёт новое виртуальное дерево из JSX
  2. Diffing — сравнивает новое дерево с предыдущим (O(n) алгоритм)
  3. Reconciliation — вычисляет минимальный набор изменений
  4. Commit — применяет изменения к реальному DOM за одну операцию
// До обновления
{ type: 'div', props: { children: 'Count: 0' } }

// После обновления
{ type: 'div', props: { children: 'Count: 1' } }

// React вычисляет: нужно обновить только текстовый узел "0" → "1"
// Одно изменение DOM вместо пересоздания всего div

Fiber Architecture (React 16+):

Кандидат верно упомянул Fiber. Fiber — это внутренняя архитектура React, заменившая старый стековый реконсилер. Fiber-узел — расширенная версия Virtual DOM-узла с дополнительной информацией:

// Fiber-узел (упрощённо):
{
type: 'div',
props: { className: 'container' },
child: Fiber, // Первый дочерний
sibling: Fiber, // Следующий сосед
return: Fiber, // Родитель
alternate: Fiber, // Ссылка на предыдущий fiber (для сравнения)
effectTag: 'UPDATE', // Тип изменения
stateNode: DOMNode, // Ссылка на реальный DOM-узел
}

Преимущества Fiber:

  • Инкрементальный рендеринг — работа разбивается на чанки, не блокируя основной поток
  • Приоритеты — срочные обновления (ввод текста) обрабатываются раньше несрочных (загрузка данных)
  • Пауза и возобновление — рендеринг можно прервать и продолжить
  • Откат — ошибки в рендеринге не крашат всё приложение (Error Boundaries)

Итог: Virtual DOM решает проблему производительности, минимизируя количество дорогих операций с реальным DOM. Кандидат верно описал концепцию, но не смог объяснить техническую разницу в «весе» объектов и механизм оптимизации.

Вопрос 27. Что происходит в React при изменении стейта глубоко вложенного компонента? Опиши процесс по шагам.

Таймкод: 00:32:51

Ответ собеседова: Неполный. При изменении стейта React перестраивает дерево (теперь называется дерево Fiber). Упомянул фазу рендеринга и reconciliation, но не смог внятно описать процесс по шагам. Объяснение было размытым и не содержало конкретики о том, как React сравнивает старое и новое дерево и применяет изменения к реальному DOM.

Правильный ответ:

Пошаговый процесс обновления в React:

Шаг 1: Инициация обновления (Trigger)

Компонент вызывает функцию обновления состояния (setState, dispatch, useState setter). React помечает компонент как «грязный» и планирует обновление.

function DeepChild() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount((prev) => prev + 1); // ← Триггер обновления
};

return <button onClick={handleClick}>Count: {count}</button>;
}

Шаг 2: Фаза рендера (Render Phase) — вычисление изменений

React вызывает функцию компонента (или хук) и строит новое виртуальное дерево (Fiber tree). Эта фаза может быть прервана и возобновлена (благодаря Fiber architecture).

// React вызывает компонент и получает новый JSX:
// До: <button>Count: 0</button>
// После: <button>Count: 1</button>

В этой фазе React:

  • Вызывает функции компонентов / хуки
  • Строит новое Fiber-дерево
  • Сравнивает с предыдущим деревом (diffing)
  • Помечает Fiber-узлы с изменениями (effectTag)

Правила сравнения (Reconciliation):

  1. Если тип элемента изменился — старое дерево уничтожается полностью
  2. Если тип элемента тот же — обновляются только изменившиеся пропсы
  3. Для списков — ключи определяют соответствие элементов
// Тип тот же → обновляем пропсы
<div className="old" /><div className="new" />

// Тип изменился → полная пересоздание
<span>text</span><div>text</div>

Шаг 3: Фаза фиксации (Commit Phase) — применение к DOM

React синхронно применяет все вычисленные изменения к реальному DOM. Эта фаза не может быть прервана.

React выполняет три типа операций с DOM:

  1. Мутации — добавление, удаление, обновление DOM-узлов
  2. Вызов layout effectsuseLayoutEffect выполняется синхронно после мутаций
  3. Вызов effectsuseEffect выполняется асинхронно после отрисовки браузера

Шаг 4: Отрисовка браузера (Paint)

Браузер отображает изменения на экране. После этого React вызывает useEffect колбэки.

Полная диаграмма процесса:

setCount(1)


┌─────────────────────┐
│ Render Phase │ ← Может быть прервана
│ (асинхронная) │
│ │
│ 1. Вызов компонента │
│ 2. Построение нового│
│ Fiber-дерева │
│ 3. Diffing с │
│ предыдущим деревом│
│ 4. Пометка изменений│
└─────────┬───────────┘


┌─────────────────────┐
│ Commit Phase │ ← Синхронная, непрерывная
│ (синхронная) │
│ │
│ 1. Применение │
│ изменений к DOM │
│ 2. useLayoutEffect │
│ 3. Планирование │
│ useEffect │
└─────────┬───────────┘


┌─────────────────────┐
│ Browser Paint │ ← Браузер рисует пиксели
└─────────┬───────────┘


┌─────────────────────┐
│ useEffect │ ← Выполняется после paint
└─────────────────────┘

Важные нюансы:

Батчинг (Batching):

React группирует несколько обновлений состояния в один рендер:

function handleClick() {
setCount((c) => c + 1); // Не вызывает немедленный рендер
setFlag((f) => !f); // Не вызывает немедленный рендер
// React ждёт завершения обработчика события
// и выполняет ОДИН рендер с обоими обновлениями
}

В React 18+ батчинг работает везде: в обработчиках событий, таймерах, промисах. В React 17 и ранее — только в обработчиках событий.

Приоритеты обновлений:

React 18+ с startTransition позволяет помечать обновления как несрочные:

import { startTransition } from 'react';

// Срочное: ввод текста должен быть мгновенным
setInputValue(e.target.value);

// Несрочное: фильтрация списка может подождать
startTransition(() => {
setFilter(e.target.value);
});

Что происходит с родительскими компонентами:

При изменении состояния в дочернем компоненте, родительский компонент не ре-рендерится (если только его состояние или контекст, который он потребляет, не изменились). Ре-рендерятся только компоненты, чей стейт изменился, и их потомки.

function Parent() {
console.log('Parent render');
return (
<div>
<ChildA /> {/* Изменил стейт → ре-рендерится */}
<ChildB /> {/* Не зависит от стейта ChildA → НЕ ре-рендерится */}
</div>
);
}

Итог: Кандидат упомянул правильные термины, но не смог выстроить чёткую последовательность шагов. Понимание фаз рендера и коммита важно для отладки производительности и правильного использования useEffect vs useLayoutEffect.

Вопрос 28. Реализовать фильтрацию списка с использованием debounce на чистом JavaScript

Таймкод: 00:36:25

Ответ собеседова: Неполный. Приступил к реализации задачи. Написал функцию debounce как декоратор, принимающий функцию и задержку, с использованием setTimeout и clearTimeout для отмены предыдущего вызова. Пытался получить элементы списка через querySelectorAll и перебрать их в цикле для получения textContent. Возникли ошибки: Cannot read property textContent of undefined — не все элементы находились селектором. Задача не была доведена до конца из-за ошибок с получением DOM-элементов и неполного понимания работы с коллекциями (псевдомассивами).

Правильный ответ:

Полная реализация:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Debounce Filter</title>
<style>
.hidden { display: none; }
.list-item { padding: 8px; border-bottom: 1px solid #eee; }
</style>
</head>
<body>
<input type="text" id="searchInput" placeholder="Поиск..." />
<ul id="list">
<li class="list-item">Яблоко</li>
<li class="list-item">Банан</li>
<li class="list-item">Апельсин</li>
<li class="list-item">Виноград</li>
<li class="list-item">Груша</li>
<li class="list-item">Киви</li>
<li class="list-item">Манго</li>
<li class="list-item">Персик</li>
</ul>

<script>
// 1. Функция debounce
function debounce(func, delay) {
let timeoutId;

return function (...args) {
// Отменяем предыдущий таймер
clearTimeout(timeoutId);

// Устанавливаем новый таймер
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}

// 2. Функция фильтрации
function filterList(query) {
const items = document.querySelectorAll('#list .list-item');
const normalizedQuery = query.toLowerCase().trim();

items.forEach((item) => {
const text = item.textContent.toLowerCase();

if (text.includes(normalizedQuery)) {
item.classList.remove('hidden');
} else {
item.classList.add('hidden');
}
});
}

// 3. Привязка к инпуту
const searchInput = document.getElementById('searchInput');

const debouncedFilter = debounce((event) => {
filterList(event.target.value);
}, 300);

searchInput.addEventListener('input', debouncedFilter);
</script>
</body>
</html>

Разбор ключевых моментов:

Функция debounce:

function debounce(func, delay) {
let timeoutId;

return function (...args) {
clearTimeout(timeoutId); // Сбрасываем предыдущий таймер
timeoutId = setTimeout(() => { // Устанавливаем новый
func.apply(this, args); // Вызываем с правильным контекстом
}, delay);
};
}

Принцип работы: каждый новый вызов сбрасывает предыдущий таймер. Функция выполнится только после того, как пользователь перестанет вводить текст на указанное время.

Работа с результатом querySelectorAll:

querySelectorAll возвращает NodeList — псевдомассив, по которому можно итерировать через forEach:

const items = document.querySelectorAll('#list .list-item');

// Правильные способы итерации:

// 1. forEach (рекомендуется)
items.forEach((item) => {
console.log(item.textContent);
});

// 2. for...of
for (const item of items) {
console.log(item.textContent);
}

// 3. Array.from + методы массива
Array.from(items)
.filter((item) => item.textContent.includes('Яблоко'))
.forEach((item) => item.classList.remove('hidden'));

// 4. Оператор расширения
[...items].forEach((item) => {
console.log(item.textContent);
});

Ошибка кандидата:

Ошибка Cannot read property textContent of undefined возникает при неправильной индексации:

// Неправильно: NodeList — не массив, индексация может не работать как ожидается
const items = document.querySelectorAll('.list-item');
for (let i = 0; i <= items.length; i++) { // Ошибка: <= вместо <
console.log(items[i].textContent); // items[items.length] === undefined
}

// Правильно:
for (let i = 0; i < items.length; i++) {
console.log(items[i].textContent);
}

Улучшенная версия с подсветкой совпадений:

function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}

function filterAndHighlight(query) {
const items = document.querySelectorAll('#list .list-item');
const normalizedQuery = query.toLowerCase().trim();

items.forEach((item) => {
const text = item.textContent.toLowerCase();

if (!normalizedQuery || text.includes(normalizedQuery)) {
item.classList.remove('hidden');
// Сбрасываем подсветку
item.innerHTML = item.textContent;
} else {
item.classList.add('hidden');
}
});
}

const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', (e) => {
debounce(() => filterAndHighlight(e.target.value), 300)();
});

Разница между debounce и throttle:

// Debounce: выполнить после паузы
// Печатает "Hello" → ждём 300ms → если не печатаем → выполняем
// Снова печатаем → сбрасываем таймер → ждём 300ms → выполняем

// Throttle: выполнять не чаще раза в N миллисекунд
// Печатает "Hello" → выполняем → игнорируем 300ms → выполняем следующий вызов

function throttle(func, delay) {
let lastCall = 0;

return function (...args) {
const now = Date.now();

if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}

Итог: Кандидат правильно реализовал логику debounce, но допустил ошибки при работе с DOM-коллекциями. Это базовый навык, необходимый для любого фронтенд-разработчика.