Fresher's Frontend Interview 🎉 | JavaScript | #reactjs (Mock) [Most Asked Questions -2023]
Сегодня мы разберем собеседование джуниор-фронтенд-разработчика, который уверенно оперирует базовыми определениями 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) | Только Content | width + padding + border + margin | Исторический стандарт W3C. Сложен для адаптивной верстки: добавление padding расширяет элемент. |
border-box | Content + Padding + Border | width + 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) никогда не схлопываются.
Правила:
- Соседние братья: Маржины объединяются. Итоговый отступ =
max(marginBottom_1, marginTop_2). - Родитель и первый/последний ребенок: Маржины "просачиваются" наружу, если у родителя нет:
padding-top/bottom> 0border-top/bottom> 0- Созданного BFC (см. ниже)
overflow: hidden/auto/scroll(классический трюк)display: flow-root(современный способ создать BFC без сайд-эффектов)
- Пустые блоки: Если у элемента нет контента, 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 API | document.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). Браузер решает конфликт свойств в таком порядке:
- Origin & Importance (Происхождение и
!important):- User Agent (браузер) normal
- User (настройки пользователя) normal
- Author (разработчик) normal
- Author
!important - User
!important - User Agent
!important - Transition/Animation origins (специфичные слои)
- Context (Контекст / Shadow DOM / Layers): Стили внутри Shadow DOM изолированы. CSS Cascade Layers (
@layer) позволяют управлять приоритетом групп стилей до специфичности. - Inline Styles (
style="color: red") — вес (1, 0, 0, 0) в расширенной нотации, побеждают любой ID в Author origin. - Specificity (Специфичность селектора) — наш кортеж (A, B, C).
- Order of Appearance (Порядок в коде) — побеждает последний объявленный (для одинаковой специфичности).
- 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)добавляет вес класса.
В. Современные инструменты контроля каскада
-
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), делая специфичность селекторов внутри слоя второстепенной.
-
: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 */
-
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)
- Есть ли
#idв CSS-файлах? (Только если это утилита типа#skip-linkдля a11y). - Не превышает ли глубина вложенности селекторов 1-2 уровней (БЭМ:
.block__element--modifier)? - Используется ли
@layerдля разделения Reset / Base / Components / Utilities? - Применяется ли
:where()для сторонних виджетов/ресетов? - Нет ли
!importantв Author normal origin (кроме утилит в верхнем слое или переопределения инлайн-стилей из JS-библиотек)? - Используются ли 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) — Ключевое различие
| Характеристика | var | let / 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).
| Этап | var | let / 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)
- Default to
const. 80-90% переменных не должны менять ссылку. - Use
letтолько при необходимости реассигнмента (счетчики, флаги, переключение состояния). - Never use
varв современном коде (ESM, TypeScript, React/Vue/Node.js).- Исключение: легаси-код без транспиляции, специфичные хитрости с
functionscope (редко).
- Исключение: легаси-код без транспиляции, специфичные хитрости с
- 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)
| Фича | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Да (init undefined) | Да (но TDZ) | Да (но TDZ) |
| Re-declaration | ✅ | ❌ SyntaxError | ❌ SyntaxError |
| Re-assignment | ✅ | ✅ | ❌ TypeError |
| Global Object Prop | ✅ (window.var) | ❌ | ❌ |
| Loop Binding | Одна на все | Новая на итерацию | Новая на итерацию |
| TDZ | Нет | Есть | Есть |
| Usage | Legacy / Avoid | Mutable binding | Default 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 Type | screen, print, speech, all (по умолчанию) | Тип устройства. all подразумевается, если опущен. only скрывает стили от старых браузеров (не парсящих MQ). |
| Media Feature | width, height, orientation, resolution, hover, pointer, prefers-color-scheme, prefers-reduced-motion... | Характеристика для проверки. |
| Logical Operators | and, , (или), 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 |
|---|---|---|
hover | none, hover | Есть ли у устройства наведение (мышь/трекпад vs тачскрин). @media (hover: hover) { .dropdown:hover ... } |
pointer | none, 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)
- Mobile First: Базовые стили без
@media. Брейкпоинты толькоmin-width. - Relative Units: Брейкпоинты в
rem/em(неpx!), чтобы respecter пользовательский зум/шрифт./* Хорошо: масштабируется с базовым шрифтом пользователя */@media (min-width: 48rem) { ... } /* 768px при 16px base */ - Interaction Features: Всегда проверяйте
hover/pointerдля выпадающих меню, тултипов, ховер-эффектов карточек.@media (hover: hover) and (pointer: fine) {.card:hover { transform: scale(1.02); } /* Только для мыши */} - Accessibility First:
@media (prefers-reduced-motion: reduce) {*, *::before, *::after {animation-duration: 0.01ms !important;transition-duration: 0.01ms !important;scroll-behavior: auto !important;}}
- Dark Mode: Используйте CSS Custom Properties внутри
@media (prefers-color-scheme: dark), а не дублирование селекторов. - Container Queries: Для переиспользуемых UI-компонентов (Card, Form, Widget) — мигрируйте на
@container. - 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/Grid | margin: 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)
Решения:
- Стрелочные функции (лексический
this). cb.call(thisArg, ...args)внутри библиотеки.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).
- Синхронные — для обобщения логики (Array methods, Strategies).
- Асинхронные — для продолжения вычислений после I/O (Continuation Passing Style).
- Проблема: "Callback Hell", ручное прокидывание ошибок, потеря
this, отсутствие стандарта отмены. - Решение 2015+: Promises + Async/Await для линейных цепочек; Observables/Signals для стримов.
- На собеседовании: Умейте написать
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) — Неизменяемые и Исключительные
Промис находится строго в одном из состояний. Переходы односторонние: Pending → Fulfilled ИЛИ Pending → Rejected.
| Состояние | Описание | Результат (value / reason) | Методы реакции |
|---|---|---|---|
pending (Ожидание) | Начальное состояние. Операция в процессе. | undefined | — |
fulfilled (Выполнен / Resolved) | Операция завершена успешно. | value (любое значение, включая undefined, null, другой Промис) | .then(onFulfilled) |
rejected (Отклонен) | Операция завершилась ошибкой. | reason (обычно Error объект, но может быть любым) | .catch(onRejected) / .then(null, onRejected) |
Ключевые гарантии спецификации (Promises/A+):
- Immutability: Состояние не может измениться после перехода в
fulfilled/rejected. - Thenable Assimilation: Если
onFulfilledвозвращает thenable (объект с методомthen), промис "распаковывает" его и принимает его состояние. - 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. Если все rejected → AggregateError (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). Паттерны:
- AbortController / AbortSignal (Fetch API, EventTarget) — стандарт де-факто для I/O.
- Флаг в замыкании (для своих промисов):
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 };}
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
- Всегда возвращай промисы из
asyncфункций и.thenколбэков (если не прерываешь цепочку намеренно). - Не оборачивай промисы в
new Promiseбез нужды (Promisify только callback-based API:util.promisify,new Promise(res => fs.readFile(..., res))). - Используй
Promise.allSettledдля независимых UI-виджетов/аналитики. - Оборачивай точку входа (
main,handler,controller) вtry/catch/.catchдля предотвращения Unhandled Promise Rejection (краш Node.js / предупреждение в браузере). - Избегай
awaitв циклах (forEach,mapбезPromise.all), если операции независимы. - Передавай
AbortSignalво все fetch/axios вызовы для отмены при анмаунте компонента / навигации. - Типизируй в 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 Callbacks | requestAnimationFrame (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):
- Timers —
setTimeout,setIntervalcallbacks. - Pending Callbacks — отложенные I/O колбэки (TCP errors).
- Idle / Prepare — внутренние нужды libuv.
- Poll — Главная фаза. Ожидание I/O (epoll/kqueue/IOCP). Выполнение I/O колбэков (
fs,net,http). Если таймеров нет — блокируется здесь. - Check —
setImmediate()callbacks (гарантированно после Poll). - Close Callbacks —
socket.on('close', ...).
Важные отличия Node:
process.nextTick()— не микрозадача по стандарту HTML, но выполняется после каждой фазы и перед микрозадачами Promise. Приоритет выше, чем у Promise.setImmediate()— выполняется в фазе Check (после Poll/I/O), гарантированно быстрееsetTimeoutв I/O контексте.Promisemicrotasks — выполняются между фазами (после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. Чек-лист для отладки и понимания
- Stack Trace — показывает только синхронный путь. Асинхронный стек разорван (кроме
async/awaitс поддержкойError.stackв современных движках). - Performance Tab (DevTools) — вкладка Main. Видишь: Task (желтый) → Microtasks (фиолетовый/розовый) → Render (фиолетовый) → Idle.
console.logв микрозадаче — выполнится до следующего лога в макрозадаче.MutationObserver— срабатывает синхронно после мутации DOM, но до рендера (в микрозадаче). Полезно для реакции на изменения безsetTimeout.navigator.scheduling.isInputPending()— проверка, есть ли пользовательский ввод, чтобы отдать приоритет (yield).
Резюме: Event Loop — это диспетчер единственного потока JS.
- Call Stack исполняет синхронный код.
- Web/Node APIs выполняют.async работу параллельно.
- Microtask Queue (Promise,
queueMicrotask,nextTick) имеет высший приоритет — очищается полностью после каждой макрозадачи и перед рендером. - Macrotask Queue (Timers, I/O, Events) — обрабатывается по одной за тик цикла.
- 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:
- Новые проекты: Оценивайте отказ от Sass в пользу Native CSS + PostCSS (autoprefixer, cssnano) + CSS Modules / Tailwind / Panda CSS.
- Легаси / Дизайн-системы: Sass всё еще важен для сложной генерации утилит, темизации через maps/loops,
@useархитектуры. - Миграция: Постепенный переход:
postcss-scssпарсер позволяет прогонять.scssчерез PostCSS плагины, понемногу заменяя фичи на нативные.
5. Частые ошибки и Best Practices
-
@importvs@use/@forward@import— устарел (deprecated), создает глобальное пространство имен, дублирует импорты, не поддерживает приватность (_prefix).- Всегда используйте
@use(с namespace) и@forward(для реэкспорта API библиотек).
-
Глобальные переменные vs CSS Custom Properties
- Не путайте
$font-size: 16px(compile-time constant) с--font-size: 16px(runtime variable). - Паттерн: Sass переменные для конфигурации сборки (брейкпоинты, палитра токенов), CSS переменные — для темизации/режимов (dark/light, density) в браузере.
- Не путайте
-
Глубина вложенности (Nesting Hell)
- Sass позволяет
.a { .b { .c { .d { ... } } } }→.a .b .c .d(высокая специфичность, жесткая привязка к DOM). - Правило: Максимум 1-2 уровня вложенности. Используйте БЭМ / CSS Modules / Utility-first для плоских селекторов.
- Sass позволяет
-
@extendvs%placeholdervs@mixin@extend .btn→.btn, .my-btn { ... }(группировка селекторов).%btn(placeholder) → не выводится в CSS, если не расширен.- Рекомендация: Избегайте
@extend. Используйте@mixin(композиция) или Utility классы.@extendломает Source Maps и порядок каскада.
-
Деление чисел (Division)
- Старый Sass:
$width / 2— деление. - Современный Sass (Dart Sass):
/— это разделитель значений (как в CSSgrid-template: 1fr / 2fr). - Обязательно:
math.div($width, 2)илиcalc(#{$width} / 2).
- Старый Sass:
Резюме:
Браузер видит только 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 не является отдельным языком или библиотекой. Это комбинация:
XMLHttpRequest(XHR) или современныйFetch API— браузерные API для выполнения HTTP-запросов из JavaScript.Promise/async-await— для управления асинхронностью и цепочками обработки.JSON— стандарт де-факто для сериализации данных (вместо XML).- DOM API — для динамического обновления интерфейса на основе полученных данных.
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 / Cancel | xhr.abort() | AbortController / AbortSignal (стандартный, работает везде). |
| Credentials (Cookies/CORS) | xhr.withCredentials = true | credentials: '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
- Инициация: JS вызывает
fetch()/xhr.send(). Это синхронная операция (микросекунды). - Делегирование: Браузер передает запрос сетевому стеку ОС (отдельный поток/процесс браузера). JS стек освобождается.
- Ожидание (Network Latency): Сервер обрабатывает, данные идут по проводам. Main Thread свободен для UI (клики, скролл, анимации).
- Приход ответа: Сетевой поток кладит задачу в Task Queue (Macrotask Queue) — "Network callback".
- Event Loop: После текущего макротаска и всех микрозадач (Promise callbacks) Event Loop забирает задачу из очереди.
- Resolve Promise:
fetchпромис резолвится вResponseобъект. - Чтение тела:
response.json()/.text()/.blob()— это тоже асинхронные операции (читают поток), возвращающие новые промисы (микрозадачи). - Обновление 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-слоя
- Используется
fetch/axios/kyсasync/await(нетthen/callback hell). - Обработка
!response.ok(HTTP ошибки не выбрасывают исключение вfetch). - Есть
try/catchс разделениемAbortErrorи реальных ошибок. - Используется
AbortControllerдля отмены при анмаунте / навигации. - Заголовки:
Content-Type: application/json,Accept: application/json. -
credentials: 'include'настроено правильно для кук / HttpOnly токенов. - Нет утечев чувствительных данных в URL (
GET /api/users?password=123). - Идемпотентность для мутаций (Idempotency Keys для платежей/заказов).
- Типизация ответа (TypeScript:
interface UserResponse,zod/valibotвалидация в рантайме). - Логирование ошибок в Sentry/LogRocket с контекстом (userId, url, body).
- Для списков: пагинация / бесконечный скролл / виртуализация (не грузить 10к записей).
- Используется библиотека управления серверным состоянием (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 merge | Merge Commit (non-fast-forward). | Сохранение истории "как было". Для интеграции feature-веток в main/develop. git merge --no-ff гарантирует merge-коммит. |
git merge --squash | Squash (один коммит). | Для 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>) | БЕЗВОЗВРАТНО удаляет незакоммиченные правки в рабочей директории. |
| Отменить последний коммит, оставить изменения в staged | git 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 / amend | git reflog | Спасательный круг. Показывает историю движения HEAD. Найди хеш -> git checkout <hash> -> git switch -c recovery-branch. |
| Очистить рабочую директорию до состояния HEAD | git clean -fd | Удаляет untracked файлы (-f) и директории (-d). -x — включая игнорируемые (.gitignore). ОСТОРОЖНО. |
5. Работа с удаленным репозиторием (Remotes & Collaboration)
| Команда | Назначение | Важные нюансы |
|---|---|---|
git fetch | Скачивает объекты и рефы (ветки/теги) с удаленного. Не трогает рабочую директорию и HEAD. | git fetch --prune (удалить локальные трекинг-ветки, удаленные на сервере). git fetch origin main:main (обновить локальную main без чекаута). |
git pull | fetch + 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 Flow | main (deployable) -> feature branches -> PR -> merge to main -> deploy. | Веб-приложения, CI/CD, простая модель. |
| GitFlow | develop + main + feature/* + release/* + hotfix/*. Строгая модель релизов. | Релизные циклы (мобильные приложения, boxed software, embedded), когда нужно поддерживать несколько версий в продакшене одновременно. |
| GitLab Flow | main -> pre-prod/staging -> production (environment branches). | Когда деплой — это ручной/сложный процесс через окружения. |
Резюме для собеседования:
- Знайте разницу:
reset(local history rewrite) vsrevert(safe public history) vsrestore(working dir changes). - Понимайте граф:
HEAD,HEAD~,HEAD^,refs,detached HEAD. - Безопасность:
push --force-with-lease>push --force.reflog— ваша страховка. - Эффективность:
add -p,rebase -i --autosquash,worktree,bisect. - Командная работа: Conventional Commits, PR/Merge Request workflow, Code Review через
git diff branch...origin/main(triple dot diff — изменения только в ветке). - Инструменты: 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)
| Характеристика | undefined | null |
|---|---|---|
| Смысл | «Значение отсутствует неявно / еще не определено». Переменная объявлена, но не инициализирована. Свойство не существует в объекте. Функция ничего не вернула. | «Значение отсутствует намеренно / пусто по дизайну». Разработчик явно указал: «здесь ничего нет и это нормально». |
| Источник | Движок 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.missing — undefined).
5. Best Practices & Code Style (Airbnb / Google / Standard)
-
Не используйте
undefinedкак значение для присваивания.- ❌
user.name = undefined; - ✅
user.name = null;(смысл: «имя сброшено») - ✅
delete user.name;(смысл: «свойства нет»)
- ❌
-
Инициализируйте переменные сразу.
// Плохоlet user;if (loggedIn) user = getUser();// Хорошоconst user = loggedIn ? getUser() : null; -
Проверка на «пустоту» (Falsy) — опасна.
if (!value) { ... } // Ловит: null, undefined, 0, '', false, NaN- Явно проверяйте:
value === null,value == null,typeof value === 'undefined'.
- Явно проверяйте:
-
Возвращайте
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 < 1 | null — это валидное переданное значение. |
| Глубокая проверка на «пустоту» | value == null (только null/undefined) | Идиоматично, быстро, читаемо. |
Senior Insight:
Разница между null и undefined — это не просто трюк для собеседования. Это моделирование состояния в приложении.
undefined= Entropy (хаос, неинициализированность, ошибка, отсутствие).null= Controlled Empty State (контролируемое пустое состояние, бизнес-логика «нет данных»).
Смешивание их ведет к багам: user.address.city падает, если address — null (хорошо, падает сразу), но тихо возвращает undefined, если address — undefined (плохо, баг уйдет в прод). Явное 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'.
- Named:
- 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/Set | Map вместо 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 — только имя переменной (var — undefined, 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 Expression | Function Declaration в модулях ведет себя как let (блочная область), но лучше единообразие. |
5. Производительность и Память (Senior Insight)
-
Создание замыкания: Стрелочная функция, использующая внешние переменные (включая
this), создает замыкание (Closure) при каждом вызове внешней функции (если определена внутри) или один раз (если в классе/модуле).- В методах классов:
handleClick = () => {}создает новую функцию на каждый экземпляр (память O(N)). Обычный метод вprototype— одна функция на все экземпляры (O(1)). - Вердикт: Для частых инстанцируемых классов — лучше обычные методы +
bindв конструкторе или публичные поля-функции только для хендлеров.
- В методах классов:
-
Оптимизация движка (V8/SpiderMonkey):
- Современные движки отлично оптимизируют оба вида.
- Стрелочные функции чуть проще для инлайнинга (нет логики
thisbinding), но разница микроскопична. - Не выбирайте стрелочную "за производительность". Выбирайте по семантике
this.
-
Стек вызовов (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)
this: Обычная — динамический (кто вызвал). Стрелочная — лексический (где написана). Это главное отличие.arguments: Обычная — есть. Стрелочная — нет (используй...rest).new/prototype: Обычная — да. Стрелочная — нет (TypeError).super/new.target/yield: Обычная — да. Стрелочная — лексический / нет / нет.- Hoisting: Стрелочные не поднимаются как функции. Всегда Function Expression. Поведение = поведению
let/const/varпеременной, в которую записана. - Синтаксис: Стрелочные короче для колбэков, неявный возврат выражения, возврат объекта требует
(). - 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) =
- Environment Record (записи переменных:
let,const,var, параметры,this,arguments). - Ссылка на внешнее окружение (
[[OuterEnv]]) — цепочка областей видимости.
Алгоритм поиска переменной (Identifier Resolution):
- Ищем в Environment Record текущего Лексического Окружения.
- Если нет — переходим по
[[OuterEnv]]к внешнему Лексическому Окружению. - Повторяем до глобального объекта (
globalThis/window). - Если не найдено —
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
}
Как избежать:
- Обнулять ссылки:
hugeData = nullпосле использования (если функция долгоживущая). - Минимизировать замыкание: Выносить нужные данные в аргументы или слабые ссылки (
WeakMap,WeakRef). - Удалять слушатели:
removeEventListener(требует именованной функции). - Использовать
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 / Архитектуры
- Не создавайте функции в цикле/рендере без необходимости (React:
useCallback,useMemo; Vanilla: выносите наружу). Каждый вызов = новое замыкание = память + GC pressure. - Избегайте захвата больших объектов в долгоживущие замыкания (слушатели, таймеры, кэши).
- Понимайте разницу:
letв цикле = безопасно.varв цикле = баг. - Не используйте
eval/new Function— они ломают оптимизацию замыканий (создают "слоупоки" — Sloppy Closures, захватывающие всё). - Модульный паттерн / IIFE — старый способ инкапсуляции. В современном коде — ES Modules (статический импорт/экспорт, нет рантайм-замыканий для приватности).
- 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 Binding | new Fn() | Новый пустой объект ({}), созданный конструктором. |
| 2 | Explicit Binding | fn.call(obj), fn.apply(obj), fn.bind(obj)() | Переданный явно первый аргумент (obj). |
| 3 | Implicit Binding | obj.fn() | Объект перед точкой (obj). |
| 4 (Низший) | Default Binding | fn() (самостоятельный вызов) | 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)
this— это аргумент, скрытый от глаз. Передается в момент вызова.- Правило "Кто слева от точки?" (
obj.fn()->this = obj). - Стрелочные функции не имеют
this. Они замыкаются на внешнем. Не используйте их как методы объектов/классов, если не хотите авто-биндинга на экземпляр. bindсоздает новую функцию.call/applyвызывают сразу.bind>call/apply(hard binding), ноnew>bind.- Strict mode = безопасность.
this = undefinedлучше, чемwindow. - Потеря контекста — #1 баг. Всегда биндьте колбэки:
onClick={this.handleClick.bind(this)}/onClick={() => this.handleClick()}/ Public FieldhandleClick = () => {}. - В классах: Методы в прототипе (память) vs Стрелочные поля (автобиндинг, память на экземпляр). Выбирайте осознанно.
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, поддерживаются, но не рекомендуются для нового кода (нет хуков, сложнее оптимизация,thisbinding). - Компонент — чистая функция:
(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 / useDeferredValue | Concurrent Features: пометка обновлений как низкоприоритетных. |
Д. Контекст (Context API) & Prop Drilling
- Механизм передачи данных через дерево без явного пробрасывания пропсов на каждом уровне.
- Кейсы: Тема (Theme), Авторизация (User), i18n, Настройки.
- Антипаттерн: Класть в контекст часто меняющиеся данные (каждый
Providervalue change -> ре-рендер всех консьюмеров). Решение: разделение контекстов,useMemoдля value, атомарные библиотеки (Jotai, Recoil, Zustand).
Е. Жизненный цикл (Functional Component Mental Model)
Нет componentDidMount/Update/Unmount. Мы мыслим в терминах Эффектов синхронизации (useEffect):
- Mount: Рендер -> Commit -> Layout Effects (
useLayoutEffect) -> Paint -> Effects (useEffect). - Update: Пропсы/Стейт меняются -> Рендер -> Commit -> Layout Effects Cleanup -> Layout Effects -> Paint -> Effects Cleanup -> Effects.
- Unmount: Layout Effects Cleanup -> Effects Cleanup.
3. Производительность и Оптимизация (Performance)
React.memo(Component)— HOC для пропуска рендера, если пропсы не изменились (shallow compare). АналогPureComponent.useMemo(() => value, deps)— кэширует результат вычислений.useCallback(fn, deps)— кэширует ссылку на функцию (важно для пропсов детей, обернутых вmemo, и зависимостейuseEffect).- Виртуализация списков —
react-window,@tanstack/react-virtual(рендерить только видимое). - Code Splitting / Lazy Loading —
React.lazy(() => import(...))+<Suspense fallback={<Spinner />}>. Уменьшает Initial Bundle. - 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 Fetching | TanStack 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
- Нет
useEffectдля синхронизации состояния (derived state ->useMemo/ просто переменная во время рендера). - Нет
useEffectдля data fetching без очистки (race conditions) -> TanStack Query / SWR. - Стабильность ссылок: Колбэки в пропсах ->
useCallback. Объекты/массивы в пропсах/контексте ->useMemo. - Избегайте Prop Drilling -> Composition (
children/ slots) или Context / State Management. - Ключи (
key) — стабильные, уникальные ID из БД. НЕindex, НЕMath.random(). - Строгая типизация:
strict: trueв TS. Пропсы —interface Props. Избегайтеany. - Доступность (a11y): Семантический HTML,
aria-*, фокус-менеджмент,eslint-plugin-jsx-a11y. - Ошибки:
Error Boundary(классовый компонент) для graceful degradation UI.react-error-boundaryбиблиотека. - Bundle Size: Аудит (
vite-bundle-analyzer,next/bundle-analyzer). Динамические импорты для тяжелых фич (Charts, Editors, Modals). - Миграция на 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, {})
]
});
}
Ключевые моменты трансформации:
- Теги -> Вызовы функций:
<Tag />→_jsx('Tag', props)или_jsxs('Tag', props)(оптимизация для статических детей). - Атрибуты -> Объект props:
className="x"→{ className: 'x' }.onClick={fn}→{ onClick: fn }. - Интерполяция
{expr}→ Прямая подстановка выраженияexprв пропсы/children. - Компоненты (PascalCase):
<CustomIcon />→_jsx(CustomIcon, {})(передается ссылка на функцию/класс, а не строка). - 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):
- Dev:
src/App.tsx→ esbuild/SWC (трансформация TSX->JS, ~мс) → ESM модули → Браузер (Native ESM + HMR). - Prod:
src/App.tsx→ SWC/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>
Почему это НЕ для продакшена:
- Производительность: Транспиляция на клиенте = задержка TTI (Time to Interactive). Парсинг JSX + компиляция + выполнение.
- Размер бандла:
babel.min.jsвесит ~1-2 МБ (gzipped ~300-400 КБ) — только для компилятора. - Безопасность:
eval/new Functionиспользуются для исполнения сгенерированного кода (CSP issues). - Отсутствие оптимизаций: Нет 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). - Оптимизируют
_jsxsvs_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)
- Runtime:
"jsx": "react-jsx"(TS) /"runtime": "automatic"(Babel) — Автоматический рантайм (React 17+). Не импортируйтеReactвручную. - Трансформер: SWC (в Next.js, Vite 5+, Turbopack) или esbuild (Vite dev) — для скорости. Babel — только если нужны специфические плагины (macro, styled-components, relay), которых нет в SWC.
- TS Config:
"jsx": "react-jsx"+"jsxImportSource": "react"(или@emotion/reactи т.д.). - React Refresh: Настроен HMR (Fast Refresh) для сохранения стейта при редактировании компонентов (встроено в Vite/Next/CRA).
- 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) — Рождение компонента
Порядок вызова при первом рендере:
constructor(props)- Инициализация
this.state, биндинг обработчиков (this.handleClick = this.handleClick.bind(this)). - Единственное место, где можно присваивать
this.stateнапрямую. - Вызов
super(props)обязателен, если нуженthis.propsв конструкторе.
- Инициализация
— УСТАРЕЛ. Вызывался перед рендером. Не используйте.UNSAFE_componentWillMount()render()— Обязательный. Чистая функция. Возвращает React Element / null / string / number / array / portal. Не должен мутировать стейт.componentDidMount()— Главный метод фазы.- Вызывается после коммита в DOM (после
useLayoutEffectв хуках). - Кейсы: Подписки (WebSocket, EventListeners), запуск анимаций,
fetchданных (хотя лучше вuseEffect/библиотеках), работа с DOM-нодами (ref.current), интеграция с не-React библиотеками (D3, Chart.js, Mapbox).
- Вызывается после коммита в DOM (после
2. Фаза Обновления (Updating) — Реакция на пропсы/стейт
Вызывается при setState, forceUpdate или изменении пропсов родителя.
— УСТАРЕЛ. Источник багов при ререндерах родителя.UNSAFE_componentWillReceiveProps(nextProps)static getDerivedStateFromProps(props, state)— Static метод.- Вызывается перед каждым рендером (и при монтировании, и при обновлении).
- Назначение: Синхронизация стейта с пропсами (редкий кейс: controlled -> uncontrolled переходы).
- Возврат: Объект для обновления стейта или
null. - Нет доступа к
this! Только чистая логикаprops -> state.
shouldComponentUpdate(nextProps, nextState)— Оптимизация.- Возврат
falseпропускаетrender,componentDidUpdateи ререндер детей (но не предотвращает рендер детей, если они неPureComponent/memo). - По умолчанию
true. Используется для ручной оптимизации (глубокое сравнение). - Аналог:
React.PureComponent(shallow compare) /React.memo.
- Возврат
— УСТАРЕЛ.UNSAFE_componentWillUpdate(nextProps, nextState)render()getSnapshotBeforeUpdate(prevProps, prevState)— Редко, но важно.- Вызывается прямо перед коммитом в DOM (после
render, до мутаций DOM). - Возврат: Любое значение (
snapshot), передается 3-м аргументом вcomponentDidUpdate. - Кейс: Захват скролл-позиции, размеров элементов (
el.getBoundingClientRect()) перед обновлением DOM, чтобы восстановить их после.
- Вызывается прямо перед коммитом в DOM (после
componentDidUpdate(prevProps, prevState, snapshot)— Главный метод обновления.- Вызывается после коммита изменений в DOM.
- Кейсы: Сетевые запросы при изменении пропсов (сравнивая
prevProps), работа с DOM, логирование, синхронизация со сторонними либами. - Обязательно оборачивать
setStateв условие сравнения, иначе бесконечный цикл.
3. Фаза Размонтирования (Unmounting) — Смерть компонента
componentWillUnmount()— Главный метод очистки.- Вызывается прямо перед удалением из DOM.
- Кейсы (Cleanup):
- Очистка таймеров (
clearTimeout,clearInterval). - Отписка от событий (
removeEventListener,ws.close(),subscription.unsubscribe()). - Отмена сетевых запросов (
abortController.abort()). - Очистка внешних библиотек (уничтожение экземпляров D3, Chart.js, Mapbox).
- Очистка таймеров (
4. Фаза Обработки Ошибок (Error Boundaries) — Ловля ошибок рендера
Только для Классовых компонентов. Превращают компонент в "Error Boundary".
static getDerivedStateFromError(error)— Static.- Вызывается при ошибке в поддереве (дети).
- Назначение: Обновить стейт для показа фоллб UI (например,
this.setState({ hasError: true })). - Не имеет доступа к
this.
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 Method | Hooks Equivalent | Нюансы |
|---|---|---|
constructor | useState / useRef (lazy init) | useRef для mutable значений без ре-рендера. |
componentDidMount | useEffect(() => { ... }, []) | Эффект с пустым массивом зависимостей. Запускается после paint (асинхронно). |
componentDidUpdate | useEffect(() => { ... }, [deps]) | Зависимости — ключ. Сравнение Object.is. |
componentWillUnmount | useEffect(() => { return () => { cleanup } }, []) | Функция очистки (cleanup) в возврате эффекта. |
getSnapshotBeforeUpdate | useLayoutEffect(() => { ... }, [deps]) | Синхронно после мутаций DOM, до paint. Для измерений. |
componentDidCatch / getDerivedStateFromError | Нет прямого аналога | Error Boundaries остаются только классовыми. Оборачивайте дерево в классовый EB. |
shouldComponentUpdate | React.memo(Comp, areEqual?) / useMemo / useCallback | memo — shallow compare пропсов. Кастомный компаратор — 2-й аргумент. |
PureComponent | React.memo | Аналог shallow compare. |
8. Best Practices для Legacy Class Components (Code Review Checklist)
- Никакой логики в
render— только чистый вывод. setStateтолько функционально:this.setState(prev => ({ count: prev.count + 1 })).- Биндинг в конструкторе или Property Initializers (Stage 3):
handleClick = () => {}(стрелочные свойства класса — авто-биндингthis). - Очистка в
componentWillUnmount— обязательна для любогоaddEventListener,setInterval, подписок. getDerivedStateFromProps— крайняя мера. Часто задачу решает "Lifting State Up" илиkeyпроп для ресета компонента.- Error Boundary — обязателен на корне приложения и вокруг виджетов/фич, чья ошибка не должна ломать весь экран.
- Миграция: Не переписывайте рабочие классовые компоненты "для красоты". Пишите новые на хуках. При рефакторинге фичи — конвертируйте.
Резюме (Cheatsheet для собеседования)
> Фазы: Mount → Update → Unmount → Error Handling.
>
> Must Know методы:
> * Mount: constructor → render → componentDidMount (сайд-эффекты, подписки).
> * Update: getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate (реакция на изменения).
> * 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 Events | window.addEventListener('resize', this.handleResize)<br>document.addEventListener('click', this.handleClick, true) | window.removeEventListener('resize', this.handleResize)<br>document.removeEventListener('click', this.handleClick, true) |
| WebSockets / SSE | this.ws = new WebSocket(url);<br>this.es = new EventSource(url); | this.ws.close()<br>this.es.close() |
| Подписки на Store/Event Bus | this.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 / MutationObserver | this.observer = new IntersectionObserver(cb) | this.observer.disconnect() |
| requestAnimationFrame | this.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).
Child.componentWillUnmount()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):
- React прервет размонтирование текущего компонента.
- Ошибка всплывет к ближайшему Error Boundary (
componentDidCatch). - Остальные
componentWillUnmountв дереве (родители) НЕ ВЫПОЛНЯТСЯ.
- Вывод: Оборачивайте чистку в
try/catch, если вызываете ненадежный сторонний код (chart.destroy()может бросить ошибку, если уже уничтожен).
5. Антипаттерны (Чего НЕ делать)
- Забывать
bind/ стрелочные функции для слушателей:// ПЛОХО: removeEventListener не сработает, т.к. ссылка на функцию другаяcomponentDidMount() { window.addEventListener('click', () => this.handleClick()); }componentWillUnmount() { window.removeEventListener('click', () => this.handleClick()); }// ХОРОШО: Сохраняем ссылкуhandleClick = () => { ... } // Class field arrow functioncomponentDidMount() { window.addEventListener('click', this.handleClick); }componentWillUnmount() { window.removeEventListener('click', this.handleClick); } - Чистить состояние (
setState) вместо ресурсов. Стейт умрет с компонентом сам. - Использовать для отмены анимаций CSS/Transition. Для этого есть
TransitionGroup/react-transition-groupилиonExitedколбэки. - Дублировать логику в
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 Method | useEffect Pattern | Когда запускается Cleanup / Effect |
|---|---|---|
componentDidMount | useEffect(() => { ... }, []) | Mount: Effect после рендера.<br>Unmount: Cleanup при размонтировании. |
componentDidUpdate | useEffect(() => { ... }, [dep1, dep2]) | Update: Cleanup (предыдущие deps) → Effect (новые deps) при изменении deps. |
componentWillUnmount | useEffect(() => { return () => { ... } }, []) | Unmount: Cleanup при размонтировании (effect пустой или только cleanup). |
getSnapshotBeforeUpdate | useLayoutEffect (см. ниже) | Синхронно после мутаций DOM, до paint. |
3. Детальная механика: Фазы выполнения (React 18+ / Concurrent Features)
Понимание порядка критично для избежания багов (race conditions, утечки памяти).
А. Монтирование (Mount)
- Рендер (Render Phase) — компонент выполняется, создается новый UI.
- Commit Phase — React обновляет DOM.
- Layout Effects (
useLayoutEffect) — синхронно, до браузерного paint. - Browser Paint — пользователь видит UI.
- Passive Effects (
useEffect) — асинхронно, после paint (не блокирует отрисовку).
Б. Обновление (Update) — при изменении зависимостей
- Рендер (новый стейт/пропсы).
- Cleanup предыдущего эффекта (вызывается синхронно перед Layout Effects нового рендера!).
- Commit Phase (DOM обновлен).
- Layout Effects (новые).
- Paint.
- Effect (новый) — асинхронно.
В. Размонтирование (Unmount)
- Cleanup последнего эффекта (синхронно, перед удалением DOM).
- Удаление DOM-узлов.
- 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
setState—dispatch,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]);
Разница:
| Характеристика | useEffect | useLayoutEffect |
|---|---|---|
| Тайминг | Асинхронно, после 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) на собеседованиях
- Забытый
returncleanup → Утечка памяти, двойные подписки,setStateна размонтированном. - Неполный
depsмассив (или его отсутствие) → Бесконечные циклы, стёртые инпуты, лишние запросы. ESLintexhaustive-deps— закон. - Асинхронная функция прямо в
useEffect:// ОШИБКА: async функция возвращает Promise, React думает, что это cleanup функция!useEffect(async () => { await fetch(); }, []);// ПРАВИЛЬНО: IIFE или внутренняя функцияuseEffect(() => { (async () => { await fetch(); })(); }, []); - Чтение
state/propsв эффекте без депы (Stale Closure) → Видят старые значения.- Решение: Добавить в deps, или использовать функциональное обновление
setState(prev => ...), илиuseRefдля актуального значения без ре-рендеров.
- Решение: Добавить в deps, или использовать функциональное обновление
- Сложная логика в одном
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 Ref | ref={(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)
- Использование рефа там, где нужен стейт/пропс.
- Пример:
ref.current = newValueи ожидание ре-рендера. Не сработает. - Фикс:
useState/useReducer.
- Пример:
- Проброс рефа на кастомный компонент без
forwardRef.- Ошибка:
Warning: Function components cannot be given refs.
- Ошибка:
- Мутация
ref.currentв теле компонента (Render Phase).- Ломает Time Slicing, Suspense, Strict Mode. Только в
useEffect/useLayoutEffect/хендлерах.
- Ломает Time Slicing, Suspense, Strict Mode. Только в
- Измерение DOM в
useEffect(после Paint) вместоuseLayoutEffect.- Вызывает визуальное мерцание (Layout Shift).
useLayoutEffect— синхронно до Paint.
- Вызывает визуальное мерцание (Layout Shift).
- Хранение в рефе объектов, которые должны быть стабильны для
useEffectdeps.const objRef = useRef({ a: 1 });— объект мутируем, но ссылка стабильна. Если кладете в depsuseEffect(() => {}, [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 → onChange → setState → Новый 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.
- Решения:
- Локализация стейта — выносить инпут в отдельный компонент
MemoizedInput. useDeferredValue(React 18) — откладывает обновление «тяжелой» части UI, инпут остается быстрым.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) — это функция, которая удовлетворяет хотя бы одному из двух условий:
- Принимает одну или более функций в качестве аргументов (колбэки).
- Возвращает функцию как результат своей работы.
Это возможно благодаря тому, что в 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 Hooks | HOF, возвращающая хук (функцию) с логикой. | 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 Handlers | onClick={() => handleClick(id)} — стрелочная функция создает замыкание над id. | <button onClick={e => onDelete(e, item.id)} /> |
| Array Methods в JSX | .map(), .filter() прямо в рендере. | {items.filter(active).map(renderItem)} |
5. Зачем это нужно? (Бизнес-ценность для Senior/Lead)
- DRY (Don't Repeat Yourself) — Выносим общую инфраструктуру (ретраи, логирование, кэш, обработку ошибок) в HOF. Бизнес-логика остается чистой.
- Composition over Inheritance — Комбинируем поведение (
memoize(withLogging(fn))) вместо создания иерархий классов. - Abstraction Level — Скрываем «как» (итерация, запрос, кэш), показываем «что» (предикат, трансформация).
- Lazy Evaluation / Partial Application — Готовим функцию заранее (при старте), выполняем быстро (в хот-пути).
- 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. |
Глубокое клонирование аргументов в memoize | JSON.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 / some | boolean | Нет |
| Просто перебрать (сайд-эффекты) | forEach | undefined | Нет (но колбэк может) |
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. Типичные ошибки на собеседовании
- Мутация в
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}` })); - Забытый
returnвreduce:// ПЛОХО: acc становится undefined после первой итерации[1,2].reduce((acc, n) => { acc + n; }, 0);// ХОРОШО[1,2].reduce((acc, n) => acc + n, 0); - Отсутствие
initialValueвreduce:- На пустом массиве:
TypeError: Reduce of empty array with no initial value. - На массиве объектов без initialValue:
accстановится первым объектом, ломая логику (например,acc + n->[object Object]1).
- На пустом массиве:
- Использование
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:
- Read-only: Никогда не мутируйте
props(props.items.push()— запрещено). - Serialization: В идеале props должны быть сериализуемы (примитивы, объекты, массивы, функции-колбэки) для работы DevTools, SSR, RSC.
- 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:
- Локальность: State инкапсулирован внутри компонента. Родитель не имеет прямого доступа к
countиsetCount(если не проброшен через props). - Иммутабельность обновления: Не мутируйте state напрямую (
state.items.push()— не вызовет рендер). Всегда создавайте новые ссылки:setItems([...items, newItem]). - Минимизация: Храните в state только то, что нельзя вычислить из props или других state (derived state — антипаттерн).
- Колоизация (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 роутер:
- Берет дерево маршрутов (
<Routes><Route path="..." />...</Routes>). - Рекурсивно сравнивает сегменты URL с
pathмаршрутов (используяpath-to-regexpили свой парсер в v6). - Находит наиболее конкретное совпадение (Longest Prefix Match).
- Формирует массив
matches(данные о параметрах:id, данныхloader/action, элементе компонента). - Триггерит ре-рендер
<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>, но перехватывает onClick → navigator.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 Router | Type-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 роутер:
- Берет дерево маршрутов (
<Routes><Route path="..." />...</Routes>). - Рекурсивно сравнивает сегменты URL с
pathмаршрутов (используяpath-to-regexpили свой парсер в v6). - Находит наиболее конкретное совпадение (Longest Prefix Match).
- Формирует массив
matches(данные о параметрах:id, данныхloader/action, элементе компонента). - Триггерит ре-рендер
<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>, но перехватывает onClick → navigator.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 Router | Type-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
-
min-width: 0/min-height: 0для флекс-детей. По умолчанию флекс-элемент не может быть уже своего контента (min-width: auto). Если внутри есть длинный текст сtext-overflow: ellipsisили вложенный скролл — обязательно ставьтеmin-width: 0(дляrow) илиmin-height: 0(дляcolumn). -
flex-shrink: 0для фиксированных элементов. Иконки, аватары, кнопки — всегда ставьтеflex-shrink: 0(илиflex: 0 0 auto), иначе они сплющатся на мобильных. -
Не используйте
margin: autoдля центрирования во flex — используйтеjustify-content/align-items. Ноmargin-left: autoу последнего элемента — валидный паттерн для «прижатия к правому краю» (divider pattern). -
Производительность: Flexbox быстрее Grid для одномерных задач. Не оборачивайте всё в
divради флекса — используйтеdisplay: contentsу промежуточных оберток, чтобы дети участвовали в флекс-раскладке родителя напрямую. -
Gap vs Margin:
gapне работает в старых браузерах (IE11, старый Safari) без полифилов/префиксов. В продакшене 2024+ — безопасно везде.
Резюме (Cheatsheet)
| Задача | Свойство | Значение |
|---|---|---|
| Сделать контейнер флексом | display | flex / inline-flex |
| Направление | flex-direction | row / column |
| Перенос | flex-wrap | wrap |
| Горизонталь (main axis) | justify-content | center / space-between / space-around |
| Вертикаль (cross axis) | align-items | center / stretch / flex-start |
| Отступы между | gap | 16px |
| Элемент: расти/сжиматься/база | flex | 1 / auto / 0 0 200px |
| Элемент: не сжиматься | flex-shrink | 0 |
| Элемент: свой align | align-self | center |
> 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) (предположения):
- Разные типы корневых элементов → Полная замена.
// Было<div><App /></div>// Стало<span><App /></span>// React: unmount div + children → mount span + children. State потерян.
- Один тип элемента → Обновление пропсов/атрибутов.
<div className="old" /> → <div className="new" style={{color: 'red'}} />// React: меняет только className и style. Рекурсивно диффчит children.
- Списки элементов →
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 Reflow | 1 Diff + 1 Reflow | VDOM медленнее (накладные расходы на JS). |
| Много изменений (батч) | N Reflows | 1 Diff + 1 Reflow | VDOM выигрывает (батчинг обновлений). |
| Сложные вычисления | Блокирует Main Thread | Time-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 JS | Astro, 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.
