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

Fresher's Frontend Interview 🎉 | JavaScript | #reactjs (Mock) [Most Asked Questions -2023]

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

Сегодня мы разберем собеседование джуниор-фронтенд-разработчика, который уверенно оперирует базовыми определениями HTML, CSS и JavaScript, но испытывает существенные трудности с объяснением глубоких механизмов языка: контекста this, замыканий, event loop и правил хостинга для стрелочных функций. Кандидат демонстрирует начальный уровень знаний React — знает про виртуальный DOM, хуки и жизненный цикл, однако путается в деталях работы рефов, контролируемых компонентов и отличиях классовых и функциональных подходов. Встреча показывает типичный профиль выпускника с хорошей заученной теорией, которому критически не хватает практики и системного понимания процессов «под капотом» для решения нетривиальных задач.

Вопрос 1. В чем разница между HTML и HTML5 и какие новые возможности появились в HTML5?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:01:02"/>

Ответ собеседника: неполный. Кандидат назвал HTML языком разметки для структуры страницы. В HTML5 упомянул SVG, видео-теги и семантику, но упустил canvas, audio, localStorage, geolocation, веб-воркеры, новые типы input и валидацию форм.

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

HTML5 — это не просто новая версия языка разметки, а фундаментальное обновление веб-платформы, превратившее браузер в полноценную среду исполнения приложений. Главное отличие от HTML4/XHTML: отказ от жесткого SGML/XML-синтаксиса в пользу парсера с четко определенным алгоритмом обработки ошибок (HTML Living Standard), что обеспечивает интероперабельность браузеров.

1. Семантические элементы и структура документа Введен набор тегов для описания смысла контента, а не только его вида. Это критично для SEO, доступности (a11y) и поддерживаемости кода.

  • Структурные: <header>, <footer>, <nav>, <main>, <article>, <section>, <aside>.
  • Текстовые: <mark>, <time>, <data>, <details> / <summary> (нативный аккордеон без JS).
  • Пример: <article> обозначает самодостаточную композицию (пост, виджет), <section> — тематическую группировку с заголовком.

2. Нативная мультимедиа и графика Исключена необходимость во внешних плагинах (Flash, Silverlight).

  • <video> и <audio>: Поддержка кодеков (H.264, VP9, AV1, Opus), атрибуты controls, preload, poster, loop. Важно: использование <source> с type для фоллбэка.
  • <canvas>: Немедленный режим рисования (Immediate Mode) через JavaScript API (CanvasRenderingContext2D / WebGLRenderingContext). Подходит для игр, графиков, обработки изображений. В отличие от SVG (Retained Mode), не хранит DOM-дерево объектов, что дает высокую производительность при анимации тысяч объектов, но требует ручного управления перерисовкой.
  • SVG стал частью HTML (inline SVG), позволяя стилизовать вектор через CSS и манипулировать узлами из JS.

3. Расширенные возможности форм (Forms API) Декларативная валидация и новые виджеты ввода, снижающие зависимость от JS-библиотек.

  • Новые type: email, tel, url, number, range, date, datetime-local, month, week, time, color, search.
  • Атрибуты валидации: required, pattern (regexp), min/max/step, maxlength, minlength, multiple.
  • Атрибуты UX: placeholder, autofocus, autocomplete, list (связка с <datalist>), form (привязка инпута к форме за её пределами).
  • JS API: checkValidity(), reportValidity(), setCustomValidity(), событие invalid для кастомных сообщений об ошибках.

4. Клиентское хранение данных (Storage API) Решение проблемы ограниченных Cookies (4КБ, отправляются с каждым HTTP-запросом).

  • localStorage: Синхронное ключ-значение (строки), ~5-10 МБ на origin, без срока действия. Блокирует главный поток — не использовать для больших объемов в критических по производительности местах.
  • sessionStorage: Аналогичен, но очищается при закрытии вкладки (не окна браузера).
  • IndexedDB: Асинхронная NoSQL БД в браузере (транзакции, индексы, курсоры, сотни МБ/ГБ). База для офлайн-приложений и сложного кэширования.
  • Cache API / Service Workers: Программируемый сетевой прокси для перехвата fetch, реализации стратегий кэширования (Stale-While-Revalidate, Cache-First) и работы офлайн (PWA).

5. JavaScript API для параллелизма и устройств

  • Web Workers: Запуск скриптов в фоновых потоках (отдельный глобальный контекст, нет доступа к DOM). Обмен данными через postMessage (structured clone algorithm) или SharedArrayBuffer / Atomics для разделяемой памяти. Решают проблему "замораживания" UI при тяжелых вычислениях (парсинг JSON, криптография, ML).
  • Geolocation API: navigator.geolocation.getCurrentPosition() / watchPosition(). Требует HTTPS и пользовательского разрешения. Возвращает широту, долготу, точность, высоту, скорость.
  • WebSockets / Server-Sent Events (SSE): Двусторонний (ws://, wss://) и односторонний (с сервера) каналы связи поверх TCP без накладных расходов HTTP/1.1. В HTTP/2/3 приоритет отдается стримам, но WS остается стандартом для реального времени.
  • Fetch API: Современная замена XMLHttpRequest на основе Promise, поддержка Streams API (response.body.getReader()) для стриминговой обработки ответов.
  • History API: pushState, replaceState, событие popstate — основа для SPA-роутинга без хешей (#).

6. Прогрессивные веб-приложения (PWA) и офлайн Комбинация Service Worker, Web App Manifest (JSON с метаданными: иконки, start_url, display: standalone) и HTTPS позволяет "устанавливать" сайт на рабочий стол/экран телефона, запускать в полноэкранном режиме и работать без сети.

7. Доступность (ARIA) и международность HTML5 нативно поддерживает многие ARIA-роли через семантику (не нужно role="button" на <button>). Атрибуты lang, dir, translate улучшают работу скринридеров и переводчиков.

Резюме для Senior/Tech Lead: На собеседовании важно подчеркнуть не просто список тегов, а архитектурные последствия: декларативность вместо императивного JS (валидация, детали), офлайн-первый подход (Service Workers, IndexedDB), производительность (Web Workers, requestAnimationFrame для Canvas, ленивая загрузка loading="lazy"), безопасность (CSP, crossorigin, integrity для SRI, SameSite cookies) и поддержка стандартов (WHATWG Living Standard vs W3C Snapshots).

Вопрос 2. Что такое блочная модель (Box Model) в CSS?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:01:31"/>

Ответ собеседника: правильный. Кандидат правильно перечислил четыре части: контент, внутренние отступы (padding), границы (border) и внешние отступы (margin), и объяснил назначение каждой.

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

Блочная модель (CSS Box Model) — это фундаментальный механизм визуального форматирования, описывающий, как браузер вычисляет геометрию прямоугольных боксов, генерируемых элементами документа. Понимание нюансов этой модели — ключ к предсказуемой верстке и отладке проблем с раскладкой (layout).

1. Анатомия бокса: 4 вложенных области Каждый бокс состоит из четырех концентрических прямоугольников (edges), каждый из которых имеет свои размеры (width/height) и координаты:

  • Content Edge (Область контента): Содержит реальное содержимое (текст, изображение, дочерние элементы). Размеры определяются width / height (в режиме content-box).
  • Padding Edge (Внутренние отступы): Пространство между контентом и рамкой. Фон элемента (background-color, background-image) распространяется на эту область. Управляется padding-top/right/bottom/left.
  • Border Edge (Граница): Рамка вокруг padding. Имеет толщину (border-width), стиль (solid, dashed и др.) и цвет. Может быть скруглена через border-radius. Важно: border-image рисуется поверх этой области.
  • Margin Edge (Внешние отступы): Прозрачное пространство, отделяющее бокс от соседей. Фон элемента не прорисовывается здесь. Ключевая особенность: схлопывание вертикальных маржинов (см. ниже).

2. Два режима расчета размеров: box-sizing Это критическое свойство, меняющее семантику width / height.

Режим (box-sizing)Что включает width / heightФормула внешней ширины (outer width)Применение
content-box (default)Только Contentwidth + padding + border + marginИсторический стандарт W3C. Сложен для адаптивной верстки: добавление padding расширяет элемент.
border-boxContent + Padding + Borderwidth + marginДе-факто стандарт современной разработки. Размер элемента предсказуем, padding и border "съедают" место внутри заданной ширины.

Рекомендация (Best Practice): Глобальный сброс в начале проекта:

*, *::before, *::after {
box-sizing: border-box;
}

Примечание: ::before / ::after наследуют box-sizing от родителя только в современных браузерах, явное указание в универсальном селекторе гарантирует консистентность.

3. Схлопывание маржинов (Margin Collapsing) Уникальное поведение вертикальных маржинов (margin-top / margin-bottom) в блочном форматирующем контексте (BFC). Горизонтальные маржины (left/right) никогда не схлопываются.

Правила:

  1. Соседние братья: Маржины объединяются. Итоговый отступ = max(marginBottom_1, marginTop_2).
  2. Родитель и первый/последний ребенок: Маржины "просачиваются" наружу, если у родителя нет:
    • padding-top/bottom > 0
    • border-top/bottom > 0
    • Созданного BFC (см. ниже)
    • overflow: hidden/auto/scroll (классический трюк)
    • display: flow-root (современный способ создать BFC без сайд-эффектов)
  3. Пустые блоки: Если у элемента нет контента, padding, border, height/min-height — его margin-top и margin-bottom схлопываются друг с другом и с внешними маржинами.

Последствие: "Пропадающие" отступы у первого ребенка внутри контейнера — классическая ошибка новичка. Решение: padding: 1px у родителя или display: flow-root.

4. Блочные (Block-level) vs Строчные (Inline-level) боксы Модель ведет себя по-разному в зависимости от display (внешнего типа отображения):

  • Block boxes (div, p, h1...): Занимают всю доступную ширину, переносятся на новую строку. Уважают все 4 стороны padding/border/margin. Высота рассчитывается автоматически на основе контента.
  • Inline boxes (span, a, strong...): Не начинаются с новой строки.
    • width / heightигнорируются.
    • Вертикальные margin / padding / borderвизуально прорисовываются (фон, рамка), но не влияют на line-height и не сдвигают соседние строки (могут перекрывать их).
    • Горизонтальные margin / padding / border — работают нормально, сдвигая соседей по строке.
  • Inline-block / inline-flex / inline-grid: Внешне ведут себя как inline (в строку), внутренне — как block (уважают width/height/вертикальные отступы). Базовая линия (vertical-align: baseline по умолчанию) часто вызывает неожиданный отступ снизу (решение: vertical-align: top/middle/bottom или font-size: 0 у родителя).

5. Форматирующие контексты (Formatting Contexts) и Box Model Блочная модель не существует в вакууме. Она работает внутри Formatting Context (FC), который задает правила взаимодействия боксов:

  • BFC (Block Formatting Context): Стандартный поток для блоков. Здесь действует схлопывание маржинов. Создается: float, overflow != visible, display: flow-root/flex/grid/table-cell, position: absolute/fixed.
  • IFC (Inline Formatting Context): Для строчных элементов. Здесь работают line-height, vertical-align, text-align.
  • FFC (Flex Formatting Context) / GFC (Grid Formatting Context): Дочерние элементы (flex/grid items) становятся flex/grid контейнерами (block-like). Для них отключено схлопывание маржинов, float не работает, вертикальные маржины не схлопываются с родителем. margin: auto поглощает свободное пространство (центрирование).

6. Специфические кейсы для Senior-уровня

А. box-sizing и min-width / max-width В режиме border-box ограничения min/max-width применяются к Content Box (по спефикации), но браузеры исторически применяли их к Border Box. Современные браузеры (Chrome, Firefox, Safari) приводят поведение к спеке: min-width ограничивает контент. Это может ломать верстку, если полагаться на старую логику.

Б. margin: auto и центрирование

  • В BFC: margin: 0 auto центрирует блок горизонтально (требуется заданная width).
  • Во Flex/Grid: margin: auto поглощает всё свободное пространство в соответствующем направлении (мощный инструмент для выравнивания отдельных айтемов без justify-self).

В. Отрицательные маржины (margin: -Npx)

  • Сдвигают элемент визуально и геометрически (влияют на позицию следующих элементов в потоке).
  • В Flex/Grid: позволяют "вытягивать" элемент за границы контейнера или создавать перекрытия (overlapping) без position: absolute.
  • Не работают на inline элементах (только горизонтальные для inline-block/inline-flex).

Г. outline vs border outline не входит в Box Model. Не занимает место, не влияет на размеры и позиционирование соседних элементов, рисуется "поверх" всего (может быть не прямоугольным на инлайн-элементах с переносами строк). Идеален для фокус-стилей (:focus-visible), не ломая раскладку.

Д. border-radius и клиппинг border-radius скругляет Border Edge. Контент и фон обрезаются по этой кривой (background-clip: border-box по умолчанию). Если padding недостаточен, контент может наезжать на скругление — использовать padding >= border-radius или overflow: hidden (создает BFC).

Е. DevTools и отладка В инспекторе (Elements -> Computed / Layout) визуализируется модель: синий (content), зеленый (padding), желтый (border), оранжевый (margin). Практика: При верстке по пиксель-перфект макетам всегда сверяй Computed width/height с дизайном. Разница в 1-2px часто кроется в border-box vs content-box, субпиксельном рендеринге (transform: translateZ(0) для принудительного GPU-слоя) или схлопывании маржинов.

Вопрос 3. В чем разница между ID и классом в CSS и какой у них приоритет (специфичность)?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:02:03"/>

Ответ собеседника: правильный. Кандидат верно указал, что ID должен быть уникален, класс — нет; селекторы: # для ID, . для класса. ID имеет более высокий приоритет, чем класс.

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

Разница между ID и классом — это не просто синтаксис селекторов, а фундаментальное архитектурное решение, влияющее на поддерживаемость, масштабируемость и производительность CSS-архитектуры. На уровне Senior/Tech Lead ожидается понимание Specificity (специфичности), Cascade (каскада), Origin & Importance и современных практик управления сложностью (BEM, CSS Layers, Utility-first).

1. Семантика и ограничения HTML

ХарактеристикаID (id="unique")Класс (class="reusable")
УникальностьСтрого один элемент на страницу. Дублирование — невалидный HTML (браузеры простят, но document.getElementById вернет первый, JS-селекторы могут сломаться).Множественные элементы. Один элемент может иметь несколько классов (class="btn btn--primary btn--large").
Якоря и навигацияИспользуются для фрагментных ссылок (<a href="#section">), scrollIntoView(), :target псевдокласса.Не используются для навигации.
JavaScript APIdocument.getElementById() — самый быстрый нативный селектор (прямой доступ к хеш-таблице браузера).document.querySelectorAll('.class') — живая/статическая NodeList, медленнее.
CSS Селектор#my-id (вес 1-0-0).my-class (вес 0-1-0)
Специфичность (Specificity)Высокая. Перебивает любое количество классов, тегов и атрибутов.Средняя. Перебивает теги и псевдоэлементы, проигрывает ID и инлайн-стилям.

2. Математика специфичности: Система счисления (A, B, C) Специфичность — это не число, а кортеж (tuple) из трех значений, сравниваемых лексикографически (слева направо).

  • A (ID count): Количество ID-селекторов (#header).
  • B (Class/Attr/Pseudo count): Количество классов (.btn), атрибутов ([type="text"]), псевдоклассов (:hover, :nth-child).
  • C (Type/Pseudo-element count): Количество тегов (div), псевдоэлементов (::before).

Примеры весов:

  • #sidebar(1, 0, 0)
  • .sidebar(0, 1, 0)
  • div.sidebar(0, 1, 1)
  • .sidebar .widget .title(0, 3, 0)всего 3 класса, но вес всё равно (0, 3, 0) < (1, 0, 0)
  • #sidebar .widget(1, 1, 0)

Критический момент: Никакое количество классов (B) не перебьет один ID (A). (0, 255, 0) < (1, 0, 0). Это ловушка "специфичностных войн", когда разработчики пишут .header .nav .list .item .link:hover чтобы перебить #menu.

3. Полная иерархия приоритета каскада (Cascade Sorting Order) Специфичность — только 4-й этап из 6 в алгоритме каскада (CSS Cascading and Inheritance Level 4). Браузер решает конфликт свойств в таком порядке:

  1. Origin & Importance (Происхождение и !important):
    • User Agent (браузер) normal
    • User (настройки пользователя) normal
    • Author (разработчик) normal
    • Author !important
    • User !important
    • User Agent !important
    • Transition/Animation origins (специфичные слои)
  2. Context (Контекст / Shadow DOM / Layers): Стили внутри Shadow DOM изолированы. CSS Cascade Layers (@layer) позволяют управлять приоритетом групп стилей до специфичности.
  3. Inline Styles (style="color: red") — вес (1, 0, 0, 0) в расширенной нотации, побеждают любой ID в Author origin.
  4. Specificity (Специфичность селектора) — наш кортеж (A, B, C).
  5. Order of Appearance (Порядок в коде) — побеждает последний объявленный (для одинаковой специфичности).
  6. Inheritance (Наследование) — только если свойство наследуемое (color, font) и не задано явно.

4. Архитектурные последствия и Best Practices

А. Избегайте ID в CSS (Methodology: BEM, OOCSS, ITCSS, Utility-First) Использование #id для стилизации считается антипаттерном в современной разработке:

  • Нереиспользуемость: Стили привязаны к уникальному узлу DOM.
  • Непредсказуемая специфичность: Любая модификация компонента требует !important или еще более специфичных селекторов.
  • Связность с JS/HTML: ID часто нужны для JS-хуков (data-testid, id="modal-root"). Смешивание стилистических и функциональных хуков создает жесткую связность. Решение: префиксы js- для JS, data-* для тестов, классы для CSS.

Б. Управление специфичностью: Стратегия "Low Specificity"

  • Стилизация только классами (вес 0-1-0).
  • Избегание вложенности в препроцессорах (SCSS): .parent { .child { ... } }.parent .child (вес 0-2-0). Использовать плоские селекторы: .parent__child (БЭМ).
  • Псевдоклассы (:hover, :focus, :not()) увеличивают B. :not(.class) добавляет вес класса.

В. Современные инструменты контроля каскада

  1. CSS Cascade Layers (@layer)Game Changer (поддержка во всех evergreen браузерах с 2022).

    @layer reset, base, components, utilities; /* Порядок слоев определяет приоритет */

    @layer components {
    .btn { color: blue; } /* Вес внутри слоя не важен, если слой ниже */
    }

    @layer utilities {
    .text-red { color: red !important; } /* !important в слое utilities побеждает components */
    }

    Слои позволяют изолировать приоритеты архитектурных уровней (reset < base < components < utilities), делая специфичность селекторов внутри слоя второстепенной.

  2. :where() и :is() — манипуляция специфичностью

    • :where(selector)всегда специфичность 0-0-0. Идеально для дефолтов библиотек/ресетов, которые легко переопределять.
      /* Вес 0-0-0, легко перебить любым классом */
      :where(.reset) { margin: 0; }
    • :is(selector1, selector2) — специфичность = максимальная из аргументов. Удобно для группировки, но повышает вес.
      :is(#id, .class) { color: red; } /* Вес = (1, 0, 0) из-за #id */
  3. CSS Custom Properties (Variables) — обходят каскад. Переменные наследуются, а не каскадируются по специфичности. Переопределение --color в .btn--primary работает независимо от веса селектора родителя.

5. Специфичность в Shadow DOM и Scoped CSS

  • Стили внутри Shadow DOM не выходят наружу и не заходят внутрь (кроме наследуемых свойств и CSS Custom Properties через ::part / ::slotted / кастомные свойства).
  • Специфичность считается локально внутри теневого дерева. Селектор :host имеет специфичность (0, 0, 0) (как тег), но побеждает внешние стили за счет изоляции Origin/Context.
  • ::part(name) позволяет стилизовать внутренние части извне с весом (0, 1, 0) (как класс).

6. Чек-лист для Code Review (Senior Level)

  1. Есть ли #id в CSS-файлах? (Только если это утилита типа #skip-link для a11y).
  2. Не превышает ли глубина вложенности селекторов 1-2 уровней (БЭМ: .block__element--modifier)?
  3. Используется ли @layer для разделения Reset / Base / Components / Utilities?
  4. Применяется ли :where() для сторонних виджетов/ресетов?
  5. Нет ли !important в Author normal origin (кроме утилит в верхнем слое или переопределения инлайн-стилей из JS-библиотек)?
  6. Используются ли CSS переменные для темизации вместо переопределения классов?

Резюме: ID — это уникальный идентификатор для DOM/JS/якорей (вес 1-0-0). Класс — многократно используемый хук для стилей и логики (вес 0-1-0). Профессиональный подход: стилизуем только классами (или :where()), управляем каскадом через @layer и архитектуру (БЭМ/Utility), исключая специфичность из уравнения сложности.

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

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:02:35"/>

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


Правильный ответ: Детальный технический разбор

Различия фундаментальны и затрагивают область видимости (Scope), жизненный цикл (Hoisting / TDZ), связь с глобальным объектом и семантику изменяемости. Это базовый вопрос для понимания замыканий, работы циклов и архитектуры модулей (ESM).


1. Область видимости (Scope) — Ключевое различие

Характеристикаvarlet / const
Тип областиФункциональная (Function-scoped)Блочная (Block-scoped)
ГраницыГраницы функции (function() {}) или глобальный объект. Блоки if, for, {}не создают область.Границы ближайшего блока { ... } (фигурные скобки).
Пример утечкиif (true) { var x = 1 } console.log(x) // 1 — переменная "утекает" наружу.if (true) { let y = 1 } console.log(y) // ReferenceError — переменная изолирована в блоке.

Код-пример (Классическая ловушка цикла):

// var: общая переменная для всех итераций
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i), 100); // Вывод: 3, 3, 3
}

// let: новая привязка (binding) на КАЖДОЙ итерации (создается свежий лексический контекст)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log('let:', i), 100); // Вывод: 0, 1, 2
}

Техническая деталь: Спецификация ES6+ требует, чтобы let в заголовке for создавал новую лексическую среду (Lexical Environment) для каждой итерации. var использует одну среду для всего цикла.


2. Хостинг (Hoisting) и Временная мертвая зона (TDZ — Temporal Dead Zone)

Это механизм создания привязки (Binding Creation) vs инициализации (Initialization).

Этапvarlet / const
Создание (Creation)При входе в контекст выполнения (функция/глобал).При входе в контекст выполнения (блок/модуль).
ИнициализацияСразу же: undefined.Нет. Находится в TDZ до строки декларации.
Доступ до декларацииconsole.log(x); var x = 1; // undefined (без ошибки, но логическая ошибка).console.log(y); let y = 1; // ReferenceError: Cannot access 'y' before initialization

TDZ — это не про "поднятие", а про запрет доступа. Движок знает о переменной с начала блока, но выбрасывает ReferenceError при любом обращении (чтение/запись) до строки let/const.

function test() {
console.log(typeof x); // "undefined" (var не в TDZ, просто не инициализирован)
console.log(typeof y); // ReferenceError (let в TDZ, typeof не спасает!)
var x = 1;
let y = 2;
}

Важно: typeof для необъявленной переменной (typeof z) безопасно вернет "undefined". Для let/const в TDZ — бросит ошибку.


3. Переобъявление (Re-declaration) в одной области видимости

var a = 1;
var a = 2; // OK

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

const c = 1;
const c = 2; // SyntaxError

Нюанс: В разных блоках — разные области видимости, переобъявление let/const допустимо (теневание / shadowing).


4. Переприсваивание (Re-assignment) и Иммутабельность

Ключевое словоПереприсваиваниеОсобенности
var✅ Разрешено
let✅ Разрешено
constЗапрещено (TypeError: Assignment to constant variable)Привязка (binding) иммутабельна, НЕ значение.

Критический нюанс const (Shallow Immutability):

const obj = { a: 1 };
obj.a = 2; // ✅ OK: мутируем содержимое объекта
obj = { a: 3 }; // ❌ Error: меняем саму привязку (ссылку)

const arr = [];
arr.push(1); // ✅ OK
// arr = []; // ❌ Error

Best Practice: По умолчанию используйте const. let — только если переменная обязательно должна быть переприсвоена (счетчик, свап значений, аккумулятор в reduce). Это упрощает рефакторинг и анализ кода (SSA — Static Single Assignment).


5. Глобальный объект (Global Object Property)

В браузере глобальный объект — window, в Node.js — globalThis.

var globalVar = 'I am var';
let globalLet = 'I am let';
const globalConst = 'I am const';

console.log(window.globalVar); // "I am var" -> Свойство глобального объекта
console.log(window.globalLet); // undefined -> НЕ свойство глобального объекта
console.log(window.globalConst); // undefined

// Но доступны в глобальной области видимости скрипта:
console.log(globalLet); // "I am let"

Технически: var создает свойство на Global Object (атрибут configurable: false, нельзя delete). let/const создают привязку в Global Lexical Environment (Script/Module Environment Record), не загрязняя глобальный объект. Это критично для изоляции модулей и предотвращения коллизий имен.


6. Особенности в циклах for-in / for-of

// for-in / for-of с var — одна переменная на весь цикл
for (var k in {a:1, b:2}) { setTimeout(() => console.log(k), 0); } // 'b', 'b'

// for-in / for-of с let/const — НОВАЯ переменная на КАЖДУЮ итерацию
for (let k in {a:1, b:2}) { setTimeout(() => console.log(k), 0); } // 'a', 'b'

Спецификация: for (let x...) неявно оборачивает тело цикла в блок, создавая свежую лексическую среду для замыканий.


7. const в объектах/массивах: Object.freeze vs const

Частая ошибка junior/middle: путают константу ссылки с глубокой иммутабельностью.

  • const — защита от перезаписи переменной (=).
  • Object.freeze(obj) — поверхностная защита от мутации свойств (не глубокая!).
  • Для глубокой иммутабельности нужны библиотеки (Immer, Immutable.js) или structuredClone / рекурсивный Object.freeze.

8. Best Practices / Modern Standard (ES2015+ / ESM)

  1. Default to const. 80-90% переменных не должны менять ссылку.
  2. Use let только при необходимости реассигнмента (счетчики, флаги, переключение состояния).
  3. Never use var в современном коде (ESM, TypeScript, React/Vue/Node.js).
    • Исключение: легаси-код без транспиляции, специфичные хитрости с function scope (редко).
  4. TypeScript / Linters (ESLint):
    • prefer-const (ошибка, если let не перезаписывается).
    • no-var (запрет var).
    • const для объектов/массивов не защищает от мутаций → используйте Readonly<T> в TS или as const (TypeScript 3.4+).
// TypeScript 'as const' — глубокая readonly инференция типов
const config = { api: { url: '...', timeout: 5000 } } as const;
// config.api.url = 'hack' // TypeError на этапе компиляции
// config = {} // TypeError

Резюме для собеседования (Cheat Sheet)

Фичаvarletconst
ScopeFunctionBlockBlock
HoistingДа (init undefined)Да (но TDZ)Да (но TDZ)
Re-declaration❌ SyntaxError❌ SyntaxError
Re-assignment❌ TypeError
Global Object Prop✅ (window.var)
Loop BindingОдна на всеНовая на итерациюНовая на итерацию
TDZНетЕстьЕсть
UsageLegacy / AvoidMutable bindingDefault choice

Senior Insight: Понимание TDZ и блочной области видимости критично для отладки замыканий в циклах, работы с модулями (ESM — модули в строгом режиме по умолчанию, топ-левел let/const не попадают в window), и для написания кода, поддающегося статическому анализу и.tree-shaking'у.

Вопрос 5. Для чего нужны медиа-запросы в CSS?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:03:29"/>

Оценка ответа кандидата: правильный. Кандидат верно ответил, что медиа-запросы используются для создания адаптивных страниц, задавая условия (min-width, max-width) и пиша CSS внутри.


Правильный ответ: От Media Queries Level 3 к Modern Responsive Design (Level 4/5)

Медиа-запросы (Media Queries) — это механизм условного применения CSS на основе характеристик устройства вывода (User Agent) и окружения пользователя. Это фундамент Responsive Web Design (RWD), позволяющий одному HTML-коду адаптироваться под бесконечное множество вьюпортов, устройств и предпочтений пользователя.


1. Анатомия Media Query (Синтаксис Level 3/4)

@media [not | only] <media-type> [and] <media-condition> {
/* CSS Rules */
}
КомпонентЗначенияОписание
Media Typescreen, print, speech, all (по умолчанию)Тип устройства. all подразумевается, если опущен. only скрывает стили от старых браузеров (не парсящих MQ).
Media Featurewidth, height, orientation, resolution, hover, pointer, prefers-color-scheme, prefers-reduced-motion...Характеристика для проверки.
Logical Operatorsand, , (или), notКомбинирование условий. Запятая = логическое ИЛИ (список запросов).

Пример (Modern Syntax Level 4 — Range Syntax):

/* Старый синтаксис (все еще валиден) */
@media screen and (min-width: 768px) and (max-width: 1023px) { ... }

/* Новый синтаксис Level 4 (чище, без and) */
@media (width >= 768px) and (width <= 1023px) { ... }
@media (480px <= width < 768px) { ... } /* Чейнинг сравнений */

2. Ключевые Media Features (Что мы можем проверять)

А. Вьюпорт и геометрия (Layout)
  • width / height / aspect-ratio — размеры области просмотра (layout viewport).
  • orientation: portrait | landscape — ориентация экрана.
  • Best Practice: Использовать min-width / max-width (mobile-first / desktop-first), избегать device-width (устарел, не работает корректно на мобильных с зумом).
Б. Качество дисплея (Graphics)
  • resolution (dpi, dppx) — плотность пикселей. Критично для Retina / HiDPI (спрайты, image-set, srcset).
  • color-gamut: srgb | p3 | rec2020 — поддержка широкого цветового охвата (P3 на iPhone/Mac).
  • dynamic-range: standard | high — поддержка HDR.
В. Взаимодействие (Interaction Media Features — Level 4) — Критично для UX
FeatureЗначенияUse Case
hovernone, hoverЕсть ли у устройства наведение (мышь/трекпад vs тачскрин). @media (hover: hover) { .dropdown:hover ... }
pointernone, coarse, fineТочность указателя. coarse = палец (крупные тач-таргеты), fine = мышь.
any-hover / any-pointer...Проверяет любое доступное устройство ввода (например, ноутбук с тачскрином + мышью).
Г. Пользовательские предпочтения (User Preference Media Features — Level 5) — Accessibility & UX

Это современный стандарт (поддержка во всех evergreen браузерах).

  • prefers-color-scheme: light | dark — системная тема. Основа для Dark Mode без JS.
  • prefers-reduced-motion: reduce | no-preference — пользователь хочет минимизировать анимации (vestibular disorders). Обязательно для WCAG 2.1.
  • prefers-contrast: more | less | custom — высокий/низкий контраст.
  • prefers-reduced-transparency: reduce — уменьшение прозрачности (iOS/macOS).
  • forced-colors: active | none — Windows High Contrast Mode (требует forced-color-adjust: none для сохранения брендовых цветов).

3. Архитектурные подходы: Mobile-First vs Desktop-First

СтратегияСинтаксисПлюсыМинусы
Mobile First (Рекомендуемый стандарт)Базовые стили = мобайл. @media (min-width: ...) для расширения.Меньше кода, быстрее рендеринг на слабых устройствах, прогрессивное улучшение.Требует дисциплины.
Desktop FirstБазовые стили = десктоп. @media (max-width: ...) для обрезания.Привычно для старых проектов.Передает лишний CSS на мобильные, сложнее поддерживать.

Современный подход (Intrinsic Web Design): Минимизация жестких брейкпоинтов (breakpoints) в пользу fluid-верстки:

  • clamp(), min(), max() для типографики и отступов.
  • CSS Grid / Flexbox (auto-fit, minmax, fr) для раскладки.
  • Container Queries (@container) — следующий эволюционный шаг (см. ниже).

4. Продвинутые темы (Senior / Tech Lead Level)

А. Container Queries (@container) — CSS Containment Level 3

Проблема: Media Queries смотрят на viewport. Компонент не знает, в каком контейнере он лежит (сайдбар, модалка, главная колонка). Решение: Запрос стилей на основе размера родительского контейнера.

.card-container {
container-type: inline-size; /* Активируем контейнер */
container-name: card; /* Опционально, для вложенности */
}

@container card (min-width: 400px) {
.card { display: flex; } /* Стили зависят от ширины .card-container, а не окна! */
}

Статус: Baseline 2023 (Chrome 105+, FF 110+, Safari 16+). Game Changer для Design Systems и микрофронтендов.

Б. media="print" и @media print

Отдельный медиа-тип для печати. Критично для: скрытия навигации/рекламы, разворота страниц (@page), корректных разрывов (break-inside: avoid), экономии чернил.

В. CSS-in-JS / Tailwind / CSS Modules — как это работает внутри?
  • Tailwind: md:flex@media (min-width: 768px) { .md\\:flex { display: flex } }.
  • CSS-in-JS (styled-components, emotion): Инъекция @media в <style> теги в рантайме или билд-тайме.
  • CSS Modules / PostCSS: Постпроцессинг (например, postcss-custom-media для кастомных медиа-запросов: @custom-media --bp-tablet (width >= 768px);).
Г. Производительность и парсинг
  • Медиа-запросы (@media) не блокируют парсинг HTML (в отличие от <link media="..."> который блокирует рендеринг, если условие истинно).
  • Слишком много мелких @media увеличивает размер CSSOM и время рекалькуляции стилей (Recalc Style). Группируйте правила.
  • @media (prefers-reduced-motion: reduce) позволяет браузеру полностью отключить анимации на уровне компоситора (performance win).

5. Чек-лист Best Practices (Code Review Guide)

  1. Mobile First: Базовые стили без @media. Брейкпоинты только min-width.
  2. Relative Units: Брейкпоинты в rem/em (не px!), чтобы respecter пользовательский зум/шрифт.
    /* Хорошо: масштабируется с базовым шрифтом пользователя */
    @media (min-width: 48rem) { ... } /* 768px при 16px base */
  3. Interaction Features: Всегда проверяйте hover/pointer для выпадающих меню, тултипов, ховер-эффектов карточек.
    @media (hover: hover) and (pointer: fine) {
    .card:hover { transform: scale(1.02); } /* Только для мыши */
    }
  4. Accessibility First:
    @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
    }
    }
  5. Dark Mode: Используйте CSS Custom Properties внутри @media (prefers-color-scheme: dark), а не дублирование селекторов.
  6. Container Queries: Для переиспользуемых UI-компонентов (Card, Form, Widget) — мигрируйте на @container.
  7. Print Stylesheet: Не забывайте @media print для контентных страниц (статьи, чеки, отчеты).

Резюме

Медиа-запросы — это условная логика CSS Runtime. Они эволюционировали от простой проверки ширины экрана (Level 3) к мощному API для запроса возможностей устройства (Interaction), предпочтений пользователя (Accessibility/UX) и контекста компонента (Container Queries).

Senior Mindset: Не пишите @media для каждого компонента. Используйте Intrinsic Layout (Flex/Grid/Clamp) как дефолт, @media — для глобальных сдвигов лайаута (сайдбар/хедер/футер), @container — для адаптации компонентов, @media (prefers-...) — для уважения к пользователю.

Вопрос 6. В чем разница между margin и padding?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:03:52"/>

Оценка ответа кандидата: неправильный. Кандидат ошибся: назвал margin пространством между границей и заголовком, а padding — между содержимым и внешним блоком. Правильно: margin — внешний отступ за границей, padding — внутренний отступ между контентом и границей.


Правильный ответ: Боксовая модель — внешнее vs внутреннее пространство

Разница фундаментальна: padding расширяет фон и область клика элемента, margin — нет. margin управляет расстоянием между боксами, padding — пространством внутри бокса.


1. Визуальная и геометрическая разница

Характеристикаpadding (Внутренний отступ)margin (Внешний отступ)
ПоложениеВнутри border-box (между content и border).Снаружи border-box (после border).
Фон (background)Покрывается фоном элемента (background-color, background-image).Прозрачен. Всегда показывает фон родителя.
Область клика / ХоверВходит в hit-test область. Клик по паддингу = клик по элементу.Не входит. Клик по маржину — это клик по родителю.
Схлопывание (Collapsing)Никогда не схлопываются. Вертикальные паддинги соседних элементов суммируются.Вертикальные маржины схлопываются (max(m1, m2)) в Block Formatting Context.
Отрицательные значенияНедопустимы (invalid value).Допустимы. Отрицательный margin сдвигает элемент, перекрывая соседей или вынося за пределы родителя.
Влияние на width/heightВ box-sizing: border-box (стандарт) — съедает место контента. В content-boxувеличивает общий размер бокса.Никогда не влияет на вычисленные width/height элемента. Влияет на позицию соседних элементов.
Инлайн-элементы (display: inline)Работает по горизонтали (сдвигает соседей). По вертикали — визуально рисуется (фон, рамка), но НЕ сдвигает соседние строки (line-box height не меняется).Работает только по горизонтали (margin-left/right). Вертикальные (top/bottom) — игнорируются (не влияют на layout).
Проценты (%)Рассчитываются от width содержащего блока (даже для padding-top/bottom!).Рассчитываются от width содержащего блока (и для margin-top/bottom тоже!).

2. Критические нюансы для Senior/Tech Lead

А. Схлопывание маржинов (Margin Collapsing) — Главная ловушка

Происходит только для вертикальных маржинов (margin-top, margin-bottom) в Block Formatting Context (BFC). Не схлопываются, если у родителя или элемента:

  • overflow: hidden | auto | scroll | clip (создает BFC).
  • display: flow-root (современный способ создать BFC без сайд-эффектов).
  • display: flex | grid | inline-flex | table-cell (Flex/Grid контейнеры создают BFC для детей).
  • border / padding > 0 у родителя (барьер между маржинами ребенка и родителя).
  • position: absolute | fixed / float: left | right.

Пример ошибки: У .parent нет паддинга/бордера. У .child margin-top: 20px. Ожидаемо: отступ 20px от верха родителя. Реальность: маржин "вылезает" наружу, отодвигая родителя от своего соседа.

Б. Отрицательные маржины (Negative Margins) — Инструмент Layout
/* 1. Вытягивание элемента за границы контейнера (очень частый паттерн для фулл-ширинных баннеров в контейнере) */
.full-width-banner {
margin-left: calc(50% - 50vw); /* или -var(--container-padding) */
margin-right: calc(50% - 50vw);
width: 100vw;
max-width: none;
}

/* 2. Перекрытие элементов (Overlapping) */
.card { margin-bottom: -20px; } /* Следующая карта наезжает на тень этой */

/* 3. Центрирование флекс-элементов с отступами (Gap alternative) */
.item { margin: 10px; } /* Гуттеры */
.container { margin: -10px; } /* Компенсация внешних гуттеров */
В. Padding и box-sizing — Стандарт де-факто
*, *::before, *::after {
box-sizing: border-box; /* width = content + padding + border */
}

Без этого padding ломает адаптивную верстку (width: 50% + padding: 20px = overflow). С border-box padding безопасен для сеток.

Г. Процентные отступы — Ловушка вертикали
.parent { width: 500px; height: 300px; }
.child { padding-top: 10%; margin-top: 10%; }
/* Оба = 50px (10% от 500px ширины родителя), НЕ от 300px высоты! */

Используется для сохранения соотношения сторон (Aspect Ratio):

.video-wrapper {
width: 100%;
padding-top: 56.25%; /* 16:9 */
position: relative;
height: 0; /* Важно: высота 0, весь размер за счет padding */
}
.video-wrapper iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }

Modern way: aspect-ratio: 16/9 (CSS 2021), но padding-top хак все еще актуален для старых браузеров и сложных кейсов.

Д. Инлайн-контекст (Inline Formatting Context)
<span style="padding: 10px; background: red;">Text</span>
<span style="margin: 10px; background: blue;">Text</span>
  • padding: Фон расширится на 10px во все стороны. Текст не сдвинется вертикально (может налезть на строку выше/ниже).
  • margin: Горизонтальные отступы сдвинут соседа. Вертикальные — проигнорируются браузером (не применятся).
Е. margin: auto — Магия центрирования и Flex/Grid
  • Block (Flow): margin: 0 auto центрирует по горизонтали (только если задана width/max-width). Вертикально auto = 0.
  • Flex/Grid: margin: auto на flex-item поглощает свободное пространство.
    • margin-left: auto → прижимает элемент вправо.
    • margin: auto → центрирует по обоим осям (в отличие от block layout!). Это нативный способ центрирования модалок внутри флекс-контейнера без justify-content/align-items на родителе.
Ж. Collapsing Margins vs Flex/Grid Gap

В современной верстке (display: flex/grid) маржины детей не схлопываются и не "вылезают" из контейнера. Лучше использовать gap (grid-gap / gap):

.grid { display: grid; gap: 20px; } /* Идеальные гуттеры без отрицательных маржинов на контейнере */

gap не является маржином/паддингом, это свойство контейнера. Оно не влияет на width: 100% детей.


3. Чек-лист: Что выбрать? (Decision Matrix)

ЗадачаИнструментПочему?
Фон/рамка должны заходить за контентpaddingФон клипается по border-box (по умолчанию background-clip: border-box).
Кликабельная зона должна быть большеpaddingУвеличивает hit-area кнопки/ссылки без JS.
Расстояние между соседними блокамиmargin (или gap у родителя)Схлопывание дает предсказуемые отступы в потоке.
Вытянуть элемент за край родителяmargin: negativeЕдинственный CSS-способ (без position: absolute).
Центрировать блок по горизонталиmargin: 0 autoКлассика для display: block с шириной.
Центрировать во Flex/Gridmargin: autoРаботает на обе оси, не требует настройки родителя.
Сохранить Aspect Ratio (до aspect-ratio)padding-top: %Хак, основанный на расчете % от ширины.
Отступ от края экрана/окна (body)padding на body / контейнереmargin на body исторически баговый в старых браузерах, padding безопаснее.

Резюме

  • Padding — это "внутренняя набивка". Часть элемента. Имеет фон. Не схлопывается. Не бывает отрицательным.
  • Margin — это "персональное пространство" снаружи. Прозрачно. Схлопывается вертикально. Может быть отрицательным (мощный layout tool).
  • Senior Rule: В современных Layouts (Flex/Grid) минимизируйте margin у детей. Используйте gap на родителе и padding на самом компоненте. margin оставьте для: центрирования (auto), вытягивания за границы (negative), вертикального ритма в потоке статей (typography), где схлопывание — это фича, а не баг.

Вопрос 7. Что такое функция обратного вызова (callback) и когда она используется?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:04:13"/>

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


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

Callback (функция обратного вызова) — это функция, переданная в качестве аргумента другой функции (функции высшего порядка — Higher-Order Function), которая должна быть вызвана (invoked) внутри этой внешней функции для завершения какого-либо действия или события.

Ключевое отличие от обычной функции: инверсия управления (Inversion of Control). Вы не вызываете callback сами напрямую в своем коде («не звоните нам, мы вам перезвоним») — вы передаете контроль выполнения другой функции/библиотеке/фреймворку.


1. Фундаментальные сценарии использования (Почему это нужно?)

СценарийСуть проблемыРоль Callback
Асинхронность (Non-blocking I/O)Операция (сеть, диск, таймер) занимает неизвестное время. Главный поток не может ждать (блокировать UI/Event Loop).Функция-продолжение (Continuation), которая выполнится после завершения операции. Основа Event Loop.
Обобщение алгоритмов (Strategy Pattern)Алгоритм фиксирован (сортировка, фильтрация, обход), но критерий или действие на каждом шаге должен быть настраиваемым.Предикат (item => item > 5), трансформер (x => x * 2), консьюмер (console.log).
Обработка событий (Event Handling)Источник события (DOM, EventEmitter, WebSocket) генерирует сигналы в произвольный момент.Слушатель (handler), регистрируемый один раз, вызываемый много раз.
Отложенное / Условное выполнениеЛогика "перед/после", "если успешно/если ошибка", "ретраи".Хук жизненного цикла (onSuccess, onError, finally, retryLogic).
API библиотек / ФреймворковФреймворк контролирует жизненный цикл (рендеринг, миграции, мидлвары).Пользовательский код, встраиваемый в слоты фреймворка (useEffect, middleware, webpack plugins).

2. Классификация колбэков (Critical для понимания архитектуры)

А. Синхронные (Blocking) Callbacks Вызываются до того, как внешняя функция вернет управление.

// map, filter, forEach, sort - классические примеры
const doubled = [1, 2, 3].map(x => x * 2); // callback вызван 3 раза синхронно
console.log(doubled); // Готово здесь

Использование: Трансформация данных, валидация, итераторы. Не создают асинхронности.

Б. Асинхронные (Deferred / Async) Callbacks Вызываются после возврата управления внешней функцией (в будущем тике Event Loop).

// setTimeout, fetch, fs.readFile, addEventListener
fetch('/api/user').then(res => res.json()); // callback в .then выполнится позже
console.log('Это выполнится ДО fetch callback');

Использование: I/O, таймеры, события. Создают асинхронность.

> Важно: Асинхронность определяется не тем, передан ли колбэк, а тем, когда он вызывается (в текущем стеке или в будущем макро/микрозадаче).


3. Эволюция паттернов в JS: От Callback Hell к Modern Async

Эра 1: Callback Hell / Pyramid of Doom (ES5 и ниже) Проблема: Последовательные зависимые асинхронные операции.

getUser(id, (err, user) => {
if (err) return handleErr(err);
getPosts(user.id, (err, posts) => {
if (err) return handleErr(err);
getComments(posts[0].id, (err, comments) => { // Уровень вложенности растет
// ...
});
});
});

Проблемы:

  • Error Handling: try/catch не работает (стек вызова разорван). Ошибки нужно прокидывать вручную в каждом колбэке (Error-first callback pattern: (err, data) => {}).
  • Control Flow: Сложно параллелить (Promise.all аналог), гонять циклы, отменять.
  • Readability: Код "уезжает" вправо.

Эра 2: Promises (ES2015) — Инверсия управления возвращается к вам Promise — это объект-обертка над будущим значением, который гарантирует вызов переданных обработчиков (then/catch) ровно один раз.

getUser(id)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.catch(handleErr); // Единая точка ошибок!

Колбэки (then/catch) становятся методами объекта, а не аргументами функции.

Эра 3: Async / Await (ES2017) — Синтаксический сахар над Promise Код выглядит синхронно, сохраняя неблокирующую природу.

async function fetchData() {
try {
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
return comments;
} catch (err) {
handleErr(err); // Нативный try/catch!
}
}

Эра 4: Observables / Streams (RxJS, Signals) — Для множественных значений во времени Promise — это один результат (или ошибка). Callback / Observable — это поток событий (клики, вебсокеты, прогресс-бар).

// Observable: callback вызывается много раз
fromEvent(button, 'click').subscribe(event => console.log(event));
// или современный Signal (Solid, Angular, Preact)
const count = signal(0);
effect(() => console.log(count())); // callback-реакция на изменение

4. Продвинутые темы (Senior Level)

А. Error-First Callback Pattern (Node.js Convention) Стандарт для Node.js API (fs, crypto, старые драйверы БД):

fs.readFile(path, (err, data) => {
if (err) return callback(err); // Ошибка — первый аргумент
callback(null, data); // Успех — err = null, данные — второй
});

Правило: Никогда не смешивайте throw и callback в асинхронном коде — throw уйдет в uncaughtException.

Б. this контекст и потеря привязки (Classic Pitfall)

const obj = {
value: 42,
method(cb) { cb(); } // Вызов без контекста!
};
obj.method(() => console.log(this.value)); // undefined (или window/globalThis в sloppy mode)

Решения:

  1. Стрелочные функции (лексический this).
  2. cb.call(thisArg, ...args) внутри библиотеки.
  3. bind при передаче: obj.method(handler.bind(obj)).

В. Колбэки в TypeScript — Типизация сигнатур

// Правильная типизация колбэка
type DoneCallback = (err: Error | null, result: string) => void;

function fetchData(cb: DoneCallback) { ... }

// Generic колбэки (map, filter)
declare function map<T, U>(arr: T[], fn: (item: T, index: number) => U): U[];

Г. Микрооптимизации и GC (Garbage Collection)

  • Анонимные функции () => {} создаются при каждом вызове родительской функции → давление на GC.
  • В хот-путях (рендеринг списков, игровые циклы, middleware) выносите колбэк в замыкание или используйте именованные функции.
// Плохо в цикле 1000 раз
items.forEach(item => process(item, () => doSomething(item.id)));

// Хорошо
const handler = (id) => doSomething(id);
items.forEach(item => process(item, handler));

Д. Отмена (Cancellation) — слабое место колбэков У нативного колбэка нет стандартного механизма отмены.

const id = setTimeout(() => doWork(), 5000);
clearTimeout(id); // Специфичный для API способ отмены

В Promise-based API используют AbortController / signal. В колбэках — передают cancelToken или флаг isCancelled в замыкании.


5. Чек-лист: Когда что выбирать (Decision Matrix)

СитуацияИнструментПочему
Массив/Коллекция: map, filter, reduce, sortСинхронный CallbackПроизводительность, чистота, иммутабельность. forEach — только для side-effects.
Одна асинхронная операция (fetch, timeout, чтение файла)Promise / async-awaitЧитаемость, try/catch, композиция (Promise.all, race).
Поток событий (clicks, WS messages, input changes)Observable / EventEmitter / SignalsМножественные значения во времени, операторы (debounce, map, filter), отписка (unsubscribe).
Низкоуровневое Node.js API / Legacy библиотекиError-first CallbackСовместимость. Оборачивайте в util.promisify или new Promise сразу на границе.
Middleware / Pipeline (Express, Redux, Webpack)Callback (next)Управление потоком: next() — продолжить, next(err) — ошибка, return — прервать.
Производительность в хот-пути (10k+ ops/sec)Именованная функция / замыканиеИзбегание аллокаций замыканий/стрелочек каждый кадр.

Резюме: Callback — это фундаментальный строительный блок JS (First-Class Functions).

  1. Синхронные — для обобщения логики (Array methods, Strategies).
  2. Асинхронные — для продолжения вычислений после I/O (Continuation Passing Style).
  3. Проблема: "Callback Hell", ручное прокидывание ошибок, потеря this, отсутствие стандарта отмены.
  4. Решение 2015+: Promises + Async/Await для линейных цепочек; Observables/Signals для стримов.
  5. На собеседовании: Умейте написать promisify, объяснить Event Loop (Micro/Macro tasks queue) в контексте колбэков, показать Error-first паттерн и знать, почему forEach не ждет await.

Вопрос 8. Что такое промисы (Promises) в JavaScript?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:04:45"/>

Оценка ответа кандидата: неполный. Кандидат назвал промисы инструментом для асинхронных задач, чтобы сделать функцию асинхронной. Не раскрыл суть: объект, представляющий результат асинхронной операции (pending, fulfilled, rejected), методы then/catch/finally.


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

Promise (Промис) — это объект-обертка (proxy), представляющий конечный результат (успех или неудачу) асинхронной операции. Это стандартный паттерн для работы с будущими значениями (Future / Deferred), устраняющий "Callback Hell" и предоставляющий гарантии выполнения.

Фундаментальная идея: Промис — это значение, которого может еще не быть, но которое гарантированно будет разрешено (resolved) или отклонено (rejected) ровно один раз.


1. Три состояния (State Machine) — Неизменяемые и Исключительные

Промис находится строго в одном из состояний. Переходы односторонние: PendingFulfilled ИЛИ PendingRejected.

СостояниеОписаниеРезультат (value / reason)Методы реакции
pending (Ожидание)Начальное состояние. Операция в процессе.undefined
fulfilled (Выполнен / Resolved)Операция завершена успешно.value (любое значение, включая undefined, null, другой Промис).then(onFulfilled)
rejected (Отклонен)Операция завершилась ошибкой.reason (обычно Error объект, но может быть любым).catch(onRejected) / .then(null, onRejected)

Ключевые гарантии спецификации (Promises/A+):

  1. Immutability: Состояние не может измениться после перехода в fulfilled/rejected.
  2. Thenable Assimilation: Если onFulfilled возвращает thenable (объект с методом then), промис "распаковывает" его и принимает его состояние.
  3. Microtask Queue: Обработчики (.then, .catch, .finally) выполняются асинхронно в очереди Microtasks (Job Queue), после текущего макрозадачи (Call Stack) и перед следующим рендером/макрозадачей (setTimeout, I/O).

2. Создание и Исполнитель (Executor)

const promise = new Promise((resolve, reject) => {
// Executor Function: выполняется СИНХРОННО и НЕМЕДЛЕННО при создании промиса

// Асинхронная работа (I/O, Timer, CPU heavy)
doAsyncWork((err, data) => {
if (err) reject(err); // Переход в rejected
else resolve(data); // Переход в fulfilled (или thenable assimilation)
});

// Ошибки внутри executor автоматически ловятся и превращаются в reject
// throw new Error('oops') === reject(new Error('oops'))
});

> Важно: executor запускается синхронно. Если там тяжелый синхронный код — он заблокирует Event Loop до первого await/.then/микрозадачи.


3. Методы экземпляра (Consumer API)

Все методы возвращают НОВЫЙ Промис (Chaining).

МетодСигнатураНазначениеОсобенности
.then(onFulfilled?, onRejected?)(value) => newValue | PromiseРеакция на успех (и опционально ошибку).1. Если handler не передан — значение пробрасывается дальше (value => value).<br>2. Возврат значения → fulfilled.<br>3. Возврат промиса/thenable → передача состояния (flattening).<br>4. throw / возврат rejected промиса → rejected.
.catch(onRejected)(reason) => newValue | PromiseРеакция на ошибку.Синтаксический сахар: .then(null, onRejected). Позволяет восстановиться (вернуть значение) или пробросить ошибку (throw).
.finally(onFinally)() => void | PromiseОчистка ресурсов (лоадеры, закрытие соединений).Не получает аргументов. Возвращает исходный промис (пробрасывает состояние/значение), если не бросает ошибку/не возвращает rejected промис.

Правило цепочки (Chaining):

fetchUser(id)
.then(user => fetchPosts(user.id)) // Вернули промис -> ждем его
.then(posts => posts.map(p => p.title)) // Вернули значение -> fulfilled
.then(titles => console.log(titles)) // Вернули undefined -> fulfilled
.catch(err => { // Ловит ошибку ЛЮБОГО предыдущего шага
logError(err);
return fallbackData; // ВОССТАНОВЛЕНИЕ: цепочка идет в .then дальше!
})
.finally(() => hideLoader()); // Выполнится ВСЕГДА

4. Статические методы конструктора (Combinators) — Управление параллелизмом

МетодПоведениеКейс использования
Promise.all(iterable)Fail-fast. Ждет все fulfilled. Если хотя бы один rejected — сразу rejected с этой ошибкой. Результат: массив значений в порядке аргументов.Независимые запросы, которые все нужны для рендера (User + Settings + Permissions).
Promise.allSettled(iterable)Never fails. Ждет завершения всех. Результат: массив объектов {status: 'fulfilled', value} или {status: 'rejected', reason}.Независимые виджеты на дашборде: если один упал — остальные должны показаться.
Promise.race(iterable)Первый завершившийся (fulfilled или rejected) определяет исход.Таймауты: Promise.race([fetch(), timeout(5000)]).
Promise.any(iterable)Первый успешный (fulfilled). Игнорирует rejected. Если все rejectedAggregateError (errors[]).Фоллбэки: пробуем CDN, потом Origin, потом Cache. Нужен первый успех.
Promise.resolve(value)Нормализация: если value — промис/thenable — возвращает его/ассимилирует. Иначе — fulfilled промис.Адаптация синхронных/асинхронных API к единому интерфейсу.
Promise.reject(reason)Создает сразу rejected промис.Быстрый возврат ошибки в цепочке без throw.

5. Интеграция с Async/Await (Syntactic Sugar)

async function всегда возвращает Промис. await — оператор приостановки выполнения функции до разрешения промиса.

async function getData() {
try {
// await "распаковывает" value или выбрасывает reason (как throw)
const user = await fetchUser(id);
const [posts, comments] = await Promise.all([
fetchPosts(user.id),
fetchComments(user.id)
]);
return { user, posts, comments }; // Авто-оборачивание в resolved Promise
} catch (err) {
// Ловит reject ЛЮБОГО await в блоке try
logger.error(err);
throw new AppError('Data load failed'); // Превращает в rejected промис возвращаемой функции
} finally {
analytics.track('load_attempt');
}
}

// Параллельный запуск БЕЗ ожидания друг друга (частая ошибка новичков):
// ПЛОХО (последовательно): const u = await a(); const p = await b();
// ХОРОШО (параллельно): const [u, p] = await Promise.all([a(), b()]);

6. Продвинутые паттерны и ловушки (Senior Level)

А. Promise Constructor Antipattern (Обертка за оберткой)

// ❌ ПЛОХО: Лишняя обертка, потеря stack trace, нарушение thenable assimilation
function getData() {
return new Promise((resolve, reject) => {
fetch('/api').then(res => resolve(res)).catch(reject);
});
}

// ✅ ХОРОШО: Прямой возврат (fetch уже возвращает промис)
function getData() {
return fetch('/api');
}

Б. Забытый return в цепочке (Silent Promise Loss)

fetchUser(id)
.then(user => {
fetchPosts(user.id); // ❌ ПРОМИС ПОТЕРЯН! Ошибки не попадут в .catch ниже.
// Код пойдет дальше сразу, не дожидаясь постов.
})
.then(() => console.log('Done')); // Выполнится до fetchPosts

Всегда возвращай промисы в .then, если следующая операция зависит от них.

В. forEach + async/await не работает (Параллелизм вместо последовательности)

// ❌ forEach не ждет await! Запустит все сразу, вернет undefined.
items.forEach(async item => await process(item));

// ✅ Последовательно: for...of
for (const item of items) await process(item);

// ✅ Параллельно с контролем: Promise.all + map
await Promise.all(items.map(item => process(item)));

// ✅ Параллельно с лимитом concurrency (пул): p-limit, p-map, или ручная очередь

Г. Отмена (Cancellation) — Отсутствует нативно Промисы неотменяемы (Uncancellable). Паттерны:

  1. AbortController / AbortSignal (Fetch API, EventTarget) — стандарт де-факто для I/O.
  2. Флаг в замыкании (для своих промисов):
    function makeCancellable(promise) {
    let isCancelled = false;
    const wrapped = new Promise((resolve, reject) => {
    promise.then(v => isCancelled ? reject({cancelled: true}) : resolve(v))
    .catch(e => isCancelled ? reject({cancelled: true}) : reject(e));
    });
    return { promise: wrapped, cancel: () => isCancelled = true };
    }
  3. Promise.race([promise, abortSignal.promise]).

Д. Обработка ошибок в Promise.all / allSettled

// Promise.all: Один reject ломает всё. try/catch ловит только первую ошибку.
try { await Promise.all(tasks); }
catch (err) { /* err - это причина ПЕРВОГО упавшего */ }

// Promise.allSettled: Никогда не реджектится. Ручная аналитика:
const results = await Promise.allSettled(tasks);
const errors = results.filter(r => r.status === 'rejected').map(r => r.reason);
if (errors.length) handleErrors(errors);

Е. Микрозадачи и Event Loop (Интервью-классика)

console.log('1 Sync');
Promise.resolve().then(() => console.log('2 Microtask'));
queueMicrotask(() => console.log('3 Microtask'));
setTimeout(() => console.log('4 Macrotask'), 0);
console.log('5 Sync');

// Порядок: 1, 5, 2, 3, 4
// Весь стек -> Вся Microtask Queue (FIFO) -> Рендер -> Одна Macrotask -> ...

7. Чек-лист Code Review / Best Practices

  1. Всегда возвращай промисы из async функций и .then колбэков (если не прерываешь цепочку намеренно).
  2. Не оборачивай промисы в new Promise без нужды (Promisify только callback-based API: util.promisify, new Promise(res => fs.readFile(..., res))).
  3. Используй Promise.allSettled для независимых UI-виджетов/аналитики.
  4. Оборачивай точку входа (main, handler, controller) в try/catch / .catch для предотвращения Unhandled Promise Rejection (краш Node.js / предупреждение в браузере).
  5. Избегай await в циклах (forEach, map без Promise.all), если операции независимы.
  6. Передавай AbortSignal во все fetch/axios вызовы для отмены при анмаунте компонента / навигации.
  7. Типизируй в TS: Promise<User>, Promise<User[]> — генерики работают идеально.

Резюме: Promise — это мономическое (single-future) значение с гарантированной семантикой состояния. Оно превращает асинхронность в объект, с которым можно комбинироваться (all, race, then), передавать, хранить и типизировать. async/await — это императивный синтаксис над этой монадой. Понимание Microtask Queue, Thenable Assimilation, Chaining Rules и Cancellation Patterns отличает Senior от Middle.

Вопрос 9. Как работает цикл событий (Event Loop) в JavaScript?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:05:16"/>

Оценка ответа кандидата: неполный. Кандидат упомянул проверку глобального контекста выполнения и очередь микрозадач (Microtask queue), но объяснение фрагментарно, не затрагивает стек вызовов, макрозадачи, порядок обработки.


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

Event Loop (Цикл событий) — это механизм, который позволяет однопоточному JavaScript выполнять неблокирующие асинхронные операции, координируя Стек вызовов (Call Stack), Очереди задач (Task Queues) и API окружения (Browser / Node.js).

Спецификация: HTML Living Standard (для браузеров) и Node.js Event Loop (libuv) — имеют различия в фазах, но общая модель одна.


1. Ключевые компоненты архитектуры

КомпонентРольХарактеристика
Call Stack (Стек вызовов)Исполнение синхронного кода. LIFO.Один поток = один стек. Блокируется долгими вычислениями.
Heap (Куча)Выделение памяти под объекты.Не управляется Event Loop напрямую (GC работает параллельно).
Web APIs / Node APIs (Среда выполнения)setTimeout, fetch, DOM Events, fs.readFile, crypto.Работают вне JS движка (в отдельных потоках ОС/браузера). Кладут колбэки в очереди.
Task Queue (Macrotask Queue / Очередь макрозадач)Колбэки: setTimeout, setInterval, setImmediate (Node), I/O, UI Rendering, postMessage.FIFO. Обрабатывается по одной задаче за тик Event Loop.
Microtask Queue (Очередь микрозадач)Колбэки: Promise.then/catch/finally, queueMicrotask, MutationObserver, process.nextTick (Node).FIFO. Очищается полностью до следующей макрозадачи/рендера.
Animation Frame CallbacksrequestAnimationFrame (rAF).Выполняются перед рендером (после микрозадач, до следующего кадра).

2. Алгоритм Event Loop (Псевдокод спецификации HTML)

WHILE (true) { // Бесконечный цикл
// 1. ВЗЯТЬ СТАРЕЙШУЮ МАКРОЗАДАЧУ ИЗ ОЧЕРЕДИ (Task Queue)
const task = taskQueue.popOldest();
IF (task) {
run(task); // Выполняет синхронный код -> наполняет Call Stack
// Внутри могут создаваться микрозадачи
}

// 2. ВЫПОЛНИТЬ ВСЕ МИКРОЗАДАЧИ (Microtask Queue) - ДО УПОРА
WHILE (microtaskQueue.hasTasks()) {
run(microtaskQueue.popOldest());
// ВАЖНО: Микрозадачи могут добавлять НОВЫЕ микрозадачи!
// Цикл крутится, пока очередь не станет пустой.
}

// 3. ОБНОВИТЬ РЕНДЕРИНГ (Только Браузер, если нужно)
IF (needsRender) {
// 3a. requestAnimationFrame callbacks (перед рендером)
runAnimationFrameCallbacks();

// 3b. IntersectionObserver callbacks

// 3c. РЕНДЕР (Layout, Paint, Composite) - обновление пикселей на экране
performRender();
}

// 4. ПОВТОРИТЬ ЦИКЛ
}

3. Порядок выполнения (Golden Rule)

Sync (Stack) → Microtasks (Queue) → Render (Browser) → Macrotask (Queue) → Repeat

Пример классического порядка:

console.log('1. Sync Start');

setTimeout(() => console.log('4. Macrotask (setTimeout)'), 0); // Macrotask

Promise.resolve().then(() => console.log('2. Microtask (Promise)')); // Microtask

queueMicrotask(() => console.log('3. Microtask (queueMicrotask)')); // Microtask

console.log('5. Sync End');

// ВЫВОД:
// 1. Sync Start
// 5. Sync End
// 2. Microtask (Promise)
// 3. Microtask (queueMicrotask) <-- Микрозадачи выполняются ВСЕ до следующей макрозадачи
// 4. Macrotask (setTimeout) <-- Только после очистки микрозадач

4. Критические нюансы для Senior/Tech Lead

А. Микрозадачи могут "заблокировать" рендеринг и макрозадачи Если микрозадача рекурсивно создает себя (или новую), Event Loop никогда не дойдет до рендера/макрозадачи → UI замерзнет (как while(true)).

function block() {
Promise.resolve().then(block); // Бесконечный цикл микрозадач
}
block(); // Браузер зависнет, setTimeout не сработает, рендера не будет

Защита: Браузеры лимитят длину микрозадач (например, Chrome — предупреждение после ~100-200 вложенных).

Б. requestAnimationFrame (rAF) vs setTimeout vs queueMicrotask

APIКогда выполняетсяПрименение
queueMicrotask / Promise.thenСразу после текущего синхронного кода, перед рендером.Внутренняя логика: батчинг стейта, мутации DOM перед paint.
requestAnimationFrameПеред следующим рендером (обычно 60fps = 16.6ms), после микрозадач.Анимации, измерения layout (getBoundingClientRect), подготовка кадра.
setTimeout(..., 0)В следующем тике Event Loop (после рендера, минимально ~4ms вложенность).Отложить работу до "после всего текущего кадра", yield к браузеру.

В. Node.js Event Loop (libuv) — Отличия от Браузера В Node нет рендеринга. Цикл состоит из фаз (Phases):

  1. TimerssetTimeout, setInterval callbacks.
  2. Pending Callbacks — отложенные I/O колбэки (TCP errors).
  3. Idle / Prepare — внутренние нужды libuv.
  4. PollГлавная фаза. Ожидание I/O (epoll/kqueue/IOCP). Выполнение I/O колбэков (fs, net, http). Если таймеров нет — блокируется здесь.
  5. ChecksetImmediate() callbacks (гарантированно после Poll).
  6. Close Callbackssocket.on('close', ...).

Важные отличия Node:

  • process.nextTick()не микрозадача по стандарту HTML, но выполняется после каждой фазы и перед микрозадачами Promise. Приоритет выше, чем у Promise.
  • setImmediate() — выполняется в фазе Check (после Poll/I/O), гарантированно быстрее setTimeout в I/O контексте.
  • Promise microtasks — выполняются между фазами (после nextTick).
// Node.js порядок:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

// ВЫВОД (в I/O контексте, например внутри fs.readFile):
// nextTick -> promise -> immediate -> timeout
// (вне I/O контекста порядок timeout/immediate не определен)

Г. await и микрозадачи await приостанавливает функцию и ставит продолжение в микрозадачу.

async function foo() {
console.log('1');
await Promise.resolve(); // Yield control (микрозадача)
console.log('3'); // Выполнится в следующей микрозадаче
}
foo();
console.log('2');

// 1, 2, 3

Д. Event Loop и производительность (Long Tasks)

  • Long Task — задача в Call Stack > 50ms (блокирует Main Thread).
  • Последствия: дроп фреймов (jank), задержка ввода (INP/CLS), setTimeout срабатывает позже.
  • Решение: requestIdleCallback (низкий приоритет, браузер дает дедлайн), scheduler.yield() (новый стандарт для yield), Web Workers (тяжелые вычисления вне Main Thread), wasm.

5. Практические паттерны и ловушки

Ловушка 1: forEach + async не ждет

// ПЛОХО: forEach не возвращает промис, не ждет await
users.forEach(async u => await save(u)); // Запускает все параллельно, ошибки теряются

// ХОРОШО: Последовательно
for (const u of users) await save(u);

// ХОРОШО: Параллельно с контролем
await Promise.all(users.map(u => save(u)));

Ловушка 2: Батчинг обновлений состояния (React/Vue/Signals) Фреймворки используют микрозадачи для батчинга.

function handleClick() {
setCount(c => c + 1); // Микрозадача #1
setFlag(f => !f); // Микрозадача #2
// DOM еще не обновился!
console.log(count); // Старое значение

// Чтобы прочитать обновленный DOM:
await Promise.resolve(); // Ждем микрозадачу фреймворка
// или
requestAnimationFrame(() => { /* DOM готов */ });
}

Ловушка 3: setTimeout 0 не значит "сразу"

  • Минимальная задержка вложенных таймеров: 4ms (HTML Spec).
  • В неактивных вкладках: 1000ms (1 сек) или больше (Throttling).
  • Используй requestAnimationFrame для анимаций, scheduler.postTask / scheduler.yield для приоритизации.

6. Чек-лист для отладки и понимания

  1. Stack Trace — показывает только синхронный путь. Асинхронный стек разорван (кроме async/await с поддержкой Error.stack в современных движках).
  2. Performance Tab (DevTools) — вкладка Main. Видишь: Task (желтый) → Microtasks (фиолетовый/розовый) → Render (фиолетовый) → Idle.
  3. console.log в микрозадаче — выполнится до следующего лога в макрозадаче.
  4. MutationObserver — срабатывает синхронно после мутации DOM, но до рендера (в микрозадаче). Полезно для реакции на изменения без setTimeout.
  5. navigator.scheduling.isInputPending() — проверка, есть ли пользовательский ввод, чтобы отдать приоритет (yield).

Резюме: Event Loop — это диспетчер единственного потока JS.

  1. Call Stack исполняет синхронный код.
  2. Web/Node APIs выполняют.async работу параллельно.
  3. Microtask Queue (Promise, queueMicrotask, nextTick) имеет высший приоритет — очищается полностью после каждой макрозадачи и перед рендером.
  4. Macrotask Queue (Timers, I/O, Events) — обрабатывается по одной за тик цикла.
  5. Render (Browser) — происходит после микрозадач, перед следующей макрозадачей. rAF вклинивается перед рендером.

Senior Insight: Понимание Event Loop нужно не для запоминания порядка логов, а для: предотвращения блокировок Main Thread (Long Tasks), правильного батчинга стейта, выбора между rAF/setTimeout/queueMicrotask, отладки race conditions в асинхронном коде и архитектуры высоконагруженных Node.js сервисов (фазы libuv, setImmediate vs nextTick).

Вопрос 10. Понимает ли браузер синтаксис SASS/SCSS напрямую?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:05:45"/>

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


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

Нет, браузеры не понимают SASS/SCSS (Syntactically Awesome Style Sheets) напрямую. Движки браузеров (Blink, WebKit, Gecko) парсят и применяют только стандартный CSS (Cascading Style Sheets).

SASS/SCSS — это метаязык (препроцессор), который расширяет CSS возможностями, отсутствующими в нативном стандарте (переменные, вложенность, миксины, функции, модули, наследование). Чтобы стили применились на странице, код на SASS/SCSS обязательно должен быть скомпилирован (транспилирован) в валидный CSS на этапе сборки (build time) или разработки (dev time).


1. Архитектура процесса: Source-to-Source Compilation

graph LR
A[Source: .scss / .sass] --> B[Компилятор Sass<br/>(Dart Sass / Node Sass / LibSass)]
B --> C[Output: .css]
C --> D[Браузер / CSSOM]
D --> E[Рендеринг]
  • Вход: .scss (SCSS syntax, надмножество CSS) или .sass (Indented syntax, без скобок/точек с запятой).
  • Инструмент: Компилятор (транслятор). Современный стандарт — Dart Sass (первичная реализация, быстрый, чистый JS/WASM). Устаревшие: Node Sass (LibSass binding, deprecated), Ruby Sass (оригинал, deprecated).
  • Выход: .css файл (или строка в памяти для инъекции через HMR).
  • Source Maps (.css.map): Критически важны для отладки (DevTools показывает исходный .scss и номера строк).

2. Что именно делает компилятор (Features → CSS)

SASS/SCSS FeatureКак компилируется в CSSПримечание
Переменные ($var)Подставляются значения.В CSS есть Custom Properties (--var), но они работают во время выполнения (runtime), а SASS переменные — статичны (compile-time).
Вложенность (Nesting)Раскрываются в плоские селекторы с конкатенацией (&).Нативный CSS Nesting (Chrome 115+, Safari 16.5+) теперь поддерживает это нативно, но синтаксис немного отличается (требует & или @nest).
Миксины (@mixin / @include)Код миксина инлайнится (копируется) в место вызова.Дублирует CSS. Перегрузка аргументами позволяет генерировать вариации.
Extend (@extend)Группирует селекторы через запятую (.a, .b { ... }).Опасно: может взорвать размер CSS (комбинаторный взрыв селекторов). Лучше @mixin + %placeholder.
Функции / Математика / Логика (@if, @each)Вычисляются на этапе сборки, в CSS попадают только готовые значения.Позволяет генерировать утилитарные классы, сетки, типографические шкалы.
Модули (@use, @forward)Объединяются в один CSS-файл (или чанки).Современная модульная система (namespaces, private members). Устаревший @import — глобальный, проблематичный.

3. Этапы интеграции в современный стек (Tooling)

А. Build Time (Production / CI) — Стандарт

  • Bundlers: Vite, Webpack (via sass-loader), Rollup, esbuild (имеет встроенный Sass плагин, но ограниченный), Parcel.
  • CLI: sass --watch src:dist --style=compressed --no-source-map.
  • Оптимизации: Minification (compressed), удаление комментариев, автопрефиксер (PostCSS/Autoprefixer) после компиляции Sass.

Б. Dev Time (Development) — HMR (Hot Module Replacement)

  • Vite / Webpack Dev Server: Компилируют Sass в памяти, инъецируют <style> тег или обновляют CSSOM без перезагрузки страницы.
  • Source Maps: Обязательны (devtool: 'source-map' или inline-source-map), чтобы в Elements/Styles панели видеть .scss:15, а не .css:1245.

В. Runtime Compilation (Anti-pattern для Production)

  • Использование sass.js (WASM порт Dart Sass) прямо в браузере.
  • Недостатки: Огромный бандл (~2-3 МБ gzipped), блокировка Main Thread, отсутствие кэширования компиляции, CSP проблемы (eval/wasm-eval).
  • Применение: Только для онлайн-редакторов (CodePen, StackBlitz, кастомайзеры тем пользователей).

4. Нативные CSS альтернативы (2024+): Почему можно отказаться от Sass

Многие фичи Sass теперь есть в браузере без сборки:

Фича SassНативный CSS аналогСтатус (Baseline 2024)
ПеременныеCSS Custom Properties (--color: red; var(--color))✅ Widely available. Динамические, наследуются, работают в Media Queries.
ВложенностьCSS Nesting (&:hover { })✅ Baseline 2023. Требует & для ссылок на родителя.
Миксины / ФункцииНет прямого аналога.❌ Нужны препроцессоры или JS-генерация (CSS-in-JS, Tailwind JIT).
Цветовые функции (lighten, mix)Color Level 4/5 (color-mix(), relative color syntax oklch(from var(--c) l c h))color-mix() везде. RCS — Baseline 2024.
Модули / Импорт@import (нативный) / CSS Modules (tooling)⚠️ Нативный @import блокирует загрузку (network waterfall). Лучше бандлить.
Математикаcalc(), clamp(), тригонометрия (sin(), cos())✅ Baseline.

Стратегия Senior/Tech Lead:

  1. Новые проекты: Оценивайте отказ от Sass в пользу Native CSS + PostCSS (autoprefixer, cssnano) + CSS Modules / Tailwind / Panda CSS.
  2. Легаси / Дизайн-системы: Sass всё еще важен для сложной генерации утилит, темизации через maps/loops, @use архитектуры.
  3. Миграция: Постепенный переход: postcss-scss парсер позволяет прогонять .scss через PostCSS плагины, понемногу заменяя фичи на нативные.

5. Частые ошибки и Best Practices

  1. @import vs @use / @forward

    • @importустарел (deprecated), создает глобальное пространство имен, дублирует импорты, не поддерживает приватность (_ prefix).
    • Всегда используйте @use (с namespace) и @forward (для реэкспорта API библиотек).
  2. Глобальные переменные vs CSS Custom Properties

    • Не путайте $font-size: 16px (compile-time constant) с --font-size: 16px (runtime variable).
    • Паттерн: Sass переменные для конфигурации сборки (брейкпоинты, палитра токенов), CSS переменные — для темизации/режимов (dark/light, density) в браузере.
  3. Глубина вложенности (Nesting Hell)

    • Sass позволяет .a { .b { .c { .d { ... } } } }.a .b .c .d (высокая специфичность, жесткая привязка к DOM).
    • Правило: Максимум 1-2 уровня вложенности. Используйте БЭМ / CSS Modules / Utility-first для плоских селекторов.
  4. @extend vs %placeholder vs @mixin

    • @extend .btn.btn, .my-btn { ... } (группировка селекторов).
    • %btn (placeholder) → не выводится в CSS, если не расширен.
    • Рекомендация: Избегайте @extend. Используйте @mixin (композиция) или Utility классы. @extend ломает Source Maps и порядок каскада.
  5. Деление чисел (Division)

    • Старый Sass: $width / 2 — деление.
    • Современный Sass (Dart Sass): / — это разделитель значений (как в CSS grid-template: 1fr / 2fr).
    • Обязательно: math.div($width, 2) или calc(#{$width} / 2).

Резюме: Браузер видит только CSS. Sass — это Dev Dependency (инструмент разработки), который исчезает на этапе сборки. Понимание того, как конкретная фича Sass трансформируется в CSS (инлайн миксинов, группировка @extend, вычисление математики), необходимо для отладки специфичности, размера бандла и производительности рендеринга. В 2024 году выбор Sass должен быть обоснован потребностью в сложной генерации кода (Design System internals), а не просто "переменными и вложенностью", которые дает нативный CSS.

Вопрос 11. Что такое Ajax?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:06:04"/>

Оценка ответа кандидата: неправильный. Кандидат признался, что не знает, что такое Ajax.


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

Ajax (Asynchronous JavaScript and XML) — это архитектурный подход и набор технологий для создания асинхронных веб-приложений, позволяющий обмениваться данными с сервером без полной перезагрузки страницы.

Хотя аббревиатура расшифровывается как "XML", современный Ajax практически всегда использует JSON как формат обмена данными. XML считается устаревшим для этого сценария из-за избыточности и сложности парсинга.


1. Ключевые составляющие (Стек технологий)

Ajax не является отдельным языком или библиотекой. Это комбинация:

  1. XMLHttpRequest (XHR) или современный Fetch API — браузерные API для выполнения HTTP-запросов из JavaScript.
  2. Promise / async-await — для управления асинхронностью и цепочками обработки.
  3. JSON — стандарт де-факто для сериализации данных (вместо XML).
  4. DOM API — для динамического обновления интерфейса на основе полученных данных.
  5. Event Loop / Microtasks — фундамент неблокирующего выполнения.

2. Эволюция API: От XHR к Fetch (Сравнение)

ХарактеристикаXMLHttpRequest (Legacy)Fetch API (Modern Standard)
СинтаксисКолбэк-ориентированный, event-based (onreadystatechange).Promise-based, нативно работает с async/await.
Обработка ошибокСложная: HTTP 404/500 не реджектят промис (нет промисов), нужно проверять status вручную.HTTP ошибки (4xx, 5xx) не реджектят промис (только сетевые ошибки). Требует ручной проверки response.ok.
Стриминг (Streaming)Нет (только responseText/responseXML целиком).Да (ReadableStream via response.body). Позволяет обрабатывать данные по частям (прогресс-бары, большие файлы).
Abort / Cancelxhr.abort()AbortController / AbortSignal (стандартный, работает везде).
Credentials (Cookies/CORS)xhr.withCredentials = truecredentials: 'include' | 'same-origin' | 'omit' в опциях.
Request/Response ObjectsНет (сырые строки/заголовки).Request / Response — полноценные объекты, можно клонировать, модифицировать, кэшировать (Service Workers).

Пример Modern Fetch (Best Practice):

async function fetchUserData(userId, signal) {
try {
const response = await fetch(`/api/users/${userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
// 'Authorization': `Bearer ${token}` // для авторизации
},
credentials: 'include', // важно для куков / HttpOnly токенов
signal, // для отмены запроса (AbortController)
// cache: 'no-store', // контроль кэширования
});

// Fetch НЕ бросает ошибку на 404/500!
if (!response.ok) {
// Парсим тело ошибки, если сервер вернул JSON с деталями
const errorData = await response.json().catch(() => ({}));
throw new ApiError(response.status, errorData.message || 'Request failed');
}

// Проверка Content-Type перед парсингом (безопасность)
const contentType = response.headers.get('content-type');
if (!contentType?.includes('application/json')) {
throw new Error('Invalid content type');
}

return await response.json(); // Десериализация
} catch (err) {
if (err.name === 'AbortError') return; // Игнорируем отмену
// Логирование / Sentry / UI уведомление
throw err; // Пробрасываем дальше для обработки в UI
}
}

// Использование с отменой при анмаунте компонента
const controller = new AbortController();
fetchUserData(123, controller.signal);
// controller.abort(); // Отмена

3. Жизненный цикл Ajax-запроса и Event Loop

  1. Инициация: JS вызывает fetch() / xhr.send(). Это синхронная операция (микросекунды).
  2. Делегирование: Браузер передает запрос сетевому стеку ОС (отдельный поток/процесс браузера). JS стек освобождается.
  3. Ожидание (Network Latency): Сервер обрабатывает, данные идут по проводам. Main Thread свободен для UI (клики, скролл, анимации).
  4. Приход ответа: Сетевой поток кладит задачу в Task Queue (Macrotask Queue) — "Network callback".
  5. Event Loop: После текущего макротаска и всех микрозадач (Promise callbacks) Event Loop забирает задачу из очереди.
  6. Resolve Promise: fetch промис резолвится в Response объект.
  7. Чтение тела: response.json() / .text() / .blob() — это тоже асинхронные операции (читают поток), возвращающие новые промисы (микрозадачи).
  8. Обновление UI: Данные попадают в стейт (React/Vue/Signals) → триггерится ре-рендер → DOM мутирует.

4. Критические аспекты безопасности и архитектуры (Senior Level)

А. CORS (Cross-Origin Resource Sharing) — Главная боль Браузеры блокируют ответы с других источников (origin = scheme + host + port), если сервер не разрешил явно.

  • Simple Requests (GET/POST/HEAD, стандартные заголовки) → идут сразу.
  • Preflight Requests (OPTIONS) → браузер сам шлет OPTIONS перед сложными запросами (PUT, DELETE, кастомные заголовки, application/json).
  • Заголовки ответа сервера:
    • Access-Control-Allow-Origin: https://myapp.com (не * с credentials!).
    • Access-Control-Allow-Credentials: true (для куков).
    • Access-Control-Allow-Headers, Access-Control-Allow-Methods.
    • Access-Control-Expose-Headers (чтобы JS мог прочитать кастомные заголовки ответа).

Б. CSRF (Cross-Site Request Forgery) — Защита состояния Ajax с credentials: 'include' шлет куки автоматически.

  • Защита: SameSite атрибут куков (Lax / Strict / None; Secure).
  • CSRF Tokens (Double Submit Cookie / Header) для старых браузеров / SameSite: None.
  • Custom Header (X-Requested-With: XMLHttpRequest) — простая защита, так как его нельзя поставить в простой HTML-форме (требует Preflight).

В. XSS (Cross-Site Scripting) через Ajax

  • Никогда не делайте element.innerHTML = response.data.html без санитизации (DOMPurify).
  • response.json() безопасен, но данные из него могут быть небезопасны при рендере.

Г. Кэширование и идемпотентность

  • GET / HEAD — идемпотентны, кэшируются браузером/CDN. Используйте Cache-Control, ETag, If-None-Match.
  • POST / PUT / PATCH / DELETEне кэшируются по умолчанию.
  • Проблема двойного клика: Пользователь жмет кнопку 2 раза → 2 запроса.
    • Решение: AbortController на предыдущий запрос /DISABLE кнопки / Идемпотентность ключей (Idempotency Keys) на бэкенде для платежей.

Д. Отмена запросов (Cancellation) — Must Have для SPA

  • Пользователь ушел со страницы до прихода ответа → Memory Leak / State Update on Unmounted Component.
  • Паттерн: AbortController создается в useEffect / onMounted, abort() в cleanup функции.

Е. Прогрессивное улучшение / Offline First

  • Service Workers перехватывают fetch события.
  • Стратегии: Network First, Cache First, Stale While Revalidate (Workbox).
  • Background Sync API — отложенная отправка форм при восстановлении связи.

5. Архитурные паттерны использования

ПаттернОписаниеИнструменты
Data Fetching LibrariesАбстракция над fetch: кэширование, дедупликация, рефетчинг, пагинация, SSR поддержка.TanStack Query (React Query), SWR, RTK Query, Apollo Client (GraphQL).
Optimistic UIМгновенный апдейт UI до ответа сервера. Откат при ошибке.Встроено в TanStack Query (onMutate / onError / onSettled).
Server-Sent Events (SSE)Односторонний стрим от сервера (текст/JSON). Проще WebSockets для уведомлений/логов.EventSource API (нет в IE, есть полифиллы). Авто-реконнект.
WebSockets / WebTransportДвусторонний бинарный канал. Для чатов, игр, реального времени.ws библиотеки, Socket.io (фоллбэки), нативный WebSocket.
GraphQL over HTTPОдин эндпоинт, POST с query/mutation. Требует специфичного кэширования (нормализация).Apollo Client, Urql, TanStack Query + GraphQL Request.

6. Чек-лист Code Review для Ajax-слоя

  1. Используется fetch / axios / ky с async/await (нет then/callback hell).
  2. Обработка !response.ok (HTTP ошибки не выбрасывают исключение в fetch).
  3. Есть try/catch с разделением AbortError и реальных ошибок.
  4. Используется AbortController для отмены при анмаунте / навигации.
  5. Заголовки: Content-Type: application/json, Accept: application/json.
  6. credentials: 'include' настроено правильно для кук / HttpOnly токенов.
  7. Нет утечев чувствительных данных в URL (GET /api/users?password=123).
  8. Идемпотентность для мутаций (Idempotency Keys для платежей/заказов).
  9. Типизация ответа (TypeScript: interface UserResponse, zod/valibot валидация в рантайме).
  10. Логирование ошибок в Sentry/LogRocket с контекстом (userId, url, body).
  11. Для списков: пагинация / бесконечный скролл / виртуализация (не грузить 10к записей).
  12. Используется библиотека управления серверным состоянием (TanStack Query) вместо ручного useEffect + useState.

Резюме: Ajax — это не просто "запрос без перезагрузки". Это фундамент современного веба (SPA, PWA, SSR Hydration). На собеседовании Senior-у ожидается понимание не только "как отправить fetch", а Event Loop взаимодействия, CORS/Preflight механики, потоковой обработки (Streams), стратегий отмены и кэширования, безопасности (CSRF/XSS) и архитектурных паттернов (Optimistic Updates, Server State Management с TanStack Query/SWR). "Не знаю, что такое Ajax" — это автоматический No Hire для любой фронтенд-позиции.

Вопрос 12. Какие команды Git вы знаете и как ими пользоваться?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:06:20"/>

Оценка ответа кандидата: неполный. Кандидат перечислил git status, git add, git commit -m, git push origin, git pull, но допустил ошибки в синтаксисе: сказал git add -m вместо git commit -m, git add origin вместо git push origin. Понимание базового workflow есть, но синтаксис команд неточен.


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

Git — это распределенная система контроля версий. Владеение им на уровне Senior/Tech Lead подразумевает не только знание команд, но и понимание объектной модели (blob, tree, commit, refs), графа коммитов, работа с историей и решение конфликтов. Ниже — структурированная шпаргалка от базы до продвинутых сценариев.


1. Базовый цикл работы (The Basics) — Исправление ошибок кандидата

КомандаНазначениеКлючевые флаги и примеры
git initИнициализация репозитория.git init [-b main] (сразу задать главную ветку).
git cloneКлонирование удаленного репо.git clone <url> [dir] --depth=1 (shallow clone для CI).
git statusСостояние рабочей директории и индекса.git status -s (short), -b (показать ветку).
git addДобавление изменений в Индекс (Staging Area).git add <file> / git add . / git add -p (patch mode — интерактивный выбор кусков кода, must know).
git commitСоздание коммита из индекса.git commit -m "msg" / git commit -v (показать diff в редакторе) / git commit --amend --no-edit (добавить в последний коммит без изменения сообщения).
git rmУдаление файлов из индекса и рабочей директории.git rm --cached <file> (убрать из трекинга, оставить на диске — для .gitignore фиксов).
git restore (современная замена checkout/reset для файлов)Отмена изменений в рабочей директории.git restore <file> (сбросить до HEAD) / git restore -s HEAD~1 <file> (взять версию из конкретного коммита).
git switch (современная замена checkout для веток)Переключение веток.git switch <branch> / git switch -c <new-branch> (создать и переключиться) / git switch - (предыдущая ветка).

> Важно: git add готовит снимок, git commit его фиксирует. git push отправляет на сервер. Кандидат перепутал add и commit/push.


2. Работа с историей и инспекция (History & Inspection)

КомандаНазначениеPro Tips
git logПросмотр истории.git log --oneline --graph --all --decorate (визуализация графа).<br>git log -p (показать патчи).<br>git log -S "string" (поиск коммитов, где добавилось/удалилось "string" — Pickaxe).<br>git log -- <path> (история конкретного файла).
git showПоказать объект (коммит, тег, блоб).git show <hash> / git show HEAD~3:path/to/file (показать файл в старом коммите).
git diffСравнение.git diff (working vs index).<br>git diff --staged (index vs HEAD — то, что попадет в коммит).<br>git diff branchA..branchB (разница кончиков).<br>git diff HEAD~1..HEAD --stat (статистика изменений).
git blame / git annotateКто написал строку.git blame -L 10,20 file (диапазон строк).<br>git blame -w (игнорировать пробелы).<br>git blame -C (детектить копипасту из других файлов).
git shortlogСводка по авторам.git shortlog -sn (топ контрибьютеров).
git ls-filesФайлы в индексе.git ls-files --others --ignored --exclude-standard (список игнорируемых файлов).

3. Ветвление, слияние и перебазирование (Branching & Merging) — Core Senior Skill

КомандаСтратегияКогда использовать
git mergeMerge Commit (non-fast-forward).Сохранение истории "как было". Для интеграции feature-веток в main/develop. git merge --no-ff гарантирует merge-коммит.
git merge --squashSquash (один коммит).Для PR в main: чистая линейная история, скрытие "fix typo", "wip". Требует ручного git commit.
git rebaseПеребазирование (переписывает историю).Локальная очистка истории перед пушем. git rebase -i HEAD~5 (интерактивный: squash, reword, drop, edit).<br>git rebase main (обновить фичу до актуального main).
git cherry-pickПеренос конкретного коммита.Hotfix в релизную ветку, перенос коммита в другую ветку. git cherry-pick <hash>..<hash> (диапазон).
git revertБезопасная отмена коммита (создает новый коммит-отмену).Отмена изменений в общей ветке (main, release). Не переписывает историю.

Золотое правило: Никогда не делайте rebase / push --force в общие (shared) ветки (main, develop, release/*). Только в своих feature-ветках.


4. Отмена изменений и "Спасение" (Undo & Recovery)

СитуацияКомандаПоследствия
Отменить git add (убрать из индекса)git restore --staged <file><br>(старое: git reset HEAD <file>)Файл остается измененным на диске, уходит в Untracked/Modified.
Отменить последние изменения в файле (до коммита)git restore <file><br>(старое: git checkout -- <file>)БЕЗВОЗВРАТНО удаляет незакоммиченные правки в рабочей директории.
Отменить последний коммит, оставить изменения в stagedgit reset --soft HEAD~1Коммит удален, изменения в индексе. Можно закоммитить заново (amend).
Отменить последний коммит, оставить изменения в рабочей директорииgit reset --mixed HEAD~1 (default)Коммит удален, изменения в working tree (не в staged).
Жесткий сброс: удалить коммит И измененияgit reset --hard HEAD~1ОПАСНО. Все несохраненное пропадает навсегда.
Уже закоммитил, но не запушил? Исправить сообщение/добавить файлыgit commit --amendПерезаписывает HEAD. Нужен push --force-with-lease если уже пушили в свою ветку.
Потерял коммит после reset --hard / rebase / amendgit reflogСпасательный круг. Показывает историю движения HEAD. Найди хеш -> git checkout <hash> -> git switch -c recovery-branch.
Очистить рабочую директорию до состояния HEADgit clean -fdУдаляет untracked файлы (-f) и директории (-d). -x — включая игнорируемые (.gitignore). ОСТОРОЖНО.

5. Работа с удаленным репозиторием (Remotes & Collaboration)

КомандаНазначениеВажные нюансы
git fetchСкачивает объекты и рефы (ветки/теги) с удаленного. Не трогает рабочую директорию и HEAD.git fetch --prune (удалить локальные трекинг-ветки, удаленные на сервере). git fetch origin main:main (обновить локальную main без чекаута).
git pullfetch + merge (по умолчанию) или rebase (pull --rebase).Лучше: git fetch -> посмотреть log origin/main -> решить: merge или rebase. pull скрывает конфликты в "черном ящике".
git pushОтправка коммитов.git push -u origin feature (установить upstream).<br>git push --force-with-lease (СТАНДАРТ для force push: не перезапишет, если на сервере есть коммиты, которых нет у вас локально).<br>git push origin --delete branch (удалить удаленную ветку).
git remoteУправление удаленными.git remote -v (показать URL). git remote set-url origin <new-url> (сменить HTTPS на SSH).
git push --tagsОтправить теги.Теги не пушатся автоматически.

6. Продвинутые / "Спасательные" команды (Advanced)

КомандаКейс использования
git stashВременное сохранение грязного состояния (WIP). git stash push -m "wip: refactoring" / git stash list / git stash pop (apply + drop) / git stash apply stash@{1} (не удалять). git stash --include-untracked / --all.
git bisectБинарный поиск бага в истории. git bisect start -> git bisect bad (текущий) -> git bisect good v1.0 (рабочий тег). Git сам переключается посередине. Вы тестите -> good/bad. Находит виновный коммит за O(log N). Автоматизация: git bisect run npm test.
git worktreeПараллельная работа в разных ветках без stash/переключений. git worktree add ../hotfix-branch hotfix/urgent. Создает отдельную рабочую директорию с отдельным HEAD, но общей .git папкой. Must know для hotfixов во время code review.
git submodule / git subrepoВключение одного репо в другой. Субмодули фиксируют конкретный коммит. git submodule update --init --recursive.
git filter-repo (замена filter-branch)Перепись истории всего репо: удаление секретов/больших файлов из истории, переименование авторов, выделение папки в отдельный репо. Требует force push во все клоны.
git notesПрикрепление метаданных к коммитам без изменения их хеша (для CI результатов, code review ID).
git replaceЗамена одного объекта на другой (например, склеить историю двух репо) без переписывания хешей.

7. Конфигурация и Алиасы (DX — Developer Experience)

Файл ~/.gitconfig (global) или .git/config (local).

[user]
name = Ivan Ivanov
email = ivan@company.com
signingkey = <GPG_KEY_ID> # для подписи коммитов

[core]
editor = code --wait # VS Code как редактор коммитов/ребаза
autocrlf = input # LF в репо, CRLF при чекауте на Windows
hooksPath = .githooks # общие хуки для команды (в репо)

[init]
defaultBranch = main

[pull]
rebase = true # pull --rebase по умолчанию (чище история)
ff = only # запрет merge-коммитов при pull (fast-forward only)

[push]
default = current # push текущей ветки в одноименную на origin
autoSetupRemote = true # автоматически создавать upstream при первом пуше (Git 2.37+)

[merge]
conflictstyle = diff3 # показывает базу конфликта (||||||| base)
# или zdiff3 (Git 2.35+) - лучший формат

[rebase]
autosquash = true # авто-сквош fixup!/squash! коммитов при rebase -i

[alias]
# Навигация
co = switch
br = branch -v
lg = log --oneline --graph --all --decorate --color
lol = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset'
# Инспекция
ds = diff --staged
dw = diff --color-words # поколоночный diff
# Утилиты
undo = reset --soft HEAD~1 # отменить последний коммит, оставить staged
unstage = restore --staged
discard = restore # отменить изменения в файле (ОСТОРОЖНО)
cleanup = "!git branch --merged | grep -v '\\*\\|main\\|develop' | xargs -n 1 git branch -d" # удалить смерженные локальные ветки
# Продвинутые
amend = commit --amend --no-edit
force-push = push --force-with-lease
fp = force-push
wip = commit -m "wip" --no-verify
fix = "!f() { git commit --fixup=$1; }; f" # git fix <hash>
squash = "!f() { git commit --squash=$1; }; f" # git squash <hash>
ready = "!git rebase -i --autosquash @{u}" # интерактивный ребаз на upstream с автосквошем

8. Git Hooks & Automation (Качество кода в репо)

  • Client-side (.git/hooks/ или core.hooksPath):
    • pre-commit: Линтеры, тесты, git diff --staged проверки (husky + lint-staged — стандарт индустрии).
    • commit-msg: Проверка формата сообщения (Conventional Commits: feat:, fix:, chore:).
    • pre-push: Полные тесты, сборка.
  • Server-side (GitLab CI / GitHub Actions / Gerrit / Bitbucket):
    • pre-receive: Блокировка push (защита main, проверка подписей GPG, политики именования веток).
    • update: Проверка каждой ветки при push.

9. Стратегии ветвления (Branching Strategies) — Архитектурный выбор

СтратегияОписаниеДля чего подходит
Trunk-Based Development (TBD)Короткоживущие ветки (или коммиты прямо в main), Feature Flags. CI/CD обязателен.Высокая частота деплоя, микросервисы, كبار команды (Google, Facebook style). Senior default.
GitHub Flowmain (deployable) -> feature branches -> PR -> merge to main -> deploy.Веб-приложения, CI/CD, простая модель.
GitFlowdevelop + main + feature/* + release/* + hotfix/*. Строгая модель релизов.Релизные циклы (мобильные приложения, boxed software, embedded), когда нужно поддерживать несколько версий в продакшене одновременно.
GitLab Flowmain -> pre-prod/staging -> production (environment branches).Когда деплой — это ручной/сложный процесс через окружения.

Резюме для собеседования:

  1. Знайте разницу: reset (local history rewrite) vs revert (safe public history) vs restore (working dir changes).
  2. Понимайте граф: HEAD, HEAD~, HEAD^, refs, detached HEAD.
  3. Безопасность: push --force-with-lease > push --force. reflog — ваша страховка.
  4. Эффективность: add -p, rebase -i --autosquash, worktree, bisect.
  5. Командная работа: Conventional Commits, PR/Merge Request workflow, Code Review через git diff branch...origin/main (triple dot diff — изменения только в ветке).
  6. Инструменты: CLI — база. GUI (GitKraken, Fork, GitHub Desktop, IDE) — для визуализации графа и конфликтов. Но в терминале вы должны решать сложные кейсы.

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

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:07:13"/>

Оценка ответа кандидата: неполный. Кандидат объяснил, что undefined присваивается автоматически при объявлении переменной без инициализации. Не упомянул, что null — это намеренное присваивание «пустого» значения, и про typeof null === 'object'.


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

null и undefined — это два примитивных значения, обозначающих «отсутствие значения», но с фундаментально разной семантикой и поведением в движке. Понимание разницы критично для проектирования API, работы с типами (TypeScript/Flow) и отладки.


1. Семантическая разница (Intent)

Характеристикаundefinednull
Смысл«Значение отсутствует неявно / еще не определено». Переменная объявлена, но не инициализирована. Свойство не существует в объекте. Функция ничего не вернула.«Значение отсутствует намеренно / пусто по дизайну». Разработчик явно указал: «здесь ничего нет и это нормально».
ИсточникДвижок JS (автоматически).Программист (явное присваивание).
Типичный сценарийlet x;<br>obj.missingProp<br>arr[999]<br>function f() {} ; f()<br>Параметр, не переданный в функцию.let user = null; (пользователь не залогинен)<br>cache.set(key, null) (кэширование «пустого» результата)<br>Promise.resolve(null)<br>DOM: element.cloneNode(true).parentNode (нет родителя).

2. Технические отличия в движке (Spec & Runtime)

А. typeof — классическая ловушка

typeof undefined // "undefined" (корректно)
typeof null // "object" // 🐛 ОШИБКА ЯЗЫКА (Legacy, не исправили ради совместимости)

> Почему? В первой версии JS значения хранились как 32-битные слова. Тип кодировался в младших битах. null был представлен как 0x00000000 (нулевой указатель), что совпадало с тегом типа «object». Исправление сломало бы миллионы сайтов.

Правильная проверка на null:

// Только строгое равенство
if (value === null) { ... }
if (value == null) { ... } // Ловит И null, И undefined (loose equality) — идиоматично для проверки «пустоты»

Б. Строгое и нестрогое равенство

null == undefined // true (специальное правило в спеке Abstract Equality Comparison)
null === undefined // false (разные типы: Null vs Undefined)

null == 0 // false (null не приводится к числу при ==)
undefined == 0 // false
Number(null) // 0
Number(undefined) // NaN

В. JSON сериализация

JSON.stringify({ a: null }) // '{"a":null}' — сохраняется
JSON.stringify({ b: undefined }) // '{}' — ПРОПУСКАЕТСЯ (ключ удаляется)

> Важно для API: если нужно передать «поле есть, но значение пустое» — используйте null. undefined в JSON не существует.

Г. Деструктуризация и дефолтные параметры

// Дефолт срабатывает ТОЛЬКО для undefined
function foo(x = 10) { return x; }
foo(undefined) // 10
foo(null) // null (явное значение, дефолт игнорируется)

const { a = 1 } = { a: undefined }; // a = 1
const { a = 1 } = { a: null }; // a = null

Д. Массивы: «Дыры» (Sparse arrays) vs undefined

const arr = [1, , 3]; // arr[1] — «дыра» (empty slot)
arr[1] === undefined // true (чтение дыры дает undefined)
1 in arr // false (свойства нет)
Object.keys(arr) // ['0', '2'] (дыра пропущена)

const arr2 = [1, undefined, 3]; // явный undefined
1 in arr2 // true
Object.keys(arr2) // ['0', '1', '2']

> null в массиве — это нормальный элемент. undefined — может быть и элементом, и дырой. Array.prototype методы (map, forEach) пропускают дыры, но обрабатывают undefined/null.


3. TypeScript / Статические типы (Senior Context)

В TS разница закреплена в системе типов:

// strictNullChecks: true (СТАНДАРТ СЕЙЧАС)

let x: number; // тип: number | undefined (если не инициализирована)
let y: number | null; // можно явно положить null
let z: number | undefined; // можно явно положить undefined

// Утилиты:
type NonNullable<T> = T extends null | undefined ? never : T;
// Убирает null и undefined из юниона

Паттерн «Nullable» vs «Optional»:

interface User {
name: string;
// Поле ОБЯЗАТЕЛЬНО присутствует, но может быть null (пользователь скрыл email)
email: string | null;

// Поле МОЖЕТ отсутствовать в объекте (legacy данные / частичный апдейт)
phone?: string; // === phone: string | undefined
}
  • null в объекте = «я знаю про это поле, значение пустое».
  • undefined / ? = «я не знаю / не загрузил / не применимо».

4. DOM API & Web Platform Specifics

Браузерные API почти всегда возвращают null для «не найдено»:

document.getElementById('missing') // null
element.parentNode // null (у корневого элемента)
element.closest('.missing') // null
localStorage.getItem('missing') // null
fetch(url).then(r => r.json()) // может вернуть null (JSON null)

undefined в DOM встречается редко (например, element.dataset.missingundefined).


5. Best Practices & Code Style (Airbnb / Google / Standard)

  1. Не используйте undefined как значение для присваивания.

    • user.name = undefined;
    • user.name = null; (смысл: «имя сброшено»)
    • delete user.name; (смысл: «свойства нет»)
  2. Инициализируйте переменные сразу.

    // Плохо
    let user;
    if (loggedIn) user = getUser();
    // Хорошо
    const user = loggedIn ? getUser() : null;
  3. Проверка на «пустоту» (Falsy) — опасна.

    if (!value) { ... } // Ловит: null, undefined, 0, '', false, NaN
    • Явно проверяйте: value === null, value == null, typeof value === 'undefined'.
  4. Возвращайте null из функций-поисковиков (find, get).

    function findUser(id): User | null { ... } // Контракт: нашел -> User, не нашел -> null
    // Не возвращайте undefined для «не найдено» — это путает с «функция сломалась/не вернула ничего».

6. Резюме: Чек-лист для Code Review

СитуацияЧто писатьПочему
Переменная объявлена, значение будет позжеlet x = null;Явный intent, избегаем typeof x === undefined в коде.
Свойство объекта «пустое по дизайну»obj.prop = nullВидно в JSON.stringify, понятно потребителю.
Свойства нет / опциональноdelete obj.prop / не добавлятьЭкономия памяти, корректный in / Object.keys.
Функция ничего не возвращаетreturn; (implicit undefined)Стандарт языка.
Функция ищет сущность, не нашлаreturn nullКонтракт: T | null. undefined зарезервирован под «ошибку/нет возврата».
Проверка аргумента на «не передан»if (arg === undefined) или arguments.length < 1null — это валидное переданное значение.
Глубокая проверка на «пустоту»value == null (только null/undefined)Идиоматично, быстро, читаемо.

Senior Insight: Разница между null и undefined — это не просто трюк для собеседования. Это моделирование состояния в приложении.

  • undefined = Entropy (хаос, неинициализированность, ошибка, отсутствие).
  • null = Controlled Empty State (контролируемое пустое состояние, бизнес-логика «нет данных»).

Смешивание их ведет к багам: user.address.city падает, если addressnull (хорошо, падает сразу), но тихо возвращает undefined, если addressundefined (плохо, баг уйдет в прод). Явное null лучше неявного undefined.

Вопрос 14. Какие новые возможности появились в ES6 (ECMAScript 2015)?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:07:48"/>

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


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

ES6 (ES2015) — это самый масштабный релиз в истории JavaScript, ставший фундаментом современной разработки. Он превратил язык из «скриптового» в полноценный язык для крупных приложений. Ниже — структурированный разбор всех ключевых фич с акцентом на нюансы, критичные для Senior/Tech Lead.


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

Замена var (function-scoped, hoisting с инициализацией undefined).

  • let — изменяемая привязка (mutable binding), блочная область ({}), TDZ (Temporal Dead Zone) до строки объявления.
  • const — неизменяемая привязка (immutable binding), обязательна инициализация. Объекты/массивы внутри всё ещё мутируемы.
  • Ключевое отличие в циклах: for (let i...) создает новую лексическую среду на каждой итерации (замыкание работает корректно). var — одна среда на весь цикл.
// TDZ Example
console.log(x); // ReferenceError (не undefined!)
let x = 1;

// Loop closure fix
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 0, 1, 2
}
for (var j = 0; j < 3; j++) {
setTimeout(() => console.log(j)); // 3, 3, 3
}

2. Стрелочные функции (=>)

Синтаксический сахар + лексическое связывание this / arguments / super / new.target.

  • Нет своего this — берет из внешнего контекста (нельзя использовать как конструктор, нет prototype, нельзя call/apply/bind для смены this).
  • Неявный возврат: x => x * 2 (без {} и return).
  • Параметры: () => {} / x => {} / (x, y) => {}.
  • Ловушка: return { a: 1 } — синтаксическая ошибка (блок). Нужно return ({ a: 1 }) или () => ({ a: 1 }).
const obj = {
name: 'ES6',
old: function() { setTimeout(function() { console.log(this.name) }, 100) }, // undefined (window/global)
es6: function() { setTimeout(() => console.log(this.name), 100) } // 'ES6' (лексическое this)
};

3. Шаблонные строки (Template Literals)

Обратные кавычки `.`

  • Интерполяция: `Hello ${name}`.
  • Многострочность без \n.
  • Tagged Templates: Функция-обработчик для кастомной логики (SQL escaping, i18n, styled-components).
// Tagged Template
function sql(strings, ...values) {
return strings.reduce((acc, str, i) => acc + str + (values[i] ? `'${escape(values[i])}'` : ''), '');
}
const query = sql`SELECT * FROM users WHERE id = ${userId} AND role = ${role}`; // Безопасный SQL

4. Деструктуризация (Destructuring Assignment)

Распаковка массивов и объектов в переменные.

  • Объекты: const { a, b: rename, c = 'default' } = obj;
  • Массивы: const [first, , third] = arr;
  • Rest: const [head, ...tail] = arr;
  • В параметрах функции: function({ x, y = 10 }) {}.
// Глубокая деструктуризация
const user = { id: 1, profile: { name: 'Admin', roles: ['admin', 'user'] } };
const { profile: { name, roles: [primaryRole] } } = user;
// name = 'Admin', primaryRole = 'admin'

5. Параметры функций по умолчанию (Default Parameters)

Вычисляются при вызове (late binding), а не при определении.

  • Область видимости: параметры видят друг друга слева направо.
  • arguments в non-strict mode не отражает изменения параметров (в strict — отражает).
function foo(a = 1, b = a * 2, c = someGlobal++) { }
// foo() -> a=1, b=2
// foo(5) -> a=5, b=10

6. Rest / Spread операторы (...)

  • Rest (сборка): function sum(...nums) {}только последний параметр, превращает в настоящий массив (вместо arguments — array-like).
  • Spread (распаковка): Math.max(...arr), [...arr1, ...arr2], { ...obj1, ...obj2 }.
  • Shallow Copy: Создает поверхностную копию (1 уровень вложенности).

7. Классы (class)

Синтаксический сахар над прототипами.

  • constructor, extends, super() (обязателен в наследнике до this).
  • Статические поля/методы: static prop = 1, static method() {}.
  • Публичные/Приватные поля (Stage 3 / ES2022, но де-факто стандарт): #privateField, #privateMethod().
  • Важно: Тело класса выполняется в strict mode. Методы не перечисляемы (enumerable: false). Нет hoisting (TDZ).
class User {
#password; // Private field
constructor(name) { this.name = name; }
static createAdmin(name) { return new User(name); }
getName() { return this.name; }
}
class Admin extends User {
constructor(name, level) {
super(name); // Обязательно!
this.level = level;
}
}

8. Модули (import / export)

Нативная модульная система (ESM).

  • Статический анализ: импорт/экспорт определяется на этапе парсинга (Tree Shaking).
  • Режим: Строгий ('use strict') по умолчанию.
  • Загрузка: Асинхронная, параллельная (в браузере), кэшируется (Singleton).
  • Типы экспорта:
    • Named: export const a = 1 / export { a, b as c }.
    • Default: export default 1 (один на модуль).
    • Re-export: export * from './mod', export { a } from './mod'.
  • Dynamic Import: import('./module').then(m => ...) — code splitting, ленивая загрузка.
// main.js
import React, { useState } from 'react'; // Default + Named
import * as Utils from './utils'; // Namespace object
const HeavyComponent = () => import('./Heavy'); // Lazy

9. Промисы (Promise)

Нативная реализация Promise/A+ для управления асинхронностью.

  • Состояния: pending -> fulfilled (resolved) / rejected.
  • Методы: .then(), .catch(), .finally().
  • Статические: Promise.all(), Promise.race(), Promise.allSettled() (ES2020), Promise.any() (ES2021), Promise.resolve(), Promise.reject().
  • Microtask Queue: Обработчики выполняются после текущего макротаска, но перед рендером/следующим макротаском.
// Promise Constructor Pattern (промисификация callback API)
function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => err ? reject(err) : resolve(data));
});
}

10. Итераторы и Генераторы (Symbol.iterator, function*)

Протокол итерации (Iterable Protocol).

  • Iterable: Объект с методом [Symbol.iterator]() возвращающим Iterator.
  • Iterator: Объект с методом next() возвращающим { value, done }.
  • Generators: Функции, которые можно останавливать (yield) и возобновлять. Возвращают объект, являющийся и Итератором, и Итерируемым.
function* range(start, end) {
for (let i = start; i <= end; i++) yield i;
}
for (const num of range(1, 3)) console.log(num); // 1, 2, 3
const [a, b] = range(10, 12); // Деструктуризация работает!

11. for...of цикл

Универсальный цикл по итерируемым объектам (Arrays, Maps, Sets, Strings, Arguments, NodeLists, Generators).

  • Не работает с обычными объектами (нужен Object.keys/values/entries).
  • Поддерживает break, continue, return.

12. Новые структуры данных: Map, Set, WeakMap, WeakSet

  • Map: Ключи — любые типы (объекты, функции, примитивы). Сохраняет порядок вставки. .size, .set(), .get(), .has(), .delete(), .clear(), forEach, keys(), values(), entries().
  • Set: Уникальные значения (по алгоритму SameValueZero: NaN === NaN).
  • WeakMap / WeakSet: Ключи только объекты. Слабые ссылки (GC может удалить ключ, если на него нет других ссылок). Неитерируемые, нет .size, нет clear(). Использование: приватные данные, кэши, метаданные DOM-узлов.

13. Символы (Symbol)

Новый примитивный тип для уникальных идентификаторов.

  • Symbol('desc') — уникален всегда. Symbol.for('key') — глобальный реестр (один на ключ).
  • Использование: «Скрытые» свойства объектов, метапрограммирование (Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance).
  • Не сериализуются в JSON.

14. Proxy и Reflect (Метапрограммирование)

  • Proxy(target, handler): Перехват фундаментальных операций (get, set, has, apply, construct, deleteProperty, ownKeys...).
  • Reflect: Статический класс с методами-дублерами внутренних операций (Reflect.get, Reflect.set, Reflect.construct...). Нужен для корректного super в прокси и дефолтного поведения.
const handler = {
get(target, prop, receiver) {
console.log(`Reading ${prop}`);
return Reflect.get(target, prop, receiver); // Дефолтное поведение
},
set(target, prop, value) {
if (typeof value !== 'number') throw new Error('Only numbers');
return Reflect.set(target, prop, value);
}
};
const proxied = new Proxy({}, handler);
proxied.a = 1; // OK
proxied.b = 'x'; // Error

15. Улучшения Object / Array / String / Number / Math

  • Object: assign() (shallow merge), keys(), values(), entries(), getOwnPropertySymbols(), setPrototypeOf(), is() (SameValue algorithm: NaN === NaN, 0 !== -0).
  • Array: from() (array-like -> Array), of() (создание из аргументов), find(), findIndex(), fill(), copyWithin(), keys(), values(), entries(), includes() (ES2016).
  • String: startsWith(), endsWith(), includes(), repeat(), codePointAt(), fromCodePoint().
  • Number: isNaN(), isFinite(), isInteger(), isSafeInteger(), EPSILON, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER, parseInt/Float (глобальные перенесены).
  • Math: trunc(), sign(), cbrt(), clz32(), imul(), fround(), hypot(), expm1(), log1p(), log10(), log2(), sinh(), cosh(), tanh(), asinh(), acosh(), atanh().

16. Бинарные и восьмеричные литералы

0b1010 // 10 (binary)
0o755 // 493 (octal)

17. Tail Call Optimization (TCO) — Спецификация, но не реализация

В строгом режиме хвостовые вызовы не должны растить стек.

  • Реализовано: Только в JavaScriptCore (Safari).
  • Не реализовано: V8 (Chrome, Node.js), SpiderMonkey (Firefox) — из-за сложностей с Error Stack Traces и асинхронным стеком.
  • На практике: не полагаться.

Резюме для Senior/Tech Lead: Чек-лист миграции / Code Review

ФичаBest Practice / Pitfall
varЗапретить (ESLint: no-var). Только const (по умолчанию) / let.
Arrow FunctionsИспользовать для колбэков/коллекций. Не использовать для методов объектов (нужен this), конструкторов, генераторов.
DestructuringОбязательно для импортов, пропсов React, конфигов. Избегать глубокой вложенности (>2 уровней) — читаемость падает.
Default ParamsНе использовать мутируемые значения как дефолт: fn(arr = []) — безопасно (новый массив каждый вызов). fn(obj = {}) — безопасно. Но fn(x = someExternalArray) — опасность shared state.
ClassesПредпочитать композицию (functions + closures) для инкапсуляции (#private решает часть проблем). extends — только для иерархий "Is-a".
ModulesВсегда type: "module" в package.json (Node) или <script type="module">. Избегать циклических зависимостей.
PromisesВсегда .catch() в конце цепочки. Не оборачивать Promise в new Promise без нужды (Promise Constructor Antipattern). Использовать Promise.allSettled для независимых задач.
Map/SetMap вместо Object для динамических ключей/частых добавлений/удалений. Set для уникальных коллекций.
ProxyМощный инструмент для библиотек (Vue 3 reactivity, Immer, RPC stubs). В бизнес-логике — редко, сложно отлаживать.

Итог: ES6 — это не просто «новый синтаксис». Это смена парадигмы: модульность, иммутабельность (const/spread), декларативность (arrow/map/filter), асинхронность (Promise), интроспекция (Symbol/Proxy). Знание «как работает под капотом» (TDZ, лексическое this, микрозадачи, статический анализ импортов) отличает Senior от Middle.

Вопрос 15. В чем разница между стрелочной и обычной функцией, и поднимаются ли стрелочные функции (hoisting)?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:08:10"/>

Оценка ответа кандидата: неполный. Кандидат отметил, что стрелочная функция анонимна и короче по синтаксису. Не упомянул ключевые отличия: отсутствие собственного this, arguments, нельзя использовать как конструктор, нет prototype. На вопрос о хоистинге сначала запутался, затем сказал, что не поднимаются (верно), но обоснование нечёткое.


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

Разница фундаментальна: стрелочные функции — это не «сокращенный синтаксис», а отдельный вид функций с иной семантикой выполнения, спроектированные для решения проблем this и удобства функционального стиля.


1. Ключевые отличия (Comparison Matrix)

ХарактеристикаОбычная функция (function)Стрелочная функция (=>)
thisДинамическое (определяется в момент вызова: call, apply, bind, метод объекта, new).Лексическое (статическое). Захватывает this из родительского лексического окружения на момент создания. Нельзя изменить через call/apply/bind.
argumentsИмеет свой локальный arguments (array-like object).Нет своего arguments. Захватывает из внешней функции (если есть) или вызывает ReferenceError на верхнем уровне. Используйте rest-параметры (...args).
superДинамическое (как this).Лексическое. Захватывает super из родительского класса/объекта. Критично для методов классов.
new.targetЕсть (позволяет узнать, вызвана ли через new).Нет.
Конструктор (new)Можно вызывать с new. Имеет prototype.Нельзя (TypeError: ... is not a constructor). Нет prototype.
yieldМожно использовать (Generator Functions function*).Нельзя. Не могут быть генераторами.
Дублирование параметровРазрешено в sloppy mode (function(a, a) {}).Запрещено всегда (SyntaxError), даже в non-strict mode.
Hoisting (Вознятие)Function Declaration — полностью (имя + тело).<br>Function Expression — только имя переменной (varundefined, let/const — TDZ).Никогда не поднимаются как функциональные сущности. Всегда Function Expression. Поведение зависит от ключевого слова (var / let / const).
Синтаксис телаТолько блок { ... }.Expression Body (неявный возврат): x => x * 2.<br>Block Body: x => { return x * 2; }.
Возврат объектаreturn { a: 1 }.Проблема: () => { a: 1 } — это блок, возвращает undefined.<br>Решение: () => ({ a: 1 }) — обернуть в скобки.

2. Глубокий разбор this (Самое важное)

Обычная функция: this определяется как вызывают (Call-site binding).

const obj = {
name: 'Obj',
regular() { console.log(this.name); },
arrow: () => console.log(this.name) // Лексическое: window/global или module scope
};

obj.regular(); // 'Obj' (this = obj)
obj.arrow(); // undefined / '' (this = global/module)

const fn = obj.regular;
fn(); // undefined (this = window/global/undefined в strict)

const fnArrow = obj.arrow;
fnArrow(); // undefined (this зафиксирован при создании в глобальной области)

Стрелочная функция: this берется из Lexical Environment (родительского контекста выполнения) на момент определения.

  • В методе класса (property initializer): handleClick = () => { this.setState() }this забинден на экземпляр класса.
  • В модуле (top-level): this = undefined (ESM) или window (scripts).
  • Внутри обычной функции: захватывает this этой функции.

Нюанс с bind/call/apply:

const arrow = () => this;
arrow.call({ a: 1 }); // Игнорирует аргумент call! Вернет лексический this.

3. Hoisting (Вознятие) — Техническая механика

Короткий ответ: Стрелочные функции не поднимаются как функции. Они ведут себя как переменные.

Детально по типам объявлений:

ТипHoisting поведениеПример
Function Declaration<br>function foo() {}Полное поднятие. Имя foo доступно везде в области видимости до кода.foo(); function foo() {}Работает.
Function Expression (var)<br>var foo = function() {}Поднимается только объявление переменной (var foo = undefined). Тело функции — на месте.foo(); var foo = function() {}TypeError: foo is not a function.
Function Expression (let/const)<br>const foo = function() {}TDZ (Temporal Dead Zone). Переменная существует, но доступ к ней до строки инициализации вызывает ReferenceError.foo(); const foo = function() {}ReferenceError.
Arrow Function (var)<br>var foo = () => {}Аналогично var Function Expression. foo = undefined до строки.foo(); var foo = () => {}TypeError.
Arrow Function (let/const)<br>const foo = () => {}Аналогично let/const Function Expression. TDZ.foo(); const foo = () => {}ReferenceError: Cannot access 'foo' before initialization.

Вывод: Разницы в хоистинге между function() {} и () => {} нет, если обе записаны как Function Expression (в переменную). Разница есть только у Function Declaration (объявление функции как инструкции), которого не существует для стрелочных функций.


4. Когда что использовать (Best Practices)

СценарийРекомендацияПочему
Колбэки в массивах / Promise / Event HandlersСтрелочная (arr.map(x => x*2))Коротко, лексический this не мешает, не нужен self = this.
Методы объекта / КлассаОбычная (или Property Initializer method = () => {})Нужен динамический this для полиморфизма / переопределения. Стрелочная в прототипе — плохая идея (копируется на каждый экземпляр).
Конструкторы / КлассыТолько class / functionСтрелочные нельзя вызывать с new.
Нужен argumentsОбычнаяСтрелочные не имеют arguments.
ГенераторыТолько function*Стрелочные не поддерживают yield.
Вложенные функции, где нужен this родителяСтрелочнаяАвтозахват this заменяет .bind(this) / const self = this.
Top-level в модулях (ESM)Стрелочная / Function ExpressionFunction Declaration в модулях ведет себя как let (блочная область), но лучше единообразие.

5. Производительность и Память (Senior Insight)

  1. Создание замыкания: Стрелочная функция, использующая внешние переменные (включая this), создает замыкание (Closure) при каждом вызове внешней функции (если определена внутри) или один раз (если в классе/модуле).

    • В методах классов: handleClick = () => {} создает новую функцию на каждый экземпляр (память O(N)). Обычный метод в prototype — одна функция на все экземпляры (O(1)).
    • Вердикт: Для частых инстанцируемых классов — лучше обычные методы + bind в конструкторе или публичные поля-функции только для хендлеров.
  2. Оптимизация движка (V8/SpiderMonkey):

    • Современные движки отлично оптимизируют оба вида.
    • Стрелочные функции чуть проще для инлайнинга (нет логики this binding), но разница микроскопична.
    • Не выбирайте стрелочную "за производительность". Выбирайте по семантике this.
  3. Стек вызовов (Stack Traces):

    • Анонимные стрелочные функции (() => {}) в стеке выглядят как (anonymous) или по имени переменной (const foo = () => {} -> foo).
    • Именованные Function Expression (const foo = function bar() {}) дают лучшие стеки (bar). Используйте именованные FE для критических утилит.

6. Типизация в TypeScript (Нюансы)

// Тип 'this' в стрелочной функции выводится автоматически из контекста
class Component {
state = { count: 0 };
// this: Component (автоматически)
increment = () => { this.setState({ count: this.state.count + 1 }); };

// В обычном методе нужно аннотировать или использовать 'this' тип
decrement() { this.setState({ count: this.state.count - 1 }); }
}

// Тип функции-конструктора
type Constructor<T> = new (...args: any[]) => T;
// Arrow function НЕ подходит под new (...args) => T
const Factory = <T,>(C: Constructor<T>) => new C(); // OK
// const BadFactory = <T,>(C: () => T) => new C(); // Error: 'C' cannot be used with 'new'

Резюме для собеседования (Cheat Sheet)

  1. this: Обычная — динамический (кто вызвал). Стрелочная — лексический (где написана). Это главное отличие.
  2. arguments: Обычная — есть. Стрелочная — нет (используй ...rest).
  3. new / prototype: Обычная — да. Стрелочная — нет (TypeError).
  4. super / new.target / yield: Обычная — да. Стрелочная — лексический / нет / нет.
  5. Hoisting: Стрелочные не поднимаются как функции. Всегда Function Expression. Поведение = поведению let/const/var переменной, в которую записана.
  6. Синтаксис: Стрелочные короче для колбэков, неявный возврат выражения, возврат объекта требует ().
  7. Best Practice: Стрелочные для колбэков и замыканий над this. Обычные для методов, конструкторов, генераторов, когда нужен arguments.

Вопрос 16. Что такое замыкание (closure) в JavaScript?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:09:11"/>

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


Правильный ответ: Замыкания — фундамент JS (Deep Dive)

Замыкание (Closure) — это комбинация функции и лексического окружения (Lexical Environment), в котором эта функция была объявлена.

Простыми словами: функция «запоминает» переменные из области видимости, где она была создана, и сохраняет доступ к ним даже после того, как внешняя функция завершила выполнение.

Это не просто «доступ к внешним переменным» — это механизм реализации в движке (Spec: [[Environment]] внутренний слот функции).


1. Как это работает «под капотом» (Specification Level)

При создании функции (Function Declaration / Expression / Arrow) движок записывает в её скрытый слот [[Environment]] ссылку на текущий Лексический Окружение (Lexical Environment).

Лексическое Окружение (Lexical Environment) =

  1. Environment Record (записи переменных: let, const, var, параметры, this, arguments).
  2. Ссылка на внешнее окружение ([[OuterEnv]]) — цепочка областей видимости.

Алгоритм поиска переменной (Identifier Resolution):

  1. Ищем в Environment Record текущего Лексического Окружения.
  2. Если нет — переходим по [[OuterEnv]] к внешнему Лексическому Окружению.
  3. Повторяем до глобального объекта (globalThis / window).
  4. Если не найдено — ReferenceError (или создается глобальная в sloppy mode).

Замыкание рождается в момент создания функции, а не при её вызове.

function outer() {
const secret = 42; // Записывается в Environment Record outer'а

function inner() { // Создается функция inner
// inner.[[Environment]] === LexicalEnvironment(outer)
console.log(secret); // Поиск: inner.Env -> outer.Env -> НАЙДЕНО
}

return inner;
}

const myClosure = outer(); // outer завершился, но его Environment Record жив!
myClosure(); // 42 (доступ к 'secret' сохранен)

> Ключевой момент: outer завершилась, но её Environment Record не удален Garbage Collector'ом, потому на него есть ссылка из myClosure.[[Environment]].


2. Классические ловушки и паттерны (Must Know)

А. Проблема var в цикле (Classic Interview Task)

for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Вывод: 3, 3, 3
}

Причина: var функционально-областная. Все замыкания ссылаются на одну переменную i из глобальной/функциональной области. К моменту выполнения таймеров цикл давно завершился, i === 3.

Решение 1: let (блочная область видимости) — создает новую связку на каждой итерации.

for (let i = 0; i < 3; i++) { // let создает новый Environment Record на каждой итерации
setTimeout(() => console.log(i), 100); // 0, 1, 2
}

Спецификация: for (let ...) неявно делает let i = i для каждого прохода.

Решение 2: IIFE (Immediately Invoked Function Expression) — захват значения.

for (var i = 0; i < 3; i++) {
((j) => setTimeout(() => console.log(j), 100))(i); // 0, 1, 2
}

Б. Фабрика функций / Модульный паттерн (Инкапсуляция)

function createCounter() {
let count = 0; // Приватное состояние (скрыто в замыкании)
return {
increment: () => ++count,
decrement: () => --count,
get: () => count
};
}

const counterA = createCounter();
const counterB = createCounter(); // Разные замыкания, разные 'count'
counterA.increment(); // 1
counterB.increment(); // 1 (независимо)

В. Частичное применение / Карринг (Currying)

const multiply = (a) => (b) => a * b;
const double = multiply(2); // Замыкание запомнило a=2
double(5); // 10

3. Управление памятью и утечки (Memory Leaks) — Senior Concern

Замыкания удерживают в памяти все переменные из внешнего лексического окружения, на которые есть ссылка в коде замыкания (даже если они не используются явно — * 죽은 код* не всегда удаляется парсером, зависит от движка).

Пример утечки:

function attachListener() {
const hugeData = new Array(1000000).fill('x'); // 10MB
const element = document.getElementById('btn');

element.addEventListener('click', () => {
// Используем только element, но hugeData в замыкании!
console.log('clicked', element.id);
});

// hugeData никогда не соберется GC, пока слушатель висит на element
}

Как избежать:

  1. Обнулять ссылки: hugeData = null после использования (если функция долгоживущая).
  2. Минимизировать замыкание: Выносить нужные данные в аргументы или слабые ссылки (WeakMap, WeakRef).
  3. Удалять слушатели: removeEventListener (требует именованной функции).
  4. Использовать WeakMap для кэшей/метаданных, чтобы не препятствовать GC ключам.

> V8 Optimization: Движки (V8, SpiderMonkey) делают Closure Analysis (анализ замыканий). Если переменная из внешней области не используется внутри замыкания, она не попадает в замыкание (не сохраняется в Context объекте в куче). Но если используется eval или new Function — анализ отключается, сохраняется всё.


4. Замыкания и this / arguments / super

  • this / arguments / superНЕ являются частью замыкания в обычных функциях (они динамические).
  • Стрелочные функцииНЕ имеют своих this, arguments, super. Они захватывают их лексически из внешнего окружения (замыкаются на них).
const obj = {
method() {
const arrow = () => console.log(this); // Захватило this от method
const regular = function() { console.log(this); }; // Своё this (window/undefined)
arrow(); // obj
regular(); // window/undefined
}
};

5. Продвинутые кейсы: Loop Closures & Async

Проблема асинхронности в цикле (Promise/Async):

const urls = ['/1', '/2', '/3'];
const results = [];

// ОШИБКА: forEach не ждет async колбэк
urls.forEach(async (url) => {
const data = await fetch(url);
results.push(data); // Порядок не гарантирован, forEach не возвращает промис
});

// ПРАВИЛЬНО: for...of + await (последовательно)
for (const url of urls) {
const data = await fetch(url);
results.push(data);
}

// ПРАВИЛЬНО: Promise.all + map (параллельно)
const results = await Promise.all(urls.map(url => fetch(url)));

6. Типизация замыканий в TypeScript

// Тип функции-замыкания
type Counter = {
increment: () => number;
get: () => number;
};

function createCounter(): Counter {
let count = 0; // Тип number, инференс работает
return {
increment: () => ++count,
get: () => count
};
}

// Замыкание над Generic
function createHolder<T>(initial: T) {
let value = initial;
return {
get: () => value,
set: (v: T) => { value = v; }
};
}
const numHolder = createHolder(42); // Тип { get: () => number, set: (v: number) => void }

7. Чек-лист для Code Review / Архитектуры

  1. Не создавайте функции в цикле/рендере без необходимости (React: useCallback, useMemo; Vanilla: выносите наружу). Каждый вызов = новое замыкание = память + GC pressure.
  2. Избегайте захвата больших объектов в долгоживущие замыкания (слушатели, таймеры, кэши).
  3. Понимайте разницу: let в цикле = безопасно. var в цикле = баг.
  4. Не используйте eval / new Function — они ломают оптимизацию замыканий (создают "слоупоки" — Sloppy Closures, захватывающие всё).
  5. Модульный паттерн / IIFE — старый способ инкапсуляции. В современном коде — ES Modules (статический импорт/экспорт, нет рантайм-замыканий для приватности).
  6. Private Fields (#field) в классах — нативная инкапсуляция без замыканий (хранятся в слабом мапе экземпляра, не в лексическом окружении).

Резюме (One-Liner для собеседования)

> Замыкание — это функция + ссылка на её лексическое окружение (Lexical Environment) на момент создания. > Оно позволяет функции обращаться к переменным внешней функции после завершения работы внешней функции. > В движке реализовано через скрытый слот [[Environment]], указывающий на цепочку Environment Records. > Критично для: инкапсуляции (модули, фабрики), колбэков, карринга, сохранения состояния. > Опасно: удержание памяти (лишние захватки, eval, долгоживущие слушатели), классическая ловушка с var в цикле.

Вопрос 17. Что такое ключевое слово this в JavaScript и на что оно ссылается?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:09:53"/>

Оценка ответа кандидата: неправильный. Кандидат ошибся: назвал this ключевым словом result, утверждал, что оно всегда указывает на window и служит для вывода в консоль. Не учел контекст вызова (объект, класс, строгий режим, call/apply/bind, стрелочные функции).


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

this — это специальное ключевое слово (контекст выполнения), которое неявно передается в функцию в момент её вызова. Оно указывает на объект, в контексте которого выполняется функция.

Главное правило: this определяется не там, где функция объявлена, а там, где она вызывается (Call-site). Исключение — стрелочные функции (лексическое this).


1. Четыре правила привязки this (Binding Rules) — в порядке приоритета

ПриоритетТип привязкиКак вызываетсяЧему равен this
1 (Высший)new Bindingnew Fn()Новый пустой объект ({}), созданный конструктором.
2Explicit Bindingfn.call(obj), fn.apply(obj), fn.bind(obj)()Переданный явно первый аргумент (obj).
3Implicit Bindingobj.fn()Объект перед точкой (obj).
4 (Низший)Default Bindingfn() (самостоятельный вызов)Strict mode: undefined.<br>Sloppy mode: globalThis (window в браузере).

2. Разбор каждого правила с примерами

А. Default Binding (Прямой вызов)

function foo() { console.log(this); }
foo(); // window (sloppy) / undefined (strict)

const obj = { foo };
obj.foo(); // obj (Implicit Binding срабатывает выше!)

Б. Implicit Binding (Вызов как метод объекта)

const user = {
name: 'Alice',
greet() { console.log(this.name); }
};
user.greet(); // 'Alice' (this === user)

// Ловушка: потеря контекста (Detached method)
const greet = user.greet;
greet(); // undefined / window (Default Binding!)

В. Explicit Binding (Жесткая привязка)

function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const user = { name: 'Bob' };

greet.call(user, 'Hello', '!'); // Аргументы по отдельности
greet.apply(user, ['Hi', '?']); // Аргументы массивом
const bound = greet.bind(user, 'Hey'); // Возвращает НОВУЮ функцию с зафиксированным this
bound('.'); // 'Hey, Bob.'

Г. new Binding (Конструктор)

function User(name) {
this.name = name; // this = новый пустой объект {}
// return this; // неявно
}
const u = new User('Tom'); // u = { name: 'Tom' }

> Если конструктор явно возвращает объект (return {}), this игнорируется и возвращается этот объект. Если возвращает примитив — игнорируется, возвращается this.


3. Стрелочные функции: Лексическое this (Исключение из правил)

Стрелочные функции не имеют своего this. Они захватывают this из родительского лексического окружения на момент создания.

const obj = {
name: 'Obj',
regular() { console.log(this.name); }, // 'Obj'
arrow: () => console.log(this.name), // window/undefined (лексическое: глобальный модуль)
method() {
const innerArrow = () => this; // Захватывает this от method() -> obj
return innerArrow();
}
};
obj.method(); // obj

Последствия:

  • call/apply/bind не работают на стрелочных функциях (игнорируются без ошибки).
  • Нельзя использовать как конструкторы (new Arrow() -> TypeError).
  • Нет arguments (используйте ...rest).
  • Идеальны для колбэков, где нужно сохранить this внешней функции.

4. Строгий режим ('use strict') vs Sloppy Mode

СценарийSloppy Mode (обычный)Strict Mode ('use strict')
function() { return this; }()globalThis (window)undefined
call(null/undefined)Подменяет на globalThisОставляет null / undefined
Примитивы в call(1, 'a')Автоупаковка в Number, StringОставляет примитив (this === 1)

> Best Practice: Всегда используйте 'use strict' (в модулях ESM — по умолчанию). Это предотвращает случайное загрязнение глобального объекта.


5. Приоритет правил (Кто побеждает?)

const obj1 = { name: 'Obj1' };
const obj2 = { name: 'Obj2' };

function foo() { console.log(this.name); }

// 1. new Binding побеждает все
new foo(); // undefined (this = {}, name нет)

// 2. Explicit Binding побеждает Implicit
foo.call(obj1); // 'Obj1'
obj1.foo = foo;
obj1.foo.call(obj2); // 'Obj2' (call важнее точки)

// 3. bind создает функцию с "закаменным" this (Hard Binding)
const bar = foo.bind(obj1);
bar.call(obj2); // 'Obj1' (bind не перебивается call/apply!)
new bar(); // {} (new побеждает bind! this = новый объект)

Иерархия: new > call/apply (explicit) > obj.method() (implicit) > fn() (default). Особый случай: bind создает функцию с жестко зафиксированным this, который нельзя изменить через call/apply, но может изменить new.


6. Классические ловушки (Traps) и Best Practices

А. Потеря контекста в колбэках (Event Handlers, setTimeout, map)

class Counter {
count = 0;
increment() { this.count++; }
}

const c = new Counter();
button.addEventListener('click', c.increment); // ОШИБКА: this = button (Implicit Binding к DOM элементу)

// Решения:
button.addEventListener('click', () => c.increment()); // 1. Стрелочная обертка
button.addEventListener('click', c.increment.bind(c)); // 2. bind в конструкторе/пропсе
// В классе:
increment = () => { this.count++; } // 3. Public Class Field (Stage 3) — авто-биндинг

Б. this в колбэках массивов (forEach, map, filter)

const nums = [1, 2, 3];
const obj = { factor: 10, multiply() { return nums.map(n => n * this.factor); } };
obj.multiply(); // ОШИБКА: this в колбэке map = undefined/window

// Решения:
multiply() { return nums.map(n => n * this.factor); } // 1. Arrow function (лексическое this)
multiply() { return nums.map(function(n) { return n * this.factor; }, this); } // 2. 2-й аргумент map (thisArg)
multiply() { return nums.map((n) => n * this.factor); } // 3. Arrow (best)

В. this в классах (Class Fields vs Methods)

class Logger {
prefix = 'LOG:';

// Method Definition (в прототипе)
log(msg) { console.log(this.prefix, msg); } // this зависит от вызова

// Class Field с стрелочной (на экземпляре, создается в конструкторе)
logArrow = (msg) => console.log(this.prefix, msg); // this всегда = instance

// Static method
static staticLog() { console.log(this); } // this = класс Logger
}

Память: logArrow создается на каждом экземпляре (память O(N)). Метод в прототипе — один на всех (O(1)). Используйте поля-стрелки только для хендлеров событий.


7. this в современных API и TypeScript

DOM Event Listeners:

  • Обычная функция: this = currentTarget (элемент, на котором висит слушатель).
  • Стрелочная: this = лексическое (компонент/класс). event.currentTarget доступен через аргумент.

TypeScript: Типизация this (Polymorphic this / ThisType)

// 1. Тип this в методах класса
class Base {
// this: Base (или производный класс)
clone(): this { return new (this.constructor)(); }
}
class Derived extends Base { x = 1; }
const d = new Derived().clone(); // Тип: Derived

// 2. ThisParameterType (для функций вне классов)
function setName(this: { name: string }, n: string) { this.name = n; }
const user = { name: 'A' };
setName.call(user, 'B'); // OK
setName('B'); // Error: 'this' context missing

// 3. ThisType в объектах (для миксинов/библиотек)
interface State)
const store = {
state: 0,
inc() { this.state++; }
} as { state: number } & ThisType<{ state: number; inc: () => void }>;

8. Резюме для Senior/Tech Lead (Cheat Sheet)

  1. this — это аргумент, скрытый от глаз. Передается в момент вызова.
  2. Правило "Кто слева от точки?" (obj.fn() -> this = obj).
  3. Стрелочные функции не имеют this. Они замыкаются на внешнем. Не используйте их как методы объектов/классов, если не хотите авто-биндинга на экземпляр.
  4. bind создает новую функцию. call/apply вызывают сразу. bind > call/apply (hard binding), но new > bind.
  5. Strict mode = безопасность. this = undefined лучше, чем window.
  6. Потеря контекста — #1 баг. Всегда биндьте колбэки: onClick={this.handleClick.bind(this)} / onClick={() => this.handleClick()} / Public Field handleClick = () => {}.
  7. В классах: Методы в прототипе (память) vs Стрелочные поля (автобиндинг, память на экземпляр). Выбирайте осознанно.
  8. globalThis — единый стандарт для глобального объекта (вместо window/self/global).

> Интервью-вопрос на проверку глубины: "Объясни, почему const fn = obj.method; fn() теряет контекст, а obj.method() — нет. И как bind решает это на уровне движка?" (Ответ: fn — это просто ссылка на функцию, CallExpression fn() не имеет базового референса (Base Reference), поэтому this = default. bind создает Bound Function Exotic Object с внутренним слотом [[BoundThis]], который игнорирует переданный this при вызове).

Вопрос 18. Что такое React и какие у него основные особенности?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:10:20"/>

Оценка ответа кандидата: правильный. Кандидат сказал, что React — библиотека на JavaScript для создания удобного UI, основана на компонентах, обеспечивает повторное использование кода, упомянул React Native для мобильных приложений.


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

React — это декларативная, компонентная JavaScript-библиотека с открытым исходным кодом для создания пользовательских интерфейсов (UI). Разработана и поддерживается Meta (Facebook) и сообществом.

Ключевое отличие от фреймворков (Angular, Vue): React — это только слой представления (View Layer). Он не навязывает решения для роутинга, стейт-менеджмента, HTTP-запросов или сборки — разработчик выбирает стек самостоятельно.


1. Фундаментальные принципы (Core Principles)

ПринципОписание
ДекларативностьВы описываете «Как UI должен выглядеть» для данного состояния (state), а не «Как его изменить» (императивно). React сам вычисляет минимальные изменения (Reconciliation) и обновляет DOM.
Компонентный подходUI разбивается на изолированные, переиспользуемые куски (компоненты), инкапсулирующие разметку (JSX), логику и стили. Компоненты комбинируются как лего.
Unidirectional Data Flow (Односторонний поток данных)Данные текут сверху вниз (Parent → Child через props). Состояние (state) принадлежит владельцу. Изменения инициируются событиями снизу вверх (callbacks), но мутация происходит только у владельца. Это делает поведение предсказуемым и отлаживаемым.
Immutability (Неизменяемость)Состояние (state) и пропсы (props) считаются неизменяемыми. Обновление происходит через создание новой копии (setState(newState)), а не мутацию (state.x = 1). Критично для работы PureComponent, React.memo, хуков и тайм-тревела в DevTools.

2. Ключевые технические особенности (Technical Features)

А. Virtual DOM (Виртуальный DOM) & Reconciliation (Согласование)

  • React хранит легковесное дерево JS-объектов (Fiber Nodes), копию реального DOM.
  • При изменении состояния создается новое дерево. Алгоритм Diffing (эвристика O(n)) сравнивает старое и новое дерево.
  • Вычисляется минимальный набор операций (Patch) для обновления реального DOM (Batch Updates).
  • Fiber Architecture (React 16+): Переписанный реконсилер. Поддерживает приоритизацию задач (Priority Levels), прерывание работы (Interruption), Suspense, useTransition, Concurrent Mode.

Б. JSX (JavaScript XML)

  • Синтаксический сахар: const el = <div className="box">Hello</div>;.
  • Транспилируется (Babel/Swc/TypeScript) в React.createElement('div', { className: 'box' }, 'Hello').
  • Позволяет использовать полную мощь JS внутри разметки (интерполяция {}, циклы .map(), тернарники, переменные).
  • Безопасность: Автоматически экранирует данные при интерполяции (защита от XSS), если не использовать dangerouslySetInnerHTML.

В. Компоненты: Функциональные (Function Components) vs Классовые (Class Components)

  • Современный стандарт (с 2019 / Hooks): Только Функциональные компоненты + Хуки (useState, useEffect, useContext...).
  • Классовые (class extends React.Component) — Legacy, поддерживаются, но не рекомендуются для нового кода (нет хуков, сложнее оптимизация, this binding).
  • Компонент — чистая функция: (props) => JSX. Должен быть идемпотентен (один ввод — один вывод).

Г. Хуки (Hooks) — API для логики состояния и побочных эффектов

ХукНазначение
useStateЛокальное состояние компонента.
useReducerСложная логика состояния (Redux-паттерн локально).
useEffectПобочные эффекты (fetch, подписки, DOM, таймеры). Запускается после рендера (асинхронно, не блокирует paint). Cleanup function для отписки.
useLayoutEffectСинхронно после мутаций DOM, до paint. Для измерений (getBoundingClientRect), фокуса.
useContextЧтение контекста без прокидывания пропсов (Prop Drilling).
useRefМутирующий контейнер (.current), не триггерит ре-рендер. Доступ к DOM-узлам.
useMemo / useCallbackМемоизация значений и функций (стабильность ссылок для детей / useEffect deps).
useIdУникальные ID для доступности (SSR-safe).
useTransition / useDeferredValueConcurrent Features: пометка обновлений как низкоприоритетных.

Д. Контекст (Context API) & Prop Drilling

  • Механизм передачи данных через дерево без явного пробрасывания пропсов на каждом уровне.
  • Кейсы: Тема (Theme), Авторизация (User), i18n, Настройки.
  • Антипаттерн: Класть в контекст часто меняющиеся данные (каждый Provider value change -> ре-рендер всех консьюмеров). Решение: разделение контекстов, useMemo для value, атомарные библиотеки (Jotai, Recoil, Zustand).

Е. Жизненный цикл (Functional Component Mental Model) Нет componentDidMount/Update/Unmount. Мы мыслим в терминах Эффектов синхронизации (useEffect):

  1. Mount: Рендер -> Commit -> Layout Effects (useLayoutEffect) -> Paint -> Effects (useEffect).
  2. Update: Пропсы/Стейт меняются -> Рендер -> Commit -> Layout Effects Cleanup -> Layout Effects -> Paint -> Effects Cleanup -> Effects.
  3. Unmount: Layout Effects Cleanup -> Effects Cleanup.

3. Производительность и Оптимизация (Performance)

  1. React.memo(Component) — HOC для пропуска рендера, если пропсы не изменились (shallow compare). Аналог PureComponent.
  2. useMemo(() => value, deps) — кэширует результат вычислений.
  3. useCallback(fn, deps) — кэширует ссылку на функцию (важно для пропсов детей, обернутых в memo, и зависимостей useEffect).
  4. Виртуализация списковreact-window, @tanstack/react-virtual (рендерить только видимое).
  5. Code Splitting / Lazy LoadingReact.lazy(() => import(...)) + <Suspense fallback={<Spinner />}>. Уменьшает Initial Bundle.
  6. Concurrent Features (React 18+): useTransition (неблокирующий ввод), useDeferredValue (отложенный рендер тяжелой части), автоматический батчинг в промисах/таймерах/нативных событиях.

4. Экосистема и "Батарейки" (What you need in real apps)

React не включает это "из коробки", но стандарт индустрии:

ЗадачаСтандартные решения (2024)
РоутингReact Router v6+ (Data API, Loaders/Actions), TanStack Router (Type-safe).
State ManagementЛокальный (useState/reducer), Context + useReducer, Zustand (простой глобальный), Jotai/Recoil (атомарный), Redux Toolkit (RTK) (엔терпрайз, DevTools, RTK Query).
Server State / Data FetchingTanStack Query (React Query) / SWR / RTK Query (Кэширование, дедупликация, рефетчинг, мутации, оптимистичные обновления). Не пишите fetch в useEffect в проде.
ФормыReact Hook Form (производительность, минимум ре-рендеров) + Zod / Valibot / Yup (валидация схем).
СтилизацияCSS Modules (Scoped CSS), Tailwind CSS (Utility-first, JIT), CSS-in-JS (Panda CSS, Linaria — zero-runtime; styled-components/emotion — runtime), Vanilla Extract (Type-safe, build-time).
ТипизацияTypeScript (Strict mode). React.FC<Props> (устаревает), лучше ({ props }: Props) => JSX.Element.
ТестированиеVitest / Jest (Unit), React Testing Library (Integration, user-centric), Playwright / Cypress (E2E).
Сборка (Bundlers)Vite (Стандарт де-факто, Rollup/esbuild), Next.js (Fullstack Framework, App Router, RSC), Remix (Web Standards, Nested Routing).

5. Server Components & Next.js App Router (Современная реальность 2023-2024)

React Server Components (RSC) — новая архитектура (не просто SSR):

  • Компоненты выполняются только на сервере (или в билде). Их JS-код никогда не попадает в бандл клиента (Zero Bundle Size).
  • Прямой доступ к БД, ФС, секретам внутри компонента (async/await на уровне компонента).
  • Потоковая передача (Streaming) HTML + Suspense boundaries.
  • Client Components ('use client') — интерактивность, хуки, браузерные API. Граница сериализации пропсов.
  • Next.js 13+ (App Router) — основной фреймворк, реализующий RSC на продакшн-уровне.

6. Чек-лист Senior/Tech Lead: Code Review & Architecture

  1. Нет useEffect для синхронизации состояния (derived state -> useMemo / просто переменная во время рендера).
  2. Нет useEffect для data fetching без очистки (race conditions) -> TanStack Query / SWR.
  3. Стабильность ссылок: Колбэки в пропсах -> useCallback. Объекты/массивы в пропсах/контексте -> useMemo.
  4. Избегайте Prop Drilling -> Composition (children / slots) или Context / State Management.
  5. Ключи (key) — стабильные, уникальные ID из БД. НЕ index, НЕ Math.random().
  6. Строгая типизация: strict: true в TS. Пропсы — interface Props. Избегайте any.
  7. Доступность (a11y): Семантический HTML, aria-*, фокус-менеджмент, eslint-plugin-jsx-a11y.
  8. Ошибки: Error Boundary (классовый компонент) для graceful degradation UI. react-error-boundary библиотека.
  9. Bundle Size: Аудит (vite-bundle-analyzer, next/bundle-analyzer). Динамические импорты для тяжелых фич (Charts, Editors, Modals).
  10. Миграция на RSC (если Next.js): Понимать границу 'use client'. Не передавать несериализуемые данные (функции, классы, DOM-ноды) в Server -> Client пропсы.

Резюме

> React — это экосистема для построения UI через композицию чистых функций, управляемую иммутабельным состоянием и виртуальным DOM. > > Сила: Гибкость, огромная экосистема, производительность (Fiber, Concurrent), Server Components (RSC), нативная мобильная разработка (React Native), мощное комьюнити. > > Сложность: "Decision Fatigue" (выбор инструментов), ручное управление производительностью (мемоизация), кривая обучения хуков/моделей мышления (Effects vs Events), частые ломающие изменения (App Router, RSC). > > Senior Mindset: React — это не просто npm install react. Это понимание Reconciliation, Commit Phases, Batching, Suspense Boundaries, Streaming SSR, Hydration Mismatches и умение проектировать архитектуру данных (Client vs Server State) под конкретный продукт.

Вопрос 19. Что такое JSX, почему браузер не понимает его напрямую и как его запускать в браузере?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:11:10"/>

Оценка ответа кандидата: неполный. Кандидат назвал JSX комбинацией HTML и JavaScript (верно), но ошибся, назвав его JS6 и сказав, что браузер понимает его напрямую («сканирование»). После подсказки интервьюера согласился, что нужен транспайлер (Babel), преобразующий JSX в обычный JS.


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

JSX (JavaScript XML) — это синтаксическое расширение JavaScript (ECMAScript), позволяющее писать XML-подобную разметку (HTML/SVG) непосредственно внутри JS-кода. Это не отдельный язык, а «сахар» (Syntactic Sugar), который компилируется в вызовы функций React.createElement.


1. Почему браузер НЕ понимает JSX напрямую?

Браузеры исполняют только стандартизированный ECMAScript (JavaScript) и парсят HTML/CSS.

  • JSX содержит синтаксические конструкции (<div />, <MyComponent prop={val} />), которые невалидны в спецификации ECMAScript.
  • Если передать файл с JSX в <script> тег или модуль (type="module"), парсер JS движка (V8, SpiderMonkey, JavaScriptCore) упадет с SyntaxError на первом угловом скобке <, не ожидая её в контексте выражения.

> Миф: «Браузер сканирует JSX». Реальность: Браузер видит невалидный токен и останавливает парсинг.


2. Во что компилируется JSX (Трансформация)

Стандартная трансформация (Classic Runtime / React 16+) через Babel (@babel/preset-react), TypeScript (tsx), SWC, esbuild:

Исходный код (JSX):

function Greeting({ name }) {
return (
<div className="greeting" data-id={1}>
<h1>Hello, {name}!</h1>
<CustomIcon />
</div>
);
}

Результат компиляции (JavaScript):

import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime'; // Новый Automatic Runtime (React 17+)
// import React from 'react'; // Старый Classic Runtime: React.createElement

function Greeting({ name }) {
return _jsxs('div', {
className: 'greeting',
'data-id': 1,
children: [
_jsx('h1', { children: 'Hello, ', name: name, '!' }), // Упрощенно
_jsx(CustomIcon, {})
]
});
}

Ключевые моменты трансформации:

  1. Теги -> Вызовы функций: <Tag />_jsx('Tag', props) или _jsxs('Tag', props) (оптимизация для статических детей).
  2. Атрибуты -> Объект props: className="x"{ className: 'x' }. onClick={fn}{ onClick: fn }.
  3. Интерполяция {expr} → Прямая подстановка выражения expr в пропсы/children.
  4. Компоненты (PascalCase): <CustomIcon />_jsx(CustomIcon, {}) (передается ссылка на функцию/класс, а не строка).
  5. Automatic Runtime (React 17+): Не нужно импортировать React в каждый файл. Компилятор сам импортирует jsx/jsxs из react/jsx-runtime. Уменьшает бандл и упрощает жизнь.

3. Как запустить JSX в браузере: Способы доставки

Поскольку браузер не понимает JSX, код обязательно должен быть преобразован в JS до исполнения. Есть два основных подхода:


А. Build-time Compilation (Стандарт индустрии — Production Ready)

Код трансформируется на этапе сборки (Build Step) с помощью бандлеров/транспайлеров. Браузер получает чистый, минифицированный JS.

ИнструментРольПример конфига
Vite (Рекомендуемый стандарт 2024+)Дев-сервер (esbuild) + Продакшн билд (Rollup). Мгновенный HMR.npm create vite@latest my-app -- --template react-ts
Next.js (App Router / Pages)Фреймворк: SSR, RSC, Routing, Build (Turbopack/Webpack/SWC).npx create-next-app@latest
Webpack + babel-loader / swc-loaderКлассический бандлер. Гибкий, но медленнее Vite/Next.module: { rules: [{ test: /\.[jt]sx?$/, use: 'babel-loader' }] }
esbuild / SWCНаписаны на Go/Rust. Очень быстрые (10-100x быстрее Babel). Используются внутри Vite, Next.js, Turbopack.Часто используются как замена Babel для трансформации TS/JSX.
TypeScript (tsc)Проверка типов. Может компилировать TSX -> JS (jsx: "react-jsx"), но не бандлит (не резолвит импорты, не делает code-splitting). Нужен бандлер поверх.tsconfig.json: { "jsx": "react-jsx", "module": "ESNext" }

Пайплайн (Vite/Next.js):

  1. Dev: src/App.tsxesbuild/SWC (трансформация TSX->JS, ~мс) → ESM модули → Браузер (Native ESM + HMR).
  2. Prod: src/App.tsxSWC/Babel (TSX->JS) → Rollup/esbuild (Bundling, Tree-shaking, Minification, Hashing) → dist/assets/index-[hash].js → CDN → Браузер.

Б. Runtime Compilation (Только для обучения / Песочницы / CMS)

JSX трансформируется прямо в браузере пользователя с помощью standalone-билда Babel (@babel/standalone).

<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<!-- 1. React & ReactDOM (UMD/ESM) -->
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>

<!-- 2. Babel Standalone (Транспайлер в браузере) -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

<!-- 3. Ваш JSX код (type="text/babel" говорит Babel'у обработать этот скрипт) -->
<script type="text/babel" data-presets="react">
const App = () => <h1>Hello, JSX in Browser!</h1>;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>

Почему это НЕ для продакшена:

  1. Производительность: Транспиляция на клиенте = задержка TTI (Time to Interactive). Парсинг JSX + компиляция + выполнение.
  2. Размер бандла: babel.min.js весит ~1-2 МБ (gzipped ~300-400 КБ) — только для компилятора.
  3. Безопасность: eval / new Function используются для исполнения сгенерированного кода (CSP issues).
  4. Отсутствие оптимизаций: Нет Tree-shaking, минификации, хеширования, code-splitting.

4. Нюансы, о которых спрашивают на Senior/Tech Lead уровнях

А. JSX Pragma / Custom Runtime Можно изменить функцию, в которую компилируется JSX (для не-React библиотек: Preact, Solid, htmx, кастомные рендереры).

// babel.config.js или комментарий в файле
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from 'preact';
// <div /> -> h('div', ...)

Б. jsx: "preserve" в TypeScript Если вы используете Babel/SWC/esbuild для трансформации, а tsc только для типов:

// tsconfig.json
{ "compilerOptions": { "jsx": "preserve", "module": "ESNext" } }

tsc оставит JSX как есть (.tsx -> .jsx или .ts с JSX внутри), а бандлер превратит в JS. Это ускоряет проверку типов и избегает двойной работы.

В. Ключи (key) и статический анализ Компилятор (Babel/SWC) и линтеры (eslint-plugin-react) анализируют JSX статически:

  • Предупреждают о пропущенных key в массивах.
  • Проверяют доступность (jsx-a11y: alt у img, role/tabindex).
  • Оптимизируют _jsxs vs _jsx (статические дети vs динамические).

Г. SVG в JSX SVG атрибуты в React — camelCase (strokeWidth, viewBox, fillRule), а не kebab-case как в HTML. Компилятор не исправит это за вас, но TS типы (React.SVGProps) подскажут.

Д. Комментарии в JSX Только внутри детей: {/* Комментарий */}. HTML-комментарии <!-- --> внутри JSX — ошибка синтаксиса (кроме специфических случаев в dangerouslySetInnerHTML).


5. Чек-лист: Настройка JSX в проекте (Best Practices 2024)

  1. Runtime: "jsx": "react-jsx" (TS) / "runtime": "automatic" (Babel) — Автоматический рантайм (React 17+). Не импортируйте React вручную.
  2. Трансформер: SWC (в Next.js, Vite 5+, Turbopack) или esbuild (Vite dev) — для скорости. Babel — только если нужны специфические плагины (macro, styled-components, relay), которых нет в SWC.
  3. TS Config: "jsx": "react-jsx" + "jsxImportSource": "react" (или @emotion/react и т.д.).
  4. React Refresh: Настроен HMR (Fast Refresh) для сохранения стейта при редактировании компонентов (встроено в Vite/Next/CRA).
  5. Production Build: Минификация (Terser/esbuild), удаление propTypes/__DEV__ проверок, хеширование имен файлов.

Резюме (One-Liner для собеседования)

> JSX — это синтаксический сахар над React.createElement / jsx() вызовы. > Браузеры его не понимают, так как это невалидный JS синтаксис (XML в выражениях). > Для запуска в браузере обязателен шаг компиляции (Build Step) — транспиляция в чистый JS с помощью Babel / SWC / esbuild / TypeScript, обычно встроенная в бандлер (Vite, Webpack, Next.js, Rollup). > Runtime-компиляция через @babel/standalone существует, но категорически неприменима в продакшене из-за производительности и размера.

Вопрос 20. Какие методы жизненного цикла вы знаете в React (для классовых компонентов)?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:12:17"/>

Оценка ответа кандидата: неполный. Кандидат перечислил фазы: монтирование (Mount), обновление (Update), обработка ошибок. Назвал, что Mount происходит при инициализации компонента. Не привел конкретные методы (componentDidMount, componentDidUpdate, componentWillUnmount, static getDerivedStateFromError, componentDidCatch).


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

Хотя современный React (с 2019 года) ориентирован на функциональные компоненты и Хуки, понимание жизненного цикла классовых компонентов (React.Component) необходимо для поддержки легаси-кода, миграции и понимания внутренней работы React (Fiber).

Жизненный цикл делится на 4 фазы. На схеме ниже: жирным — рекомендуемые/актуальные, зачеркнутымиустаревшие (Legacy/Unsafe), удаленные в React 17+ или требующие префикса UNSAFE_.


1. Фаза Монтирования (Mounting) — Рождение компонента

Порядок вызова при первом рендере:

  1. constructor(props)
    • Инициализация this.state, биндинг обработчиков (this.handleClick = this.handleClick.bind(this)).
    • Единственное место, где можно присваивать this.state напрямую.
    • Вызов super(props) обязателен, если нужен this.props в конструкторе.
  2. UNSAFE_componentWillMount()УСТАРЕЛ. Вызывался перед рендером. Не используйте.
  3. render()Обязательный. Чистая функция. Возвращает React Element / null / string / number / array / portal. Не должен мутировать стейт.
  4. componentDidMount()Главный метод фазы.
    • Вызывается после коммита в DOM (после useLayoutEffect в хуках).
    • Кейсы: Подписки (WebSocket, EventListeners), запуск анимаций, fetch данных (хотя лучше в useEffect/библиотеках), работа с DOM-нодами (ref.current), интеграция с не-React библиотеками (D3, Chart.js, Mapbox).

2. Фаза Обновления (Updating) — Реакция на пропсы/стейт

Вызывается при setState, forceUpdate или изменении пропсов родителя.

  1. UNSAFE_componentWillReceiveProps(nextProps)УСТАРЕЛ. Источник багов при ререндерах родителя.
  2. static getDerivedStateFromProps(props, state)Static метод.
    • Вызывается перед каждым рендером (и при монтировании, и при обновлении).
    • Назначение: Синхронизация стейта с пропсами (редкий кейс: controlled -> uncontrolled переходы).
    • Возврат: Объект для обновления стейта или null.
    • Нет доступа к this! Только чистая логика props -> state.
  3. shouldComponentUpdate(nextProps, nextState)Оптимизация.
    • Возврат false пропускает render, componentDidUpdate и ререндер детей (но не предотвращает рендер детей, если они не PureComponent/memo).
    • По умолчанию true. Используется для ручной оптимизации (глубокое сравнение).
    • Аналог: React.PureComponent (shallow compare) / React.memo.
  4. UNSAFE_componentWillUpdate(nextProps, nextState)УСТАРЕЛ.
  5. render()
  6. getSnapshotBeforeUpdate(prevProps, prevState)Редко, но важно.
    • Вызывается прямо перед коммитом в DOM (после render, до мутаций DOM).
    • Возврат: Любое значение (snapshot), передается 3-м аргументом в componentDidUpdate.
    • Кейс: Захват скролл-позиции, размеров элементов (el.getBoundingClientRect()) перед обновлением DOM, чтобы восстановить их после.
  7. componentDidUpdate(prevProps, prevState, snapshot)Главный метод обновления.
    • Вызывается после коммита изменений в DOM.
    • Кейсы: Сетевые запросы при изменении пропсов (сравнивая prevProps), работа с DOM, логирование, синхронизация со сторонними либами.
    • Обязательно оборачивать setState в условие сравнения, иначе бесконечный цикл.

3. Фаза Размонтирования (Unmounting) — Смерть компонента

  1. componentWillUnmount()Главный метод очистки.
    • Вызывается прямо перед удалением из DOM.
    • Кейсы (Cleanup):
      • Очистка таймеров (clearTimeout, clearInterval).
      • Отписка от событий (removeEventListener, ws.close(), subscription.unsubscribe()).
      • Отмена сетевых запросов (abortController.abort()).
      • Очистка внешних библиотек (уничтожение экземпляров D3, Chart.js, Mapbox).

4. Фаза Обработки Ошибок (Error Boundaries) — Ловля ошибок рендера

Только для Классовых компонентов. Превращают компонент в "Error Boundary".

  1. static getDerivedStateFromError(error)Static.
    • Вызывается при ошибке в поддереве (дети).
    • Назначение: Обновить стейт для показа фоллб UI (например, this.setState({ hasError: true })).
    • Не имеет доступа к this.
  2. componentDidCatch(error, errorInfo)Instance метод.
    • Вызывается после getDerivedStateFromError.
    • Назначение: Логирование ошибок (Sentry, LogRocket, консоль), side-эффекты.
    • errorInfo.componentStack — стек компонентов (очень ценен для дебага).

> Важно: Error Boundaries не ловят ошибки в: обработчиках событий (onClick), асинхронном коде (setTimeout, Promise), componentDidMount/Update/Unmount самого себя, SSR, самих Error Boundaries.


5. Устаревшие (Legacy) методы — "The Unsafe Trio"

Начиная с React 16.3 (2018) помечены как UNSAFE_, в React 17+ работают с предупреждением, в будущем будут удалены. Не используйте в новом коде.

  • UNSAFE_componentWillMount → Заменить на constructor / componentDidMount.
  • UNSAFE_componentWillReceiveProps → Заменить на getDerivedStateFromProps / componentDidUpdate / хуки.
  • UNSAFE_componentWillUpdate → Заменить на getSnapshotBeforeUpdate / componentDidUpdate.

Проблема: Они вызывались в "Render Phase" (фазе рендера), которая в Concurrent Mode (React 18+) может быть прервана, перезапущена или отброшена. Это приводит к багам: двойные вызовы, утечки памяти, неконсистентное состояние.


6. Специальные API (не совсем lifecycle, но связано)

  • forceUpdate(callback) — Принудительный ререндер, минуя shouldComponentUpdate. Антипаттерн. Нужен только для интеграции с не-React кодом, мутирующим данные.
  • setState(updater, callback) — Асинхронный (батчится). updater может быть функцией (prevState, props) => newState (гарантирует актуальность). callback выполняется после коммита.
  • static defaultProps / defaultProps — Дефолтные пропсы (депрекейт в пользу дефолтных параметров функции или || / ??).
  • static propTypes — Runtime проверка типов (депрекейт в пользу TypeScript).
  • displayName — Имя для DevTools (авто для именованных классов).

7. Маппинг Class Lifecycle → Hooks (Для миграции/понимания)

Class MethodHooks EquivalentНюансы
constructoruseState / useRef (lazy init)useRef для mutable значений без ре-рендера.
componentDidMountuseEffect(() => { ... }, [])Эффект с пустым массивом зависимостей. Запускается после paint (асинхронно).
componentDidUpdateuseEffect(() => { ... }, [deps])Зависимости — ключ. Сравнение Object.is.
componentWillUnmountuseEffect(() => { return () => { cleanup } }, [])Функция очистки (cleanup) в возврате эффекта.
getSnapshotBeforeUpdateuseLayoutEffect(() => { ... }, [deps])Синхронно после мутаций DOM, до paint. Для измерений.
componentDidCatch / getDerivedStateFromErrorНет прямого аналогаError Boundaries остаются только классовыми. Оборачивайте дерево в классовый EB.
shouldComponentUpdateReact.memo(Comp, areEqual?) / useMemo / useCallbackmemo — shallow compare пропсов. Кастомный компаратор — 2-й аргумент.
PureComponentReact.memoАналог shallow compare.

8. Best Practices для Legacy Class Components (Code Review Checklist)

  1. Никакой логики в render — только чистый вывод.
  2. setState только функционально: this.setState(prev => ({ count: prev.count + 1 })).
  3. Биндинг в конструкторе или Property Initializers (Stage 3): handleClick = () => {} (стрелочные свойства класса — авто-биндинг this).
  4. Очистка в componentWillUnmount — обязательна для любого addEventListener, setInterval, подписок.
  5. getDerivedStateFromProps — крайняя мера. Часто задачу решает "Lifting State Up" или key проп для ресета компонента.
  6. Error Boundary — обязателен на корне приложения и вокруг виджетов/фич, чья ошибка не должна ломать весь экран.
  7. Миграция: Не переписывайте рабочие классовые компоненты "для красоты". Пишите новые на хуках. При рефакторинге фичи — конвертируйте.

Резюме (Cheatsheet для собеседования)

> Фазы: Mount → Update → Unmount → Error Handling. > > Must Know методы: > * Mount: constructorrendercomponentDidMount (сайд-эффекты, подписки). > * Update: getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate (реакция на изменения). > * Unmount: componentWillUnmount (cleanup!). > * Errors: static getDerivedStateFromError (UI fallback) + componentDidCatch (logging). > > Legacy (Don't use): componentWillMount, componentWillReceiveProps, componentWillUpdate (проблемы с Concurrent Mode). > > Modern Way: Функциональные компоненты + Хуки (useEffect, useLayoutEffect, useMemo, memo). Error Boundaries — единственное, что пока требует классов.

Вопрос 21. Есть ли у вас представление о методе Unmount (размонтировании компонента)?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:12:59"/>

Оценка ответа кандидата: правильный. Кандидат верно описал Unmount как момент завершения жизни компонента (очистка).


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

Размонтирование (Unmounting) — это финальная фаза жизненного цикла классового компонента, происходящая непосредственно перед удалением компонента из DOM-дерева. Это единственный гарантированный момент для выполнения «чистки» (cleanup) ресурсов, захваченных компонентом за время его жизни.

В классовых компонентах это закреплено за единственным методом:

componentWillUnmount()

1. Сигнатура и момент вызова

componentWillUnmount() {
// Синхронный код очистки
}
  • Вызывается один раз за весь жизненный цикл.
  • Выполняется синхронно перед тем, как React начнет удалять DOM-узлы компонента и его детей.
  • Компонент все еще имеет доступ к this.props, this.state, this.refs (DOM-узлы еще живы).
  • Нельзя вызывать this.setState() — это приведет к предупреждению (Warning) и игнорированию обновления, так как компонент уже «мертв».

2. Главные сценарии использования (Cleanup Checklist)

Любой ресурс, созданный вручную и не управляемый React'ом, обязан быть освобожден здесь. Игнорирование приводит к утечкам памяти (Memory Leaks), накоплению слушателей и багам.

Категория ресурсаПример созданияОбязательная очистка в componentWillUnmount
Таймерыthis.timerId = setInterval(() => ..., 1000)<br>this.timeoutId = setTimeout(() => ..., 5000)clearInterval(this.timerId)<br>clearTimeout(this.timeoutId)
DOM Eventswindow.addEventListener('resize', this.handleResize)<br>document.addEventListener('click', this.handleClick, true)window.removeEventListener('resize', this.handleResize)<br>document.removeEventListener('click', this.handleClick, true)
WebSockets / SSEthis.ws = new WebSocket(url);<br>this.es = new EventSource(url);this.ws.close()<br>this.es.close()
Подписки на Store/Event Busthis.unsubscribe = store.subscribe(this.onChange)<br>EventBus.on('event', this.handler)this.unsubscribe()<br>EventBus.off('event', this.handler)
Observables (RxJS)this.sub = observable$.subscribe(...)this.sub.unsubscribe()
AbortController (Fetch)this.controller = new AbortController(); fetch(url, { signal: this.controller.signal })this.controller.abort()
Сторонние библиотеки (D3, Chart.js, Mapbox, Leaflet, jQuery plugins)this.chart = new Chart(ctx, config)<br>this.map = new mapboxgl.Map(...)this.chart.destroy()<br>this.map.remove()
IntersectionObserver / ResizeObserver / MutationObserverthis.observer = new IntersectionObserver(cb)this.observer.disconnect()
requestAnimationFramethis.rafId = requestAnimationFrame(this.tick)cancelAnimationFrame(this.rafId)
Прямые DOM-манипуляцииdocument.body.style.overflow = 'hidden'document.body.style.overflow = '' (восстановление)

3. Типичная реализация (Best Practice Pattern)

class ChatRoom extends React.Component {
constructor(props) {
super(props);
this.state = { messages: [] };
// 1. Инициализация рефов/ID в конструкторе или как поля класса
this.ws = null;
this.scrollTimeout = null;
this.handleResize = this.handleResize.bind(this); // Биндинг для removeEventListener
}

componentDidMount() {
// 2. Захват ресурсов
this.ws = new WebSocket(this.props.url);
this.ws.onmessage = (e) => this.setState({ messages: [...this.state.messages, e.data] });

window.addEventListener('resize', this.handleResize);

// Таймер для автоскролла
this.scrollTimeout = setInterval(() => this.scrollToBottom(), 500);
}

componentWillUnmount() {
// 3. ОЧИСТКА В ОБРАТНОМ ПОРЯДКЕ (не критично, но хорошая практика)

// Асинхронные операции/сетевое
if (this.ws) {
this.ws.onmessage = null; // Предотвращаем setState на размонтированном
this.ws.close();
this.ws = null;
}

// DOM Events
window.removeEventListener('resize', this.handleResize);

// Таймеры
if (this.scrollTimeout) {
clearInterval(this.scrollTimeout);
this.scrollTimeout = null;
}

// Отмена анимаций
if (this.rafId) cancelAnimationFrame(this.rafId);
}

// ...
}

4. Критические нюансы и ловушки (Senior Level)

А. setState в componentWillUnmount — ЗАПРЕЩЕНО React выдаст Warning: "Can't perform a React state update on an unmounted component".

  • Проблема: Асинхронный колбэк (WebSocket, setTimeout, Promise) срабатывает после размонтирования и вызывает setState.
  • Решение: Использовать флаг-страж (guard flag) или AbortController.
    componentDidMount() {
    this._isMounted = true; // Флаг
    fetchData().then(data => {
    if (this._isMounted) this.setState({ data }); // Проверка
    });
    }
    componentWillUnmount() {
    this._isMounted = false;
    }
    Лучше: Использовать AbortController для fetch или библиотеки вроде react-query / TanStack Query, которые управляют этим автоматически.

Б. Порядок размонтирования детей (Children First) React размонтирует дерево снизу вверх (Depth-First).

  1. Child.componentWillUnmount()
  2. Parent.componentWillUnmount()
  • Это гарантирует, что родитель может чистить свои ресурсы, зная, что дети уже почистили свои.

В. componentWillUnmount vs useEffect cleanup (Миграция) В хуках cleanup-функция возвращается из useEffect:

// Class
componentWillUnmount() { clearInterval(id); }

// Hooks (Functional)
useEffect(() => {
const id = setInterval(...);
return () => clearInterval(id); // Это АЛЬТЕРНАТИВА componentWillUnmount
}, []);
  • Важно: Cleanup в хуках срабатывает перед следующим эффектом (при изменении deps) И при размонтировании. В классах componentWillUnmount — только при смерти.

Г. Очистка refs Если вы храните ссылки на DOM-узлы (this.myRef = React.createRef()) или экземпляры классов — React обнулит их автоматически после componentWillUnmount. Явная очистка this.myRef = null не требуется, но полезна для GC, если реф держал тяжелый объект.

Д. Ошибки в componentWillUnmount Если внутри метода выбросится исключение (uncaught error):

  1. React прервет размонтирование текущего компонента.
  2. Ошибка всплывет к ближайшему Error Boundary (componentDidCatch).
  3. Остальные componentWillUnmount в дереве (родители) НЕ ВЫПОЛНЯТСЯ.
  • Вывод: Оборачивайте чистку в try/catch, если вызываете ненадежный сторонний код (chart.destroy() может бросить ошибку, если уже уничтожен).

5. Антипаттерны (Чего НЕ делать)

  1. Забывать bind / стрелочные функции для слушателей:
    // ПЛОХО: removeEventListener не сработает, т.к. ссылка на функцию другая
    componentDidMount() { window.addEventListener('click', () => this.handleClick()); }
    componentWillUnmount() { window.removeEventListener('click', () => this.handleClick()); }

    // ХОРОШО: Сохраняем ссылку
    handleClick = () => { ... } // Class field arrow function
    componentDidMount() { window.addEventListener('click', this.handleClick); }
    componentWillUnmount() { window.removeEventListener('click', this.handleClick); }
  2. Чистить состояние (setState) вместо ресурсов. Стейт умрет с компонентом сам.
  3. Использовать для отмены анимаций CSS/Transition. Для этого есть TransitionGroup / react-transition-group или onExited колбэки.
  4. Дублировать логику в componentDidUpdate при смене ключевых пропсов. Если проп userId меняется — компонент не размонтируется/монтируется (если нет key). Нужен componentDidUpdate + сравнение или смена key у родителя для полного ресета.

Резюме

> componentWillUnmount() — это деструктор компонента. > Единственная задача: детерминированная очистка сайд-эффектов (таймеры, слушатели, сокеты, подписки, внешние инстансы). > Ошибки здесь ломают размонтирование родителей → всегда try/catch для внешних API. > В современном React (Hooks) заменяется на return () => cleanup внутри useEffect. > Отсутствие очистки = Memory Leak (утечка памяти) и баги "призраков" (обновление стейта размонтированных компонентов).

Вопрос 22. Можно ли реализовать методы жизненного цикла в функциональном компоненте и с помощью какого хука?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:13:08"/>

Оценка ответа кандидата: правильный. Кандидат верно ответил, что это делается с помощью хуков, сначала назвал useState, но сам исправился на useEffect, который объединяет монтирование, обновление и размонтирование.


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

Да, полностью. В функциональных компонентах жизненный цикл не привязан к отдельным методам (как componentDidMount / componentDidUpdate / componentWillUnmount), а реализуется через Хуки (Hooks) — преимущественно useEffect и useLayoutEffect.

Фундаментальное отличие мышления: не «в какой момент жизни я нахожусь», а «с какими данными я хочу синхронизироваться».


1. useEffect — Основной хук для Side Effects (Побочных эффектов)

useEffect(effectCallback, dependencyArray?);

Сигнатура колбэка:

// effectCallback может вернуть функцию очистки (cleanup function)
useEffect(() => {
// 1. Код эффекта (Setup): Подписки, таймеры, fetch, DOM манипуляции

return () => {
// 2. Функция очистки (Cleanup/Teardown): Отписки, clearTimeout, abort
// Вызывается ПЕРЕД следующим эффектом И при размонтировании
};
}, [dep1, dep2, ...]); // 3. Массив зависимостей (Deps)

2. Маппинг Class Lifecycle → useEffect (Ментальная карта)

Class MethoduseEffect PatternКогда запускается Cleanup / Effect
componentDidMountuseEffect(() => { ... }, [])Mount: Effect после рендера.<br>Unmount: Cleanup при размонтировании.
componentDidUpdateuseEffect(() => { ... }, [dep1, dep2])Update: Cleanup (предыдущие deps) → Effect (новые deps) при изменении deps.
componentWillUnmountuseEffect(() => { return () => { ... } }, [])Unmount: Cleanup при размонтировании (effect пустой или только cleanup).
getSnapshotBeforeUpdateuseLayoutEffect (см. ниже)Синхронно после мутаций DOM, до paint.

3. Детальная механика: Фазы выполнения (React 18+ / Concurrent Features)

Понимание порядка критично для избежания багов (race conditions, утечки памяти).

А. Монтирование (Mount)

  1. Рендер (Render Phase) — компонент выполняется, создается новый UI.
  2. Commit Phase — React обновляет DOM.
  3. Layout Effects (useLayoutEffect) — синхронно, до браузерного paint.
  4. Browser Paint — пользователь видит UI.
  5. Passive Effects (useEffect) — асинхронно, после paint (не блокирует отрисовку).

Б. Обновление (Update) — при изменении зависимостей

  1. Рендер (новый стейт/пропсы).
  2. Cleanup предыдущего эффекта (вызывается синхронно перед Layout Effects нового рендера!).
  3. Commit Phase (DOM обновлен).
  4. Layout Effects (новые).
  5. Paint.
  6. Effect (новый) — асинхронно.

В. Размонтирование (Unmount)

  1. Cleanup последнего эффекта (синхронно, перед удалением DOM).
  2. Удаление DOM-узлов.
  3. Layout Effects Cleanup (родителей/детей).

> Ключевой момент: Cleanup вызывается ПЕРЕД следующим Effect (при обновлении) или ПЕРЕД удалением DOM (при анмаунте). Это гарантирует, что старая подписка снята до создания новой.


4. Массив зависимостей (Dependency Array) — Сердце логики

ВариантПоведениеАналог в классах
[] (Пустой)Effect один раз при монтировании. Cleanup — при анмаунте.componentDidMount + componentWillUnmount
[propA, stateB]Effect при монтировании + при каждом изменении propA или stateB (сравнение Object.is).componentDidMount + componentDidUpdate (с проверкой prevProps.propA !== this.props.propA)
ОтсутствуетEffect после каждого рендера. Cleanup перед каждым следующим эффектом.componentDidMount + componentDidUpdate (без проверок) — Опасно для производительности!

Правила зависимостей (Exhaustive Deps / eslint-plugin-react-hooks):

  • Все значения из области видимости компонента (пропсы, стейт, контекст, функции, переменные), используемые внутри эффекта, обязаны быть в массиве зависимостей.
  • Исключение: стабильные ссылки (API setStatedispatch, setState из useState, ref.currentне зависимость, но чтение ref.current внутри эффекта не требует его в deps, так как мутация не триггерит рендер).

5. Типичные паттерны и Best Practices

А. Fetch / Асинхронность (Race Conditions)

useEffect(() => {
let cancelled = false; // Guard flag
const controller = new AbortController(); // Modern way

async function fetchData() {
try {
const res = await fetch(`/api/${id}`, { signal: controller.signal });
const data = await res.json();
if (!cancelled) setData(data); // Проверка флага
} catch (e) {
if (!cancelled && e.name !== 'AbortError') setError(e);
}
}
fetchData();

// CLEANUP: Срабатывает при изменении id ИЛИ при анмаунте
return () => {
cancelled = true;
controller.abort(); // Отмена сетевого запроса
};
}, [id]); // Зависимость от id — при смене id: cleanup старого -> effect нового

Б. Подписки / Event Listeners

useEffect(() => {
const handler = (e) => setWindowSize(e.target.innerWidth);
window.addEventListener('resize', handler);

return () => window.removeEventListener('resize', handler); // Обязательно та же ссылка!
}, []); // Пустой массив: подписка на всю жизнь компонента

В. Таймеры / Интервалы

useEffect(() => {
const id = setInterval(() => setTime(Date.now()), 1000);
return () => clearInterval(id); // Очистка таймера
}, []);

Г. Оптимизация: Стабильные ссылки на функции (Callbacks в deps) Если эффект зависит от функции, она должна быть стабильной, иначе эффект будет перезапускаться каждый рендер.

// ПЛОХО: handleClick новая каждый рендер -> эффект перезапускается
const handleClick = () => doSomething(expensive);
useEffect(() => { ... }, [handleClick]);

// ХОРОШО 1: useCallback
const handleClick = useCallback(() => doSomething(expensive), [expensive]);
useEffect(() => { ... }, [handleClick]);

// ХОРОШО 2: Вынос логики внутрь эффекта (если не нужна снаружи)
useEffect(() => {
function doWork() { ... } // Локальная функция
doWork();
}, [deps]); // Зависит только от данных

6. useLayoutEffect — Синхронный брат useEffect

useLayoutEffect(() => { ... }, [deps]);

Разница:

ХарактеристикаuseEffectuseLayoutEffect
ТаймингАсинхронно, после Paint (не блокирует UI).Синхронно, после мутаций DOM, до Paint.
БлокировкаНет.Да, блокирует отрисовку браузера.
Кейсы95% случаев: fetch, подписки, логика, таймеры.Измерение DOM (getBoundingClientRect), фокус, скролл, предотвращение «мерцания» (flicker) при мутациях DOM.
SSRБезопасен (не выполняется на сервере).Предупреждение на сервере (выполняется синхронно, может сломать стриминг). Используйте useEffect или useIsomorphicLayoutEffect паттерн.

7. Другие хуки, связанные с жизненным циклом

ХукРоль в жизненном цикле
useState / useReducerИнициализация стейта (Mount). Обновление стейта -> триггерит Update фазу. Lazy init: useState(() => expensiveCalc()) — только при монтировании.
useMemo / useCallbackМемоизация значений/функций во время рендера. Не имеют cleanup. Используются для стабилизации deps других хуков.
useRefМутирующий контейнер (current). Не триггерит ре-рендер при изменении. Жизненный цикл = жизнь компонента. Идеален для хранения ID таймеров, экземпляров классов, предыдущих значений (usePrevious паттерн).
useImperativeHandleНастройка ref, передаваемого родителю (экспозиция методов). Вызывается при коммите.
useInsertionEffect (React 18+)Очень рано, перед Layout Effects. Для CSS-in-JS библиотек (вставка стилей в <head> до измерения layout). Не имеет доступа к DOM-нодам компонента (еще не вставлены).

8. Частые ошибки (Anti-patterns) на собеседованиях

  1. Забытый return cleanup → Утечка памяти, двойные подписки, setState на размонтированном.
  2. Неполный deps массив (или его отсутствие) → Бесконечные циклы, стёртые инпуты, лишние запросы. ESLint exhaustive-deps — закон.
  3. Асинхронная функция прямо в useEffect:
    // ОШИБКА: async функция возвращает Promise, React думает, что это cleanup функция!
    useEffect(async () => { await fetch(); }, []);

    // ПРАВИЛЬНО: IIFE или внутренняя функция
    useEffect(() => { (async () => { await fetch(); })(); }, []);
  4. Чтение state/props в эффекте без депы (Stale Closure) → Видят старые значения.
    • Решение: Добавить в deps, или использовать функциональное обновление setState(prev => ...), или useRef для актуального значения без ре-рендеров.
  5. Сложная логика в одном useEffect → Разбивайте по ответственности: отдельный useEffect для fetch, для подписки, для таймера. У каждого свои deps и cleanup.

Резюме (Senior Cheatsheet)

> useEffect — это не componentDidMount + componentDidUpdate + componentWillUnmount. > Это декларативная синхронизация с внешними системами. > > Алгоритм мышления: > 1. Что синхронизируем? (DOM, Network, Store, Timer). > 2. От чего зависит? (Deps array — props, state, стабильные функции). > 3. Как откатить? (Cleanup function — обязательна для любого side-effect). > 3. Когда запускать? (После paint — useEffect / До paint для измерений — useLayoutEffect). > > Миграция с классов: Не ищите 1-в-1 маппинг методов. Перепишите логику: «При изменении X я хочу сделать Y и откатить Z».

Вопрос 23. Что такое ref в React и для чего он нужен?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:13:34"/>

Оценка ответа кандидата: неправильный. Кандидат дал неверный и путаный ответ: назвал ref «разницей между React и React», свойством, упомянул «Rift» и фокус, не объяснил суть: доступ к DOM-узлу или сохранение значения без ререндера через useRef/createRef.


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

Ref (Reference / Ссылка) — это механизм React для императивного доступа к экземплярам (DOM-узлам или компонентам) или для хранения мутируемых значений, которые не триггерят ре-рендер при изменении.

Это «аварийный люк» из декларативной модели React в императивный мир (DOM API, сторонние библиотеки, мутации).


1. Два фундаментальных варианта использования (Use Cases)

ВариантНазначениеКлючевое отличие от State
1. Доступ к DOM-узлу / Экземпляру классаФокус, измерения (getBoundingClientRect), запуск анимаций, интеграция с не-React библиотеками (D3, Chart.js, Mapbox, Video/Audio API).ref.current указывает на конкретный экземпляр (HTMLElement или Instance).
2. Хранение мутируемого значения (Mutable Container)Значение, которое нужно читать/писать синхронно без перерисовки компонента (ID таймеров, предыдущие пропсы/стейт, флаги isMounted, счетчики рендеров).Мутация ref.current = newVal НЕ вызывает ре-рендер. Объект ref стабилен между рендерами (один и тот же объект).

2. API: useRef (Функциональные компоненты) vs createRef (Классовые)

useRef(initialValue) — Хук (Современный стандарт)

function TextInput() {
// 1. Создание рефа (один раз при монтировании)
const inputRef = useRef(null);
const renderCount = useRef(0); // Хранение мутируемого значения

// 2. Мутация БЕЗ ре-рендера
renderCount.current += 1;
console.log(`Render #${renderCount.current}`);

const focus = () => inputRef.current?.focus(); // Императивный вызов

// 3. Привязка к DOM (ref атрибут)
return <input ref={inputRef} type="text" />;
}

createRef() — Классовый API (Legacy)

class TextInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef(); // Создается в конструкторе
}
focus = () => this.inputRef.current?.focus();
render() {
return <input ref={this.inputRef} />;
}
}

> Важно: В функциональных компонентах нельзя использовать createRef внутри тела функции — он создаст новый реф на каждом рендере. Только useRef.


3. Виды Refs (Типы привязки)

ТипСинтаксисКогда использовать
Object Ref (Стандарт)ref={myRef}99% случаев. React сам кладет DOM-узел/инстанс в myRef.current.
Callback Refref={(node) => { this.node = node; }}Нужно выполнить логику в момент привязки/отвязки (измерения, регистрация в сторонней либе). Вызывается 2 раза: при монтировании (node) и анмаунте (null).
Function Ref (Setter)ref={setRef}Паттерн для передачи рефа дальше (Forwarding) или объединения нескольких рефов (mergeRefs).

Callback Ref нюанс (Частая ловушка):

// Если определять стрелочную функцию инлайн — она новая каждый рендер!
// React вызовет старый callback с null, потом новый с node -> лишние операции.
<div ref={(node) => this.node = node} /> // ПЛОХО (если не за мемоизировано)

// ХОРОШО: useCallback или метод класса
const setRef = useCallback((node) => { this.node = node; }, []);
<div ref={setRef} />

4. forwardRef — Проброс рефа в дочерний компонент (Polymorphic Components)

По умолчанию функциональные компоненты не могут принимать ref как пропс (ошибка: Function components cannot be given refs).

// Child: Обертка над input, экспонирующая фокус родителю
const FancyInput = forwardRef((props, ref) => {
// ref здесь — это реф, переданный родителем
return <input {...props} ref={ref} />;
});

// Parent
const Parent = () => {
const inputRef = useRef(null);
return (
<>
<FancyInput ref={inputRef} /> {/* Реф провалился внутрь к нативному input */}
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
});

useImperativeHandle — Кастомизация публичного API рефа (Инкапсуляция) Скрывает внутреннюю реализацию (DOM-узел), выставляя только нужные методы.

const Modal = forwardRef((props, ref) => {
const dialogRef = useRef(null);

// Родитель получит в ref.current ТОЛЬКО { open, close }, а не HTMLDialogElement
useImperativeHandle(ref, () => ({
open: () => dialogRef.current?.showModal(),
close: () => dialogRef.current?.close(),
// getInternal: () => dialogRef.current // Можно дать доступ, но лучше не надо
}), []);

return <dialog ref={dialogRef}>{props.children}</dialog>;
});

// Использование
const modalRef = useRef(null);
<Modal ref={modalRef}>Content</Modal>
<button onClick={() => modalRef.current.open()}>Open</button>

5. Паттерны и Best Practices (Senior Level)

А. mergeRefs — Объединение нескольких рефов Частая задача: внутренний реф компонента + внешний реф от родителя.

function mergeRefs(...refs) {
return (node) => {
refs.forEach(ref => {
if (typeof ref === 'function') ref(node);
else if (ref) ref.current = node;
});
};
}

// Использование
const InternalInput = forwardRef((props, ref) => {
const localRef = useRef(null);
return <input {...props} ref={mergeRefs(localRef, ref)} />;
});

Б. useRef vs useState для предыдущих значений (usePrevious)

function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; }, [value]);
return ref.current; // Значение с ПРЕДЫДУЩЕГО рендера
}

Почему не useState? useState вызывает ре-рендер. ref — нет. Нам нужно значение во время рендера для сравнения.

В. Избегайте чтения ref.current во время рендера (Render Phase)

// ПЛОХО: Чтение рефа во время рендера -> side-effect в рендере, ломает Concurrent Mode
function Component() {
const ref = useRef(0);
ref.current++; // Мутация в рендере!
return <div>{ref.current}</div>;
}

// ХОРОШО: Мутации в useLayoutEffect / useEffect / Event Handlers
function Component() {
const ref = useRef(0);
const handleClick = () => { ref.current++; console.log(ref.current); };
return <button onClick={handleClick}>{ref.current}</button>; // UI не обновится, но логика сработает
}

Г. null vs undefined инициализация

  • useRef(null) — стандарт для DOM-рефов (пока не привязан).
  • useRef(0) / useRef({}) — для мутируемых значений.
  • Проверка: ref.current?.focus() (Optional Chaining обязателен).

Д. Рефы и Strict Mode (React 18+ Dev) В разработке React дважды монтирует/размонтирует компоненты.

  • Callback Ref вызовется: node -> null -> node.
  • Object Ref: current = node -> current = null -> current = node.
  • Это нормально, проверяет чистоту cleanup.

6. Типичные ошибки (Anti-patterns)

  1. Использование рефа там, где нужен стейт/пропс.
    • Пример: ref.current = newValue и ожидание ре-рендера. Не сработает.
    • Фикс: useState / useReducer.
  2. Проброс рефа на кастомный компонент без forwardRef.
    • Ошибка: Warning: Function components cannot be given refs.
  3. Мутация ref.current в теле компонента (Render Phase).
    • Ломает Time Slicing, Suspense, Strict Mode. Только в useEffect/useLayoutEffect/хендлерах.
  4. Измерение DOM в useEffect (после Paint) вместо useLayoutEffect.
    • Вызывает визуальное мерцание (Layout Shift). useLayoutEffect — синхронно до Paint.
  5. Хранение в рефе объектов, которые должны быть стабильны для useEffect deps.
    • const objRef = useRef({ a: 1 }); — объект мутируем, но ссылка стабильна. Если кладете в deps useEffect(() => {}, [objRef]) — эффект не перезапустится при мутации. Это фича, но часто баг. Лучше класть в deps примитивы или использовать useMemo для объекта.

7. Рефы и TypeScript (Типизация)

// 1. DOM Ref (нативный элемент)
const inputRef = useRef<HTMLInputElement>(null); // null | HTMLInputElement
inputRef.current?.focus(); // OK

// 2. Component Ref (экземпляр классового компонента или forwardRef API)
interface ModalHandle { open: () => void; close: () => void; }
const modalRef = useRef<ModalHandle>(null);
modalRef.current?.open(); // OK

// 3. ForwardRef с типизацией пропсов и рефа
type Props = { label: string };
const FancyInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
return <input ref={ref} {...props} />;
});

// 4. Callback Ref типизация
const setRef = useCallback((node: HTMLDivElement | null) => {
// node типизирован
}, []);

Резюме (Cheatsheet)

> Ref — это контейнер { current: any }, который: > 1. Живет столько же, сколько компонент (стабильная ссылка между рендерами). > 2. Мутация .current НЕ вызывает ре-рендер (в отличие от useState). > 3. Дает императивный доступ: ref.current.focus(), ref.current.getBoundingClientRect(). > 4. API: useRef (функции), createRef (классы), forwardRef (проброс), useImperativeHandle (API поверхность). > 5. Правило: Читать/писать .current только в Event Handlers, useLayoutEffect, useEffect. Никогда не в теле компонента (Render Phase). > 6. Callback Ref — для сложной логики привязки/отвязки (требует useCallback для стабильности).

Вопрос 24. В чем разница между управляемыми (controlled) и неуправляемыми (uncontrolled) компонентами?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:14:07"/>

Оценка ответа кандидата: неполный. Кандидат начал говорить про управляемый компонент («у меня есть красные ячейки»), но не смог объяснить суть: управляемые — состояние в React (value/onChange), неуправляемые — состояние в DOM (ref/defaultValue).


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

Разница кроется в владении состоянием (Source of Truth) и ответственности за обновление UI.

ХарактеристикаУправляемые (Controlled)Неуправляемые (Uncontrolled)
Где хранится состояниеReact State (useState, useReducer).DOM (внутреннее состояние браузера).
Кто «хозяин» значенияReact. Компонент получает значение через проп value и уведомляет об изменении через onChange.Браузер. React читает значение по требованию через ref.current.value.
Цикл данныхvalue → UI → onChangesetState → Новый value → UI.Пользователь вводит → DOM обновляется → React читает ref.current.value когда нужно.
Ключевые пропсыvalue, checked, onChange, onSelect.defaultValue, defaultChecked, ref.
Валидация/Маска вводомПростая и мгновенная (перехват onChange, изменение стейта).Сложная/Отложенная (только после ввода, через ref или сабмит формы).
Программный контрольПолный (сброс, принудительное изменение, блокировка ввода).Ограничен (только через ref.current.value = ... — императивно).

1. Управляемые компоненты (Controlled Components) — Стандарт React

Принцип: «Единственный источник правды — стейт React». UI всегда синхронизирован со стейтом.

function ControlledInput() {
const [value, setValue] = useState('');

// 1. value ЯВНО задает значение инпута
// 2. onChange перехватывает ВСЕ изменения (ввод, вставка, автозаполнение)
const handleChange = (e) => {
const newValue = e.target.value;
// Можно трансформировать/валидировать ПЕРЕД обновлением стейта
setValue(newValue.toUpperCase()); // Пример: принудительный Uppercase
};

return (
<input
type="text"
value={value} // ОБЯЗАТЕЛЬНО (иначе станет uncontrolled)
onChange={handleChange}
/>
);
}

Когда ОБЯЗАТЕЛЬНЫ:

  • Нужна мгновенная валидация (маска телефона, ограничение длины, запрет спецсимволов).
  • Нужно программно управлять значением (сброс формы, подстановка из автокомплита, отмена изменений).
  • Значение используется в других местах UI сразу при вводе (превью, счетчик символов, кнопка сабмита disabled={!value}).
  • Доступность (a11y) — скринридеры лучше работают с controlled.

Ловушка: Если передать value={undefined} или value={null} — компонент становится uncontrolled (React выдаст предупреждение: A component is changing an uncontrolled input to be controlled). Всегда инициализируйте стейт: useState('').


2. Неуправляемые компоненты (Uncontrolled Components) — «Нативный» подход

Принцип: Состояние живет в DOM. React не التدخلляет в каждый нажатие клавиши.

function UncontrolledForm() {
const inputRef = useRef(null);

const handleSubmit = (e) => {
e.preventDefault();
// Читаем значение ТОЛЬКО в момент сабмита (Pull-based)
const value = inputRef.current.value;
console.log('Submitted:', value);
};

const handleReset = () => {
// Императивный сброс через DOM API
inputRef.current.value = '';
};

return (
<form onSubmit={handleSubmit}>
<input
type="text"
ref={inputRef}
defaultValue="Initial" // Только начальная установка
/>
<button type="submit">Submit</button>
<button type="button" onClick={handleReset}>Reset</button>
</form>
);
}

Когда уместны:

  • Простые формы (логини, поиск) без сложной валидации «на лету».
  • Интеграция с не-React кодом (jQuery плагины, старые библиотеки).
  • Файловые инпуты (<input type="file" />) — они только uncontrolled (value read-only из соображений безопасности).
  • Производительность на очень больших формах (избегаем ре-рендера на каждый keystroke) — хотя useTransition / useDeferredValue в React 18+ решают это для controlled.

3. Гибридный подход и defaultValue vs value

// ОШИБКА: Смешивание (React ругается в консоли)
<input value={state} defaultValue="initial" /> // Не делайте так

// ПРАВИЛЬНО: Переключение режима через key (полный ресет)
<input key={mode} value={state} /> // Смена key -> новый инстанс -> uncontrolled -> controlled

defaultValue / defaultChecked — используются только для инициализации uncontrolled компонентов. React игнорирует их обновления после монтирования.


4. Сравнение на практике: Валидация телефона

Controlled (Декларативно, предсказуемо):

const [phone, setPhone] = useState('');

const formatPhone = (val) => val.replace(/\D/g, '').slice(0, 11);

const onChange = (e) => {
setPhone(formatPhone(e.target.value)); // Стейт всегда чистый
};

return <input value={phone} onChange={onChange} />;

Uncontrolled (Императивно, больно):

const inputRef = useRef(null);

// Нужно слушать нативное событие input на DOM узле (useEffect)
useEffect(() => {
const el = inputRef.current;
const handler = () => {
el.value = formatPhone(el.value); // Прямая мутация DOM
};
el.addEventListener('input', handler);
return () => el.removeEventListener('input', handler);
}, []);

return <input ref={inputRef} defaultValue="+7" />;

> Вывод: Controlled выигрывает в DX и надежности для 95% кейсов.


5. Нюансы для Senior / Tech Lead

А. onChange vs onInput

  • React нормализует onChange — оно срабатывает мгновенно на любое изменение (ввод, паста, автозаполнение, речь), в отличие от нативного onchange (срабатывает на потерю фокуса). Используйте onChange.

Б. Производительность Controlled Inputs

  • Проблема: Рендер всего дерева на каждый keystroke.
  • Решения:
    1. Локализация стейта — выносить инпут в отдельный компонент MemoizedInput.
    2. useDeferredValue (React 18) — откладывает обновление «тяжелой» части UI, инпут остается быстрым.
    3. useTransition — помечает обновление стейта как низкоприоритетное.

В. Form Libraries (React Hook Form, Formik, TanStack Form)

  • Они используют Uncontrolled подход под капотом (через ref и Controller/useController для controlled-интерфейса).
  • Это дает производительность uncontrolled (нет ре-рендеров на ввод) + API controlled (валидация, маски, watch, setValue).
  • Рекомендация: Для форм > 3-5 полей — всегда библиотека (RHF стандарт де-факто).

Г. Доступность (a11y)

  • Controlled инпуты лучше работают с VoiceOver/NVDA при программном изменении значения (анонс изменений).
  • Uncontrolled могут не анонсировать изменение, сделанное через ref.current.value = 'x'.

Д. TypeScript: Value vs DefaultValue

// Controlled
interface Props { value: string; onChange: (v: string) => void; }

// Uncontrolled
interface Props { defaultValue?: string; onChange?: (v: string) => void; } // value отсутствует

Резюме (Decision Matrix)

СитуацияВыбор
Нужна маска ввода / форматирование / мгновенная валидацияControlled
Значение нужно где-то еще в UI прямо сейчасControlled
Программный сброс / установка значения (кнопка «Очистить»)Controlled
<input type="file" />Только Uncontrolled
Очень большая форма (100+ полей), лаги при вводеUncontrolled (через RHF / TanStack Form)
Простая форма логина / поиск, без валидации на летуUncontrolled (проще код)
Интеграция с легаси / jQuery / не-React виджетыUncontrolled

> Best Practice 2024: Пишите Controlled компоненты для примитивов (Input, Select, Checkbox). Для форм используйте React Hook Form (который дает Controlled API, но работает как Uncontrolled). Избегайте ручного управления ref в бизнес-логике, если нет веской причины.

Вопрос 25. Что такое функции высшего порядка (Higher-Order Functions) в JavaScript?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:14:34"/>

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


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

Функция высшего порядка (Higher-Order Function / HOF) — это функция, которая удовлетворяет хотя бы одному из двух условий:

  1. Принимает одну или более функций в качестве аргументов (колбэки).
  2. Возвращает функцию как результат своей работы.

Это возможно благодаря тому, что в JavaScript функции являются объектами первого класса (First-Class Citizens): их можно присваивать переменным, передавать как аргументы и возвращать из других функций.


1. Тип 1: Функции, принимающие функции (Callbacks / Consumers)

Самый частый паттерн. Позволяют абстрагировать алгоритм обхода/обработки, оставляя бизнес-логику вызывающему коду.

Примеры из стандартной библиотеки (Array Methods):

const users = [
{ name: 'Alice', age: 25, active: true },
{ name: 'Bob', age: 30, active: false },
{ name: 'Charlie', age: 35, active: true }
];

// map: Трансформация каждого элемента
const names = users.map(user => user.name); // ['Alice', 'Bob', 'Charlie']

// filter: Отбор по предикату (функция -> boolean)
const activeUsers = users.filter(user => user.active);

// reduce: Агрегация (аккумулятор + текущий элемент)
const totalAge = users.reduce((sum, user) => sum + user.age, 0); // 90

// find / some / every / forEach / sort — все HOF

Собственная HOF (Паттерн «Стратегия» / «Callback»):

// Общая логика: запрос + обработка ошибок + лоадинг
async function fetchWithRetry(url, options = {}, retries = 3) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (retries > 0) {
await new Promise(r => setTimeout(r, 1000)); // Backoff
return fetchWithRetry(url, options, retries - 1); // Рекурсивный вызов
}
throw error; // Пробрасываем наверх
}
}

// Использование: передаем логику "что делать с данными" как колбэк
function loadUserProfile(userId, onSuccess, onError) {
fetchWithRetry(`/api/users/${userId}`)
.then(data => onSuccess(data)) // Колбэк успеха
.catch(err => onError(err)); // Колбэк ошибки
}

2. Тип 2: Функции, возвращающие функции (Factories / Closures)

Позволяют создавать специализированные функции «на лету», запоминая конфигурацию в замыкании (Closure).

Фабрика функций (Function Factory):

// HOF: Принимает конфиг, возвращает готовую функцию-валидатор
function createValidator(rules) {
// Замыкание: 'rules' запоминается внутри возвращаемой функции
return function(data) {
const errors = {};
for (const [field, rule] of Object.entries(rules)) {
if (!rule(data[field])) {
errors[field] = `Validation failed for ${field}`;
}
}
return Object.keys(errors).length ? errors : null;
};
}

// Создаем специфические валидаторы ОДИН РАЗ (при инициализации приложения)
const validateUser = createValidator({
name: v => typeof v === 'string' && v.length > 2,
email: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
age: v => Number.isInteger(v) && v >= 18
});

// Используем МНОГОКРАТНО без накладных расходов на парсинг правил
const userData = { name: 'Al', email: 'bad', age: 10 };
const errors = validateUser(userData);
// { name: '...', email: '...', age: '...' }

Каррирование (Currying) — частный случай возврата функции:

// Обычная функция
function add(a, b) { return a + b; }

// Каррированная (HOF)
const curriedAdd = a => b => a + b;

const add5 = curriedAdd(5); // add5 — это функция (b => 5 + b)
console.log(add5(10)); // 15
console.log(add5(20)); // 25

// Практический пример: Логгер с префиксом
const createLogger = (prefix) => (level) => (message) => {
console.log(`[${prefix}] [${level}] ${message}`);
};

const apiLogger = createLogger('API');
const apiError')(API);
apiLogger('ERROR')('Connection failed'); // [API] [ERROR] Connection failed

3. Тип 3: Комбинация (Принимает И возвращает) — Higher-Order Components / Decorators

Мощнейший паттерн для композиции поведения (Cross-cutting concerns: логирование, мемоизация, авторизация, ретраи).

// HOF: Принимает функцию, возвращает обертку с мемоизацией
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args); // Упрощенно
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}

// HOF: Логирование вызовов
function withLogging(fn, name = fn.name) {
return function(...args) {
console.time(name);
try {
const result = fn.apply(this, args);
return result;
} finally {
console.timeEnd(name);
}
};
}

// Композиция HOF (снизу вверх)
const heavyCalculation = (n) => {
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += n; // Тяжелая операция
return sum;
};

const optimizedCalc = memoize(withLogging(heavyCalculation, 'HeavyCalc'));

optimizedCalc(5); // Лог + Вычисление
optimizedCalc(5); // Лог + Взято из кэша (мгновенно)

4. HOF в React (Паттерны, которые вы используете ежедневно)

ПаттернЧто этоПример
Custom HooksHOF, возвращающая хук (функцию) с логикой.const useAuth = () => { ... return { user, login } }
HOC (Higher-Order Components)Функция: Component -> EnhancedComponent. (Legacy, но важно знать).withRouter(Component), connect(mapState)(Component)
Render Props / Children as FunctionПропс-функция: () => ReactNode.<DataProvider render={data => <UI data={data} />} />
Event HandlersonClick={() => handleClick(id)} — стрелочная функция создает замыкание над id.<button onClick={e => onDelete(e, item.id)} />
Array Methods в JSX.map(), .filter() прямо в рендере.{items.filter(active).map(renderItem)}

5. Зачем это нужно? (Бизнес-ценность для Senior/Lead)

  1. DRY (Don't Repeat Yourself) — Выносим общую инфраструктуру (ретраи, логирование, кэш, обработку ошибок) в HOF. Бизнес-логика остается чистой.
  2. Composition over Inheritance — Комбинируем поведение (memoize(withLogging(fn))) вместо создания иерархий классов.
  3. Abstraction Level — Скрываем «как» (итерация, запрос, кэш), показываем «что» (предикат, трансформация).
  4. Lazy Evaluation / Partial Application — Готовим функцию заранее (при старте), выполняем быстро (в хот-пути).
  5. Testability — Легко мокать/подменять колбэки. Чистые функции (редюсеры, валидаторы) тестируются без моков.

6. Подводные камни (Performance & Debugging)

ПроблемаОбъяснениеРешение
Создание функций в рендереonClick={() => doSmth(id)} создает новую функцию каждый рендер -> ломает React.memo у детей.useCallback / useMemo / вынос компонента / bind в конструкторе (классы).
Потеря стека вызовов (Stack Traces)Глубокая вложенность HOF (a(b(c(d())))) делает стеки нечитаемыми.Именованные функции (function name() {} вместо () => {}), displayName для HOC.
Замыкания над устаревшими пропсами/стейтом (Stale Closures)Возвращенная функция «запомнила» старые значения.useRef для актуальных значений / правильные deps в useCallback / useEffect.
Глубокое клонирование аргументов в memoizeJSON.stringify медленный, не работает с циклами/функциями.Использовать WeakMap / Map с примитивными ключами / библиотеки (lodash.memoize, fast-memoize).

7. TypeScript: Типизация HOF (Generics + Infer)

// 1. Функция, принимающая функцию (Callback)
function withRetry<TArgs extends unknown[], TReturn>(
fn: (...args: TArgs) => Promise<TReturn>,
retries = 3
): (...args: TArgs) => Promise<TReturn> {
return async (...args) => {
try {
return await fn(...args);
} catch (e) {
if (retries <= 0) throw e;
await new Promise(r => setTimeout(r, 1000));
return withRetry(fn, retries - 1)(...args);
}
};
}

// 2. Фабрика (Возвращает функцию)
function createMapper<T, U>(transform: (item: T) => U): (items: T[]) => U[] {
return (items) => items.map(transform);
}

// Использование: TS выводит типы автоматически
const toUpper = createMapper((s: string) => s.toUpperCase());
// toUpper: (items: string[]) => string[]

// 3. Композиция (Pipe / Flow)
type Fn<T, R> = (arg: T) => R;
function pipe<A, B, C>(f1: Fn<A, B>, f2: Fn<B, C>): Fn<A, C> {
return (a) => f2(f1(a));
}

Резюме (Cheatsheet для собеседования)

> Higher-Order Function (HOF) — функция, оперирующая другими функциями: > 1. Принимает функцию как аргумент (map, filter, fetchWithRetry(fn)). > 2. Возвращает функцию (createValidator, memoize, curriedAdd). > 3. И то, и другое (Декораторы, Композиция: pipe, compose). > > Основа: First-Class Functions + Closures (Замыкания). > > В React: Хуки, HOC, Render Props, колбэки в пропсах, .map() в JSX. > > Senior Mindset: HOF — главный инструмент абстракции поведения и композиции. Позволяют отделить «механику» (циклы, запросы, кэш, ретраи) от «политики» (бизнес-логика, валидация, UI). Пишите маленькие чистые функции, комбинируйте их через HOF. Избегайте анонимных стрелочных функций в хот-путях рендера без мемоизации.

Вопрос 26. Можете объяснить методы массивов map, filter и reduce?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:14:52"/>

Оценка ответа кандидата: неполный. Кандидат описал map (итерация/трансформация) и filter (фильтрация по условию, пример с возрастом), но полностью опустил reduce (свертка к одному значению).


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

map, filter, reduce — три фундаментальных метода функционального программирования в JavaScript для декларативной обработки коллекций. Все они:

  • Не мутируют исходный массив (иммутабельность).
  • Возвращают новый массив (кроме reduce, который возвращает накопленное значение).
  • Принимают callback-функцию (функцию высшего порядка), которая вызывается для каждого элемента.

Сигнатура колбэка единая для всех: (element, index, array) => { ... }.


1. map — Трансформация (1-в-1)

Назначение: Создать новый массив той же длины, где каждый элемент — результат вызова колбэка для соответствующего элемента исходного.

const numbers = [1, 2, 3, 4];

// Хорошо: чистая трансформация
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8]

// Работа с объектами (важно: возвращать НОВЫЙ объект, не мутировать старый!)
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
const userNames = users.map(user => user.name);
// ['Alice', 'Bob']

// Антипаттерн: использование map для сайд-эффектов (console.log, push в другой массив)
// ПЛОХО: numbers.map(n => console.log(n)); // Используйте forEach

Ключевой момент: Длина массива не меняется. undefined возвращается, если колбэк ничего не вернул явно.


2. filter — Отбор (N-в-M, где M <= N)

Назначение: Создать новый массив только с теми элементами, для которых колбэк вернул truthy (истинное) значение.

const numbers = [1, 2, 3, 4, 5, 6];

// Оставляем только четные
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4, 6]

// Сложные условия
const activeAdults = users.filter(u => u.active && u.age >= 18);

// Важно: filter НЕ меняет индексы в исходном массиве, но НОВЫЙ массив имеет свои индексы (0, 1, 2...)
// Если нужен индекс в исходном массиве — используйте второй аргумент колбэка
const indexed = numbers.filter((n, i) => i > 2); // [4, 5, 6]

Ключевой момент: Длина массива уменьшается или остается равной. Порядок элементов сохраняется.


3. reduce — Свертка / Агрегация (N-в-1)

Назначение: «Свернуть» массив к одному значению (числа, строки, объекта, массива, промиса). Это самый мощный метод, через который можно выразить map, filter и любую итеративную логику.

Сигнатура:

array.reduce((accumulator, currentValue, index, array) => {
// ... логика обновления аккумулятора
return newAccumulator;
}, initialValue); // initialValue — ОБЯЗАТЕЛЬНО рекомендуется

Параметры:

  • accumulator (acc) — накопленное значение (результат предыдущего вызова колбэка или initialValue).
  • currentValue — текущий элемент.
  • initialValue — стартовое значение аккумулятора. Всегда передавайте его, иначе на первой итерации acc будет первым элементом массива, а currentValue — вторым (исключение: пустой массив без initialValue кидает TypeError).

Примеры reduce:

А. Сумма чисел (Классика):

const sum = [1, 2, 3, 4].reduce((acc, n) => acc + n, 0); // 10

Б. Подсчет вхождений (Map/Object):

const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];

const count = fruits.reduce((acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
}, {});

// { apple: 3, banana: 2, orange: 1 }

В. Группировка массива объектов по полю (GroupBy):

const users = [
{ id: 1, role: 'admin' },
{ id: 2, role: 'user' },
{ id: 3, role: 'admin' }
];

const grouped = users.reduce((acc, user) => {
const key = user.role;
if (!acc[key]) acc[key] = [];
acc[key].push(user);
return acc;
}, {});

/*
{
admin: [{ id: 1, role: 'admin' }, { id: 3, role: 'admin' }],
user: [{ id: 2, role: 'user' }]
}
*/

Г. flatMap через reduce (Плоское отображение):

const sentences = ['Hello world', 'JS is cool'];

const words = sentences.reduce((acc, sentence) => {
return acc.concat(sentence.split(' '));
}, []);

// ['Hello', 'world', 'JS', 'is', 'cool']
// Аналог: sentences.flatMap(s => s.split(' '))

Д. Последовательное выполнение промисов (Promise Chain):

const urls = ['/api/1', '/api/2', '/api/3'];

const results = await urls.reduce(async (promiseChain, url) => {
const resultsSoFar = await promiseChain;
const response = await fetch(url);
const data = await response.json();
return [...resultsSoFar, data];
}, Promise.resolve([])); // Начальное значение: резолвщийся промис с пустым массивом

Е. Композиция функций (Pipe):

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

const add2 = x => x + 2;
const mul3 = x => x * 3;
const toStr = x => `Result: ${x}`;

const compute = pipe(add2, mul3, toStr);
console.log(compute(5)); // "Result: 21" -> (5+2)*3 = 21

4. Сравнительная таблица и выбор метода

ЗадачаМетодВозвращаетМутирует исходный?
Преобразовать каждый элементmapНовый массив (та же длина)Нет
Отобрать подмножествоfilterНовый массив (длина <=)Нет
Вычислить одно значение / агрегатreduceЛюбое значение (число, обьект, массив)Нет
Найти один элементfindЭлемент / undefinedНет
Проверить условие (все/хотя бы один)every / somebooleanНет
Просто перебрать (сайд-эффекты)forEachundefinedНет (но колбэк может)

5. Комбинирование (Chaining) — Best Practices

Чейнинг (map -> filter -> reduce) читается слева направо, как конвейер.

// Задача: Получить сумму зарплат активных сотрудников старше 30
const totalSalary = employees
.filter(e => e.active && e.age > 30) // 1. Отбор (уменьшаем выборку)
.map(e => e.salary) // 2. Проекция (берем только нужное поле)
.reduce((sum, sal) => sum + sal, 0); // 3. Агрегация

Производительность (Senior Note):

  • Чейнинг создает промежуточные массивы (filter создает массив, map создает еще один).
  • Для маленьких/средних массивов (< 10k элементов) — невидимо, читаемость важнее.
  • Для больших данных / хот-путей — используйте один reduce или for..of цикл (один проход, ноль аллокаций памяти под промежуточные массивы).
// Оптимизированный вариант (один проход)
const totalSalaryOptimized = employees.reduce((sum, e) => {
if (e.active && e.age > 30) return sum + e.salary;
return sum;
}, 0);

6. Типичные ошибки на собеседовании

  1. Мутация в map/filter:
    // ПЛОХО: Мутируем исходный объект!
    users.map(u => { u.fullName = u.name + ' ' + u.surname; return u; });

    // ХОРОШО: Возвращаем новый объект (spread / Object.assign)
    users.map(u => ({ ...u, fullName: `${u.name} ${u.surname}` }));
  2. Забытый return в reduce:
    // ПЛОХО: acc становится undefined после первой итерации
    [1,2].reduce((acc, n) => { acc + n; }, 0);

    // ХОРОШО
    [1,2].reduce((acc, n) => acc + n, 0);
  3. Отсутствие initialValue в reduce:
    • На пустом массиве: TypeError: Reduce of empty array with no initial value.
    • На массиве объектов без initialValue: acc становится первым объектом, ломая логику (например, acc + n -> [object Object]1).
  4. Использование map вместо forEach для сайд-эффектов (рендер, логи, запросы). map сигнализирует о трансформации данных, forEach — об итерации ради эффекта.

Резюме (One-Liners для запоминания)

> map: «Дай мне новый массив, где каждый элемент превращен по правилу». > filter: «Дай мне новый массив, где остались только подходящие». > reduce: «Сверни массив в одно значение, накапливая результат шаг за шагом». > > Правило: Если можешь сделать через map/filter — делай (читаемость). Если нужна сложная агрегация, один проход или нестандартная структура результата — reduce. Всегда передавай initialValue в reduce. Никогда не мутируй исходные данные.

Вопрос 27. В чем разница между состоянием (state) и свойствами (props) в React?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:15:24"/>

Оценка ответа кандидата: неполный. Кандидат верно описал props как данные, передаваемые от родителя к ребенку. Про state сказал только «внешний интерфейс» и не раскрыл суть: локальное изменяемое состояние компонента.


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

Фундаментальное различие кроется в владении данными (Ownership) и мутабельности.

ХарактеристикаProps (Свойства)State (Состояние)
Кто владеет даннымиРодительский компонент (или потребитель).Сам компонент (локально).
МутабельностьImmutable (Только для чтения). Компонент-получатель не может изменить props.Mutable (Изменяемо). Компонент управляет своим state через setState / dispatch / setter.
НазначениеКонфигурация и данные для рендера (входные параметры функции).Внутренние данные, которые меняются во времени под действием событий.
АналогияАргументы функции (function Component(props)).Локальные переменные функции, сохраняющие значение между вызовами (замыкание / useRef + рендер).
Триггер ре-рендераИзменение props в родителе.Вызов сеттера (setState, dispatch) в самом компоненте.

1. Props — «Входные параметры» (Data Down)

Props — это механизм однонаправленного потока данных (Unidirectional Data Flow). Они делают компоненты композируемыми и переиспользуемыми.

// Parent.jsx
function Parent() {
const userName = 'Alice';
// Передаем данные ВНИЗ
return <Child name={userName} age={25} onAction={handleClick} />;
}

// Child.jsx — Pure Component (идеально)
function Child({ name, age, onAction }) {
// props — ТОЛЬКО ЧТЕНИЕ
// name = 'Bob' // ОШИБКА! TypeError: Assignment to read-only property (в строгом режиме / dev)

return (
<div>
<h1>{name}, {age} years old</h1>
<button onClick={onAction}>Click</button>
</div>
);
}

Правила Props:

  1. Read-only: Никогда не мутируйте props (props.items.push() — запрещено).
  2. Serialization: В идеале props должны быть сериализуемы (примитивы, объекты, массивы, функции-колбэки) для работы DevTools, SSR, RSC.
  3. Default Props / Destructuring: Используйте деструктуризацию с дефолтами: function Comp({ title = 'Default', count = 0 }).

2. State — «Локальная память» (State Up / Internal)

State — это приватные данные компонента, которые могут меняться во времени. React гарантирует, что при изменении state компонент перерисуется (re-render).

function Counter() {
// Инициализация (только при первом рендере)
const [count, setCount] = useState(0);

// Асинхронное обновление (батчинг в React 18+)
const increment = () => {
setCount(c => c + 1); // Функциональное обновление (безопасно для замыканий)
// setCount(count + 1) — риск stale closure в асинхронных колбэках
};

return <button onClick={increment}>{count}</button>;
}

Правила State:

  1. Локальность: State инкапсулирован внутри компонента. Родитель не имеет прямого доступа к count и setCount (если не проброшен через props).
  2. Иммутабельность обновления: Не мутируйте state напрямую (state.items.push() — не вызовет рендер). Всегда создавайте новые ссылки: setItems([...items, newItem]).
  3. Минимизация: Храните в state только то, что нельзя вычислить из props или других state (derived state — антипаттерн).
  4. Колоизация (Colocation): Держите state максимально близко к месту использования (не поднимайте выше, чем нужно).

3. Взаимосвязь: «Lifting State Up» (Подъем состояния)

Когда несколько компонентов нуждаются в одних данных — state поднимается к ближайшему общему предку (Common Ancestor). Предок становится «Source of Truth» (Источником правды), а дети получают данные через props и колбэки для изменения.

function Parent() {
// State живет ЗДЕСЬ (Single Source of Truth)
const [name, setName] = useState('');

return (
<>
{/* Child1 читает (Props) */}
<DisplayName name={name} />
{/* Child1 меняет через колбэк (Props) */}
<InputName value={name} onChange={setName} />
</>
);
}

function DisplayName({ name }) {
return <h1>Hello, {name || 'Stranger'}</h1>; // Только Props
}

function InputName({ value, onChange }) {
return <input value={value} onChange={e => onChange(e.target.value)} />; // Controlled Component
}

> Паттерн: Data flows Down (Props), Events flow Up (Callbacks -> State Setters).


4. Derived State (Вычисляемое состояние) — Важный Senior-паттерн

Не дублируйте в state то, что можно вычислить во время рендера.

// ПЛОХО: Два источника правды, рассинхронизация
function Bad({ items, filter }) {
const [filtered, setFiltered] = useState([]);
useEffect(() => setFiltered(items.filter(filter)), [items, filter]); // Лишний рендер + сложность
return <List items={filtered} />;
}

// ХОРОШО: Чистая функция рендера, нет лишнего state
function Good({ items, filter }) {
const filtered = useMemo(() => items.filter(filter), [items, filter]); // Мемоизация если тяжело
// или просто: const filtered = items.filter(filter); // Если дешево
return <List items={filtered} />;
}

5. Типизация (TypeScript)

// Props — интерфейс контракта (Readonly рекомендуется)
interface UserCardProps {
name: string;
age: number;
onDelete: (id: string) => void; // Колбэк в пропсах
isActive?: boolean; // Опционально
}

// State — внутренняя реализация
function UserCard({ name, age, onDelete, isActive = false }: UserCardProps) {
const [isEditing, setIsEditing] = useState<boolean>(false); // Локальный UI state
const [localDraft, setLocalDraft] = useState<string>(name); // Временный state формы

// ...
}

6. Чек-лист для принятия решения: Props vs State

ВопросОтвет → Решение
Приходят ли данные от родителя?Да → Props
Меняются ли данные со временем из-за пользовательских действий внутри компонента?Да → State (или useRef, если не нужен ре-рендер)
Можно ли вычислить данные из props/state прямо в рендере?Да → Derived (вычисляемое), не State.
Нужны ли данные брату/сестре/другой ветке дерева?Да → Lift State Up (подними в родителя → Props для детей).
Нужно ли избежать ре-рендера при изменении?Да → useRef / useMemo, не State.

Резюме (Senior Cheatsheet)

> Props — это API компонента (Контракт). Входные, неизменяемые, приходят извне. Делают компонент «глупым» (Presentational) и тестируемым. > > State — это Внутренняя реализация (Память). Локальные, изменяемые, управляются компонентом. Делают компонент «умным» (Container/Interactive). > > Принцип: Максимизируй Props (чистые функции), минимизируй State (сложность). Поднимай State только там, где это необходимо для синхронизации. Вычисляй всё остальное «на лету» (Derived State).

Вопрос 28. Что такое маршрутизатор (Router) в React и как он работает?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:15:59"/>

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


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

Маршрутизатор (Router) — это библиотека (чаще всего React Router v6+), реализующая клиентский роутинг (Client-Side Routing / SPA Routing). Он синхронизирует URL браузера с UI React-приложения без полной перезагрузки страницы (Hard Reload).


1. Ключевая проблема: SPA vs MPA

  • MPA (Multi-Page App): Клик по ссылке → HTTP запрос → Сервер отдает новый HTML → Браузер перерисовывает всё (мигание, потеря состояния JS).
  • SPA (Single Page App): Один index.html загружается один раз. Router перехватывает клики по ссылкам (<a>), меняет URL через History API и перерисовывает только нужную часть дерева компонентов.

2. Как это работает «под капотом» (Senior Level)

А. History API (Браузерный движок) Роутер использует два метода window.history:

// 1. Навигация без перезагрузки (добавляет запись в историю)
history.pushState(state, '', '/new-url');

// 2. Замена текущей записи (для редиректов)
history.replaceState(state, '', '/login');

// 3. Слушаем кнопки "Назад/Вперед" браузера
window.addEventListener('popstate', () => {
// Синхронизируем React State с window.location.pathname
});

Б. Контекст React (Провайдер) BrowserRouter (обертка приложения) создает React Context (NavigationContext, LocationContext), в который кладет:

  • Текущий location (pathname, search, hash, state).
  • navigator (объект с методами push, replace, go, back, forward).
  • basename (подпуть, если приложение не в корне домена).

В. Алгоритм матчинга (Matching Algorithm) При изменении location роутер:

  1. Берет дерево маршрутов (<Routes><Route path="..." />...</Routes>).
  2. Рекурсивно сравнивает сегменты URL с path маршрутов (используя path-to-regexp или свой парсер в v6).
  3. Находит наиболее конкретное совпадение (Longest Prefix Match).
  4. Формирует массив matches (данные о параметрах :id, данных loader/action, элементе компонента).
  5. Триггерит ре-рендер <Routes>, который рендерит иерархию <Outlet /> в соответствии с matches.

3. Основные сущности React Router v6+

Компонент / ХукНазначение
<BrowserRouter>История на базе History API (чистые URL: /users/1).
<HashRouter>История на базе window.location.hash (/#/users/1). Для статического хостинга без настройки сервера.
<Routes>Контейнер для <Route>. Выполняет матчинг одного лучшего маршрута (в отличие от v5 Switch).
<Route path="..." element={<Comp />} />Правило сопоставления. path поддерживает параметры (:id), сплеты (*), опциональные (?).
<Outlet />Ключевой элемент для вложенного роутинга. Рендерит дочерний match внутри родительского лейаута.
<Link to="/path" />Декларативная навигация. Рендерит <a>, но перехватывает onClicknavigator.push().
useNavigate()Императивная навигация (navigate('/path', { replace: true, state: {} })).
useParams()Доступ к параметрам URL (:id{ id: '123' }).
useSearchParams()Работа с Query String (?page=1&sort=asc) → [searchParams, setSearchParams].
useLocation()Текущий location объект (pathname, search, hash, state).
useMatches()Доступ ко всем совпавшим маршрутам (для хлебных крошек, лейаутов).

4. Паттерны и Best Practices (Senior/Tech Lead)

А. Вложенный роутинг (Layout Routes) — Основа архитектуры

// App.jsx
<Routes>
<Route path="/" element={<RootLayout />}> // Общий лейаут (Header, Footer)
<Route index element={<Home />} /> // /
<Route path="about" element={<About />} /> // /about
<Route path="dashboard" element={<DashboardLayout />}> // Вложенный лейаут
<Route index element={<DashboardHome />} /> // /dashboard
<Route path="settings" element={<Settings />} /> // /dashboard/settings
</Route>
</Route>
<Route path="login" element={<Login />} /> // Отдельный лейаут (без Header)
<Route path="*" element={<NotFound />} /> // 404 (Catch-all)
</Routes>

// RootLayout.jsx
export default function RootLayout() {
return (
<>
<Header />
<main><Outlet /></main> // <-- СЮДА рендерятся Home, About, DashboardLayout
<Footer />
</>
);
}

Б. Data Loading: Loaders & Actions (React Router v6.4+ / Remix Style) Перенос логики загрузки данных из useEffect в роутер.

// route config (createBrowserRouter)
export const router = createBrowserRouter([
{
path: "/users/:id",
element: <UserProfile />,
loader: async ({ params }) => { // Запускается ПЕРЕД рендером
const user = await fetchUser(params.id);
if (!user) throw new Response("", { status: 404 }); // Ошибка -> Error Boundary
return user; // Доступно в компоненте через useLoaderData()
},
action: async ({ request, params }) => { // Обработка POST/PUT/DELETE (формы)
const formData = await request.formData();
return updateUser(params.id, formData);
},
errorElement: <UserErrorFallback />, // UI для ошибок loader/action
}
]);

// UserProfile.jsx
export default function UserProfile() {
const user = useLoaderData(); // Типизированные данные, нет useEffect/loading state
const navigate = useNavigate();

return (
<Form method="post"> {/* Автоматически вызывает action */}
<input name="name" defaultValue={user.name} />
<button type="submit">Save</button>
</Form>
);
}

> Почему это круто: SSR-ready, кэширование, предзагрузка (<Link prefetch>), оптимистичное UI, разделение ответственности.

Г. Защищенные маршруты (Auth Guard) — Паттерн Wrapper

function PrivateRoute({ children }) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
return user ? <Outlet /> : <Navigate to="/login" replace state={{ from: location }} />;
}

// Использование
<Route element={<PrivateRoute />}>
<Route path="profile" element={<Profile />} />
<Route path="admin" element={<Admin />} />
</Route>

Д. Lazy Loading (Code Splitting) + Suspense

import { lazy, Suspense } from 'react';

const HeavyDashboard = lazy(() => import('./HeavyDashboard'));

<Route
path="dashboard"
element={
<Suspense fallback={<Spinner />}>
<HeavyDashboard />
</Suspense>
}
/>

Е. Типизация (TypeScript) — declare module 'react-router'

// types/react-router.d.ts
import 'react-router';

declare module 'react-router' {
interface LoaderData {
'/users/:id': User; // Тип данных для конкретного маршрута
}
interface ActionData {
'/users/:id': { success: boolean };
}
// Параметры URL
interface Params {
'/users/:id': { id: string };
}
}

// В компоненте -> полная типизация без generics
const user = useLoaderData(); // Тип: User
const params = useParams(); // Тип: { id: string }

5. Настройка Сервера (Production Must-Have)

Поскольку это SPA, сервер не знает про /users/1. При прямом заходе/рефреше он вернет 404. Решение: Настроить Fallback на index.html для всех не-ASSЕТ путей.

Nginx:

location / {
try_files $uri $uri/ /index.html; # Ключевая строка
}

Node/Express:

app.use(express.static('build'));
app.get('*', (req, res) => res.sendFile(path.resolve('build', 'index.html')));

6. Альтернативы и Конкуренты (2024)

БиблиотекаФишкаКогда выбирать
React Router v6Стандарт де-факто, Loaders/Actions, RSC support (v7).95% проектов.
TanStack RouterType-safe first, File-based routing, встроенный кэш/инвалидация (как React Query), DevTools.Новые проекты, если нужен максимальный DX и типизация.
WouterМинималистичный (1.5kb), хуки, нет контекста.Микро-фронтенды, встраиваемые виджеты, лендинги.
Next.js / Remix (App Router)Файловая система = Роутинг, RSC, Server Actions.Фуллстек React фреймворки (рекомендуется для новых стартов).

Резюме (Interview Cheatsheet)

> Router — это синхронизатор URL ↔ UI. > > Механика: History API (pushState/popstate) + React Context (Location/Navigator) + Алгоритм матчинга путей. > > Ключевые концепции v6+: > 1. <Outlet /> — вложенные лейауты (Composition > Inheritance). > 2. Loaders / Actions — загрузка и мутации данных в роутере, а не в useEffect (Single Source of Truth для данных страницы). > 3. <Link prefetch /> — предзагрузка данных/кода при наведении. > 4. Error Boundaries + errorElement — декларативная обработка ошибок загрузки. > > Senior Mindset: Не пишите логику загрузки в useEffect. Используйте Loaders для GET и Actions + <Form /> для POST. Настройте серверный Fallback (try_files). Типизируйте параметры и данные лоадеров через Module Augmentation. Рассмотрите TanStack Router для новых проектов ради типизации URL first-class.

Вопрос 28. Что такое маршрутизатор (Router) в React и как он работает?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:15:59"/>

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


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

Маршрутизатор (Router) — это библиотека (чаще всего React Router v6+), реализующая клиентский роутинг (Client-Side Routing / SPA Routing). Он синхронизирует URL браузера с UI React-приложения без полной перезагрузки страницы (Hard Reload).


1. Ключевая проблема: SPA vs MPA

  • MPA (Multi-Page App): Клик по ссылке → HTTP запрос → Сервер отдает новый HTML → Браузер перерисовывает всё (мигание, потеря состояния JS).
  • SPA (Single Page App): Один index.html загружается один раз. Router перехватывает клики по ссылкам (<a>), меняет URL через History API и перерисовывает только нужную часть дерева компонентов.

2. Как это работает «под капотом» (Senior Level)

А. History API (Браузерный движок) Роутер использует два метода window.history:

// 1. Навигация без перезагрузки (добавляет запись в историю)
history.pushState(state, '', '/new-url');

// 2. Замена текущей записи (для редиректов)
history.replaceState(state, '', '/login');

// 3. Слушаем кнопки "Назад/Вперед" браузера
window.addEventListener('popstate', () => {
// Синхронизируем React State с window.location.pathname
});

Б. Контекст React (Провайдер) BrowserRouter (обертка приложения) создает React Context (NavigationContext, LocationContext), в который кладет:

  • Текущий location (pathname, search, hash, state).
  • navigator (объект с методами push, replace, go, back, forward).
  • basename (подпуть, если приложение не в корне домена).

В. Алгоритм матчинга (Matching Algorithm) При изменении location роутер:

  1. Берет дерево маршрутов (<Routes><Route path="..." />...</Routes>).
  2. Рекурсивно сравнивает сегменты URL с path маршрутов (используя path-to-regexp или свой парсер в v6).
  3. Находит наиболее конкретное совпадение (Longest Prefix Match).
  4. Формирует массив matches (данные о параметрах :id, данных loader/action, элементе компонента).
  5. Триггерит ре-рендер <Routes>, который рендерит иерархию <Outlet /> в соответствии с matches.

3. Основные сущности React Router v6+

Компонент / ХукНазначение
<BrowserRouter>История на базе History API (чистые URL: /users/1).
<HashRouter>История на базе window.location.hash (/#/users/1). Для статического хостинга без настройки сервера.
<Routes>Контейнер для <Route>. Выполняет матчинг одного лучшего маршрута (в отличие от v5 Switch).
<Route path="..." element={<Comp />} />Правило сопоставления. path поддерживает параметры (:id), сплеты (*), опциональные (?).
<Outlet />Ключевой элемент для вложенного роутинга. Рендерит дочерний match внутри родительского лейаута.
<Link to="/path" />Декларативная навигация. Рендерит <a>, но перехватывает onClicknavigator.push().
useNavigate()Императивная навигация (navigate('/path', { replace: true, state: {} })).
useParams()Доступ к параметрам URL (:id{ id: '123' }).
useSearchParams()Работа с Query String (?page=1&sort=asc) → [searchParams, setSearchParams].
useLocation()Текущий location объект (pathname, search, hash, state).
useMatches()Доступ ко всем совпавшим маршрутам (для хлебных крошек, лейаутов).

4. Паттерны и Best Practices (Senior/Tech Lead)

А. Вложенный роутинг (Layout Routes) — Основа архитектуры

// App.jsx
<Routes>
<Route path="/" element={<RootLayout />}> // Общий лейаут (Header, Footer)
<Route index element={<Home />} /> // /
<Route path="about" element={<About />} /> // /about
<Route path="dashboard" element={<DashboardLayout />}> // Вложенный лейаут
<Route index element={<DashboardHome />} /> // /dashboard
<Route path="settings" element={<Settings />} /> // /dashboard/settings
</Route>
</Route>
<Route path="login" element={<Login />} /> // Отдельный лейаут (без Header)
<Route path="*" element={<NotFound />} /> // 404 (Catch-all)
</Routes>

// RootLayout.jsx
export default function RootLayout() {
return (
<>
<Header />
<main><Outlet /></main> // <-- СЮДА рендерятся Home, About, DashboardLayout
<Footer />
</>
);
}

Б. Data Loading: Loaders & Actions (React Router v6.4+ / Remix Style) Перенос логики загрузки данных из useEffect в роутер.

// route config (createBrowserRouter)
export const router = createBrowserRouter([
{
path: "/users/:id",
element: <UserProfile />,
loader: async ({ params }) => { // Запускается ПЕРЕД рендером
const user = await fetchUser(params.id);
if (!user) throw new Response("", { status: 404 }); // Ошибка -> Error Boundary
return user; // Доступно в компоненте через useLoaderData()
},
action: async ({ request, params }) => { // Обработка POST/PUT/DELETE (формы)
const formData = await request.formData();
return updateUser(params.id, formData);
},
errorElement: <UserErrorFallback />, // UI для ошибок loader/action
}
]);

// UserProfile.jsx
export default function UserProfile() {
const user = useLoaderData(); // Типизированные данные, нет useEffect/loading state
const navigate = useNavigate();

return (
<Form method="post"> {/* Автоматически вызывает action */}
<input name="name" defaultValue={user.name} />
<button type="submit">Save</button>
</Form>
);
}

> Почему это круто: SSR-ready, кэширование, предзагрузка (<Link prefetch>), оптимистичное UI, разделение ответственности.

Г. Защищенные маршруты (Auth Guard) — Паттерн Wrapper

function PrivateRoute({ children }) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
return user ? <Outlet /> : <Navigate to="/login" replace state={{ from: location }} />;
}

// Использование
<Route element={<PrivateRoute />}>
<Route path="profile" element={<Profile />} />
<Route path="admin" element={<Admin />} />
</Route>

Д. Lazy Loading (Code Splitting) + Suspense

import { lazy, Suspense } from 'react';

const HeavyDashboard = lazy(() => import('./HeavyDashboard'));

<Route
path="dashboard"
element={
<Suspense fallback={<Spinner />}>
<HeavyDashboard />
</Suspense>
}
/>

Е. Типизация (TypeScript) — declare module 'react-router'

// types/react-router.d.ts
import 'react-router';

declare module 'react-router' {
interface LoaderData {
'/users/:id': User; // Тип данных для конкретного маршрута
}
interface ActionData {
'/users/:id': { success: boolean };
}
// Параметры URL
interface Params {
'/users/:id': { id: string };
}
}

// В компоненте -> полная типизация без generics
const user = useLoaderData(); // Тип: User
const params = useParams(); // Тип: { id: string }

5. Настройка Сервера (Production Must-Have)

Поскольку это SPA, сервер не знает про /users/1. При прямом заходе/рефреше он вернет 404. Решение: Настроить Fallback на index.html для всех не-ASSЕТ путей.

Nginx:

location / {
try_files $uri $uri/ /index.html; # Ключевая строка
}

Node/Express:

app.use(express.static('build'));
app.get('*', (req, res) => res.sendFile(path.resolve('build', 'index.html')));

6. Альтернативы и Конкуренты (2024)

БиблиотекаФишкаКогда выбирать
React Router v6Стандарт де-факто, Loaders/Actions, RSC support (v7).95% проектов.
TanStack RouterType-safe first, File-based routing, встроенный кэш/инвалидация (как React Query), DevTools.Новые проекты, если нужен максимальный DX и типизация.
WouterМинималистичный (1.5kb), хуки, нет контекста.Микро-фронтенды, встраиваемые виджеты, лендинги.
Next.js / Remix (App Router)Файловая система = Роутинг, RSC, Server Actions.Фуллстек React фреймворки (рекомендуется для новых стартов).

Резюме (Interview Cheatsheet)

> Router — это синхронизатор URL ↔ UI. > > Механика: History API (pushState/popstate) + React Context (Location/Navigator) + Алгоритм матчинга путей. > > Ключевые концепции v6+: > 1. <Outlet /> — вложенные лейауты (Composition > Inheritance). > 2. Loaders / Actions — загрузка и мутации данных в роутере, а не в useEffect (Single Source of Truth для данных страницы). > 3. <Link prefetch /> — предзагрузка данных/кода при наведении. > 4. Error Boundaries + errorElement — декларативная обработка ошибок загрузки. > > Senior Mindset: Не пишите логику загрузки в useEffect. Используйте Loaders для GET и Actions + <Form /> для POST. Настройте серверный Fallback (try_files). Типизируйте параметры и данные лоадеров через Module Augmentation. Рассмотрите TanStack Router для новых проектов ради типизации URL first-class.

Вопрос 29. Что такое Flexbox и какие основные свойства вы знаете?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:16:49"/>

Оценка ответа кандидата: правильный. Кандидат верно назвал Flexbox инструментом для верстки/стилизации, перечислил display: flex, flex-direction, justify-content, align-items, flex-wrap.


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

Flexbox (Flexible Box Layout) — это одномерная модель компоновки (layout mode) в CSS, разработанная для эффективного распределения пространства между элементами контейнера и управления их выравниванием, порядком и размером, даже когда их размер неизвестен или динамически меняется.

Главная идея: Контейнер управляет геометрией детей (распределением свободного места, выравниванием по главной/поперечной оси), а не дети сами себя (в отличие от float или inline-block).


1. Архитектура: Две оси

Понимание осей — ключ к Flexbox. Все свойства выравнивания работают относительно этих осей.

Главная ось (Main Axis) — ось, вдоль которой выкладываются флекс-элементы.

  • Направление задается flex-direction.
  • Начало: main-start, Конец: main-end.
  • Размер элемента вдоль этой оси: main size (width или height).

Поперечная ось (Cross Axis) — ось, перпендикулярная главной.

  • Начало: cross-start, Конец: cross-end.
  • Размер элемента вдоль этой оси: cross size.

2. Свойства контейнера (Parent / Flex Container)

Объявляется: display: flex (блочный) или display: inline-flex (строчно-блочный).

А. flex-direction — Направление главной оси

.container {
flex-direction: row | row-reverse | column | column-reverse;
}
  • row (default): слева направо (ltr).
  • column: сверху вниз. Важно: в column главная ось вертикальна, justify-content работает вертикально, align-items — горизонтально.

Б. flex-wrap — Перенос строк

.container {
flex-wrap: nowrap | wrap | wrap-reverse;
}
  • nowrap (default): все в одну строку, сжимаются.
  • wrap: перенос на новые строки (сверху вниз).
  • wrap-reverse: перенос снизу вверх.

В. flex-flow — Шорткат

.container {
flex-flow: <flex-direction> <flex-wrap>; /* напр. row wrap */
}

Г. justify-content — Выравнивание по ГЛАВНОЙ оси (Распределение свободного места)

.container {
justify-content:
flex-start | /* К началу */
flex-end | /* К концу */
center | /* По центру */
space-between | /* Первый в начале, последний в конце, равные отступы между */
space-around | /* Равные отступы вокруг каждого (крайние половинные) */
space-evenly; /* Абсолютно равные отступы везде */
}

Д. align-items — Выравнивание по ПОПЕРЕЧНОЙ оси (для ВСЕХ элементов)

.container {
align-items:
stretch | /* Default: растягивает на всю высоту строки (если нет height) */
flex-start | /* К cross-start */
flex-end | /* К cross-end */
center | /* По центру поперечной оси */
baseline; /* По базовой линии текста */
}

Е. align-content — Выравнивание СТРОК (только если flex-wrap: wrap и есть свободное место по поперечной оси)

.container {
align-content:
stretch | /* Default: строки растянуты на весь контейнер */
flex-start | flex-end | center |
space-between | space-around | space-evenly;
}

> Senior Note: align-content не работает на однострочном контейнере (nowrap).

Ж. gap, row-gap, column-gap — Отступы между элементами Современный стандарт, заменяет хак с margin у детей.

.container {
gap: 16px; /* И по вертикали, и по горизонтали */
gap: 16px 24px; /* row-gap column-gap */
}

Не создает отступов у краев контейнера (в отличие от margin у детей).


3. Свойства элементов (Children / Flex Items)

А. order — Порядок отображения

.item { order: <integer>; } /* Default: 0. Отрицательные допускаются. */

Меняет визуальный порядок без изменения DOM. Accessibility Warning: ломает навигацию Tab/скринридеры, если порядок важен для смысла.

Б. flex-grow — Коэффициент РОСТА (как забрать свободное место)

.item { flex-grow: <number>; } /* Default: 0 (не растет) */

Если у всех 1 — делят место поровну. Если у одного 2, а у других 1 — первый получит в 2 раза больше свободного места.

В. flex-shrink — Коэффициент СЖАТИЯ (как отдать место при нехватке)

.item { flex-shrink: <number>; } /* Default: 1 (сжимается) */

0 — элемент не сжимается ниже flex-basis/width (может вызвать overflow контейнера).

Г. flex-basis — Начальный базовый размер ГЛАВНОЙ оси

.item { flex-basis: <length> | auto | content; } /* Default: auto */
  • auto: смотрит на width/height, если нет — на контент.
  • 0: размер строго по контенту (используется с flex-grow для равномерного распределения всего места).
  • content: размер по контенту (max-content).

Д. flex — Шорткат (Рекомендуется использовать только его)

.item { flex: <flex-grow> <flex-shrink> <flex-basis>; }

Популярные пресеты:

flex: 1; /* = 1 1 0% — Равномерное заполнение всей ширины */
flex: auto; /* = 1 1 auto — Растет/сжимается от базы width */
flex: none; /* = 0 0 auto — Фиксированный размер (width/height) */
flex: 0 0 200px; /* Фиксированный 200px, не растет, не сжимается */

Е. align-self — Индивидуальное выравнивание по поперечной оси Переопределяет align-items контейнера для конкретного элемента.

.item { align-self: auto | stretch | flex-start | flex-end | center | baseline; }

4. Типичные паттерны (Senior Cookbook)

А. «Holy Grail» / Sticky Footer (Колонка с прижатым футером)

.body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.content { flex: 1; } /* Растет, заполняя середину */
.footer { flex-shrink: 0; } /* Не сжимается */

Б. Центрирование чего угодно (Внутри родителя)

.centered {
display: flex;
justify-content: center; /* Горизонт */
align-items: center; /* Вертикаль */
}

В. Media Object / Card (Иконка слева, текст справа, текст не обтекает иконку)

.card {
display: flex;
gap: 16px;
}
.icon { flex-shrink: 0; } /* Иконка фиксирована */
.text { min-width: 0; } /* Критично для text-overflow: ellipsis во flex-ребенке */

Г. Равномерная сетка карточек (без media queries, с minmax логикой)

.grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.card {
flex: 1 1 300px; /* Растет от 300px, сжимается до 300px, потом перенос */
/* Или: flex: 0 0 calc(33.333% - 16px); — строгая сетка 3 колонки */
}

5. Подводные камни и Best Practices

  1. min-width: 0 / min-height: 0 для флекс-детей. По умолчанию флекс-элемент не может быть уже своего контента (min-width: auto). Если внутри есть длинный текст с text-overflow: ellipsis или вложенный скролл — обязательно ставьте min-width: 0 (для row) или min-height: 0 (для column).

  2. flex-shrink: 0 для фиксированных элементов. Иконки, аватары, кнопки — всегда ставьте flex-shrink: 0 (или flex: 0 0 auto), иначе они сплющатся на мобильных.

  3. Не используйте margin: auto для центрирования во flex — используйте justify-content / align-items. Но margin-left: auto у последнего элемента — валидный паттерн для «прижатия к правому краю» (divider pattern).

  4. Производительность: Flexbox быстрее Grid для одномерных задач. Не оборачивайте всё в div ради флекса — используйте display: contents у промежуточных оберток, чтобы дети участвовали в флекс-раскладке родителя напрямую.

  5. Gap vs Margin: gap не работает в старых браузерах (IE11, старый Safari) без полифилов/префиксов. В продакшене 2024+ — безопасно везде.


Резюме (Cheatsheet)

ЗадачаСвойствоЗначение
Сделать контейнер флексомdisplayflex / inline-flex
Направлениеflex-directionrow / column
Переносflex-wrapwrap
Горизонталь (main axis)justify-contentcenter / space-between / space-around
Вертикаль (cross axis)align-itemscenter / stretch / flex-start
Отступы междуgap16px
Элемент: расти/сжиматься/базаflex1 / auto / 0 0 200px
Элемент: не сжиматьсяflex-shrink0
Элемент: свой alignalign-selfcenter

> Senior Rule: Для 90% задач достаточно display: flex, gap, flex: 1 (для растягивания), flex-shrink: 0 (для фиксированных) и min-width: 0 (для текста с обрезкой). Grid оставляйте для двумерных сеток.

Вопрос 30. В чем разница между виртуальным DOM (Virtual DOM) и реальным DOM (Real DOM)?

Таймкод: <YouTubeSeekTo id="oDNmGfAIpQY" time="00:17:34"/>

Оценка ответа кандидата: правильный. Кандидат верно объяснил: Real DOM — тяжелое дерево браузера, любое изменение вызывает полную перерисовку. Virtual DOM — легкая копия в памяти, React сравнивает её с предыдущей (диффинг) и обновляет только измененные части в реальном DOM, что быстрее.


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

Real DOM (Browser DOM) — это программный интерфейс (API) браузера, представляющий HTML/XML документ в виде иерархического дерева узлов (Node Tree). Он живой, мутабельный и тяжеловесный: любое изменение (даже атрибута) может триггерить Reflow (Пересчет макета) и Repaint (Перерисовку) всего поддерева или страницы.

Virtual DOM (VDOM) — это обычный JavaScript-объект (Plain Object), который является легковесной иммутабельной копией Real DOM. React хранит его в памяти. Это паттерн программирования, а не браузерная технология.

> Ключевая мысль: VDOM не быстрее Real DOM сам по себе (накладные расходы на создание JS-объектов и диффинг есть). Выгода в минимизации количества дорогостоящих обращений к браузерному API (батчинг, отложенные обновления, умный диффинг).


1. Архитектура и Жизненный цикл кадра (Frame)

Real DOM Pipeline (Браузер):

JS Mutation → Style Calculation → Layout (Reflow) → Paint → Composite

Каждый шаг дорогой. layout (reflow) — самый тяжелый (O(N) по элементам).

React Pipeline (VDOM + Fiber):

Render Phase (In Memory) Commit Phase (Browser)
───────────────────────── ─────────────────────
1. Вызов компонентов (JSX) → 1. Синхронный DOM Mutations
2. Создание нового VDOM Tree 2. Layout/Paint (Browser)
3. Diffing (Reconciliation)
4. Генерация Effect List (Fiber)

React разделяет работу на Render Phase (может быть прервана, асинхронна, чистая) и Commit Phase (синхронная, побочные эффекты, реальные DOM-изменения).


2. Алгоритм Реконсиляции (Diffing) — O(N) Эвристики

Полное сравнение деревьев — O(N³). React использует эвристики O(N) (предположения):

  1. Разные типы корневых элементов → Полная замена.
    // Было
    <div><App /></div>
    // Стало
    <span><App /></span>
    // React: unmount div + children → mount span + children. State потерян.
  2. Один тип элемента → Обновление пропсов/атрибутов.
    <div className="old" /><div className="new" style={{color: 'red'}} />
    // React: меняет только className и style. Рекурсивно диффчит children.
  3. Списки элементов → key как уникальный идентификатор. Без key: React сравнивает по индексу (проблемы при вставке/удалении в середину). С key: React находит перемещения, минимальные вставки/удаления.
    // Эффективно: key стабилен, уникален, предсказуем (ID из БД)
    <li key="user-1">Аня</li>
    // Антипаттерн: key={index} или key={Math.random()}

Senior Note: key должен быть стабильным (не меняться между рендерами), уникальным среди соседей и предсказуемым. Использование index как key ломает состояние инпутов/анимаций при сортировке/фильтрации.


3. Fiber Architecture (React 16+) — Реализация VDOM

VDOM в современном React — это не просто дерево объектов, а Fiber Tree (связный список).

Структура Fiber Node:

{
// Тип компонента (Function, Class, Host 'div')
type: Component,
// Пропсы для рендера
pendingProps: {}, memoizedProps: {},
// State / Hooks
memoizedState: null,
// Указатели на дерево (Linked List)
return: Fiber | null, // Родитель
child: Fiber | null, // Первый ребенок
sibling: Fiber | null, // Следующий брат
// Эффекты (Side Effects)
flags: Placement | Update | Deletion | ..., // Что сделать в Commit
subtreeFlags: ..., // Агрегация флагов детей
// Альтернатива (Double Buffering)
alternate: Fiber | null, // Текущее дерево (current) ↔ Рабочее дерево (workInProgress)
}

Double Buffering (Текущее ↔ Рабочее дерево):

  • current — то, что на экране (committed).
  • workInProgress — строится в памяти при рендере.
  • После успешного Commit — указатели меняются местами (root.current = workInProgress). Это дает атомарность и возможность отката (rollback) при ошибках или приоритетах.

Приоритезация (Lanes / Priority Levels):

// Примеры приоритетов (lanes)
SyncLane // onClick, setState (высший)
InputContinuousLane // drag, scroll
DefaultLane // fetch, Suspense (обычный)
IdleLane // prefetch, analytics (фон)

React может прервать рендер низкого приоритета, если пришел высокий (например, ввод текста во время загрузки списка), и продолжить позже. Real DOM этого не умеет.


4. Почему «VDOM быстрее» — это упрощение (Trade-offs)

СценарийReal DOM (Наивный)VDOM (React)Комментарий
Мало изменений1 Reflow1 Diff + 1 ReflowVDOM медленнее (накладные расходы на JS).
Много изменений (батч)N Reflows1 Diff + 1 ReflowVDOM выигрывает (батчинг обновлений).
Сложные вычисленияБлокирует Main ThreadTime-slicing (Fiber)VDOM выигрывает (реакт отдает управление браузеру).
Первичный рендер (SSR)Быстрее (HTML из сети)Медленнее (Hydration)VDOM добавляет шаг гидрации.

Вывод: VDOM — это компромисс. Мы платим CPU (JS) за экономию GPU/Layout (Browser). Для интерактивных приложений (SPA) это выигрыш. Для статических страниц — оверхед.


5. Продвинутые оптимизации поверх VDOM (Senior Level)

React не волшебство. Разработчик должен помогать алгоритму:

А. React.memo / PureComponent — Пропуск диффинга поддерева

const ExpensiveChild = React.memo(({ data, onAction }) => {
// Ре-рендер ТОЛЬКО если data или onAction изменились (по ссылке!)
return <div>{data.map(renderItem)}</div>;
});

// В родителе: стабилизация ссылок
const handleAction = useCallback((id) => {...}, [dep]); // Чтобы onAction не менялся
const data = useMemo(() => computeExpensive(props), [props]); // Чтобы data не менялся

Без стабилизации пропсов memo бесполезен (сравнение по ссылке → всегда true → ре-рендер).

Б. useMemo / useCallback — Мемоизация значений для пропсов Прямое влияние на эффективность VDOM Diffing: если пропсы не менялись по ссылке — React.memo сработает, Fiber не спустится вглубь.

В. key для стабильности инстансов Сохраняет инстансы классовых компонентов и DOM-узлы (фокус, selection, состояние инпутов) при переупорядочивании.

Г. useTransition / useDeferredValue — Приоритизация (Concurrent Features)

function SearchResults({ query }) {
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
const value = e.target.value;
setInputValue(value); // Urgent: обновляем инпут мгновенно
startTransition(() => {
setSearchQuery(value); // Non-urgent: тяжелый рендер списка можно прервать
});
};
}

Позволяет React отложить тяжелый диффинг/рендер низкого приоритета, не блокируя UI.


6. Альтернативы VDOM (Контекст 2024+)

ПодходПримерыФилософия
VDOM (Diffing)React, Vue 2/3, PreactДекларативный UI, платформонезависимость.
Fine-grained Reactivity (Signals)SolidJS, Svelte 5, Vue 3 (ref), Preact SignalsНет VDOM. Компилятор/рантайм знает точно, какой DOM-узел обновить. O(1) обновление.
Island Architecture / No JSAstro, Qwik, HTMXМинимизация JS на клиенте. Resumability (Qwik) вместо Hydration.

> Senior View: VDOM (React) выигрывает в экосистеме, типизации (TSX), Server Components (RSC), Suspense, конкурентности. Signals (Solid/Svelte) выигрывают в сырой производительности простых обновлений и размере бандла. Выбор — архитектурный компромисс.


Резюме (Interview Cheatsheet)

> Real DOM — браузерное API, тяжелое, синхронное, вызывает Layout/Paint при каждой мутации. > > Virtual DOM — JS-объект в памяти, иммутабельный, дешевый к созданию. > > Reconciliation (Fiber) — алгоритм O(N) диффинга с эвристиками (тип элемента, key). Строит Effect List (список изменений). > > Commit Phase — единственный момент, когда React трогает Real DOM (батчинг). > > Ключ к производительности: > 1. Стабильные key (ID из БД). > 2. Стабильные ссылки пропсов (useMemo, useCallback) + React.memo. > 3. Понимание приоритетов (useTransition) для тяжелых деревьев. > 4. Избегание инлайн-функций/объектов в JSX пропсах без необходимости.

VDOM — это не магия скорости, это архитектура для предсказуемого управления сложностью UI.