РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ / Middle - Senior FRONTEND разработчик - ГАЗПРОМ
Сегодня мы разберем живое техническое собеседование по фронтенду, в котором кандидат уверенно проходит базовые темы по HTML, CSS, JavaScript и React, демонстрируя понимание ключевых концепций: семантика, области видимости, замыкания, event loop, hooks и оптимизация рендера. По ходу практических заданий он успешно находит и исправляет логические баги в коде, показывает умение рассуждать про state, эффекты и обработку событий, хотя в отдельных моментах заметна лёгкая неуверенность в деталях и продвинутых инструментах оптимизации.
Вопрос 1. Что такое семантическая разметка HTML и какую задачу она решает?
Таймкод: 00:00:50
Ответ собеседника: неполный. Семантика облегчает парсинг HTML и помогает поисковым системам корректно распознавать структуру и содержимое страницы.
Правильный ответ:
Семантическая разметка HTML — это использование тегов, которые описывают смысл и роль содержимого, а не только его визуальное оформление. Вместо универсальных контейнеров вроде div и span применяются элементы, явно отражающие структуру документа и назначение блоков контента.
Ключевая идея: каждый блок на странице должен быть размечен так, чтобы по одному лишь HTML коду было понятно, что это за содержимое — навигация, основной контент, статья, сайдбар, футер, заголовок раздела и т.д.
Основные семантические элементы:
header— шапка страницы или секции.nav— блок навигации.main— основной уникальный контент страницы.section— логический раздел документа.article— независимый, самодостаточный блок (новость, пост, комментарий, документация).aside— дополнительная информация, сайдбар, контекстные блоки.footer— подвал страницы или секции.h1–h6— заголовки уровней, задают иерархию документа.figure/figcaption— медиа с подписью.time— временные метки.mark,strong,em,codeи др. — уточняют смысл текста (акцент, важность, код и т.п.).
Задачи, которые решает семантическая разметка:
-
Структурированность и понятность документа
- Код становится самодокументируемым: по HTML можно понять структуру и назначение блоков без CSS.
- Упрощает поддержку, рефакторинг и командную разработку — меньше "магических"
divбез смысла.
-
Доступность (a11y)
- Скринридеры и другие assistive-технологии используют семантические теги и заголовки для построения карты документа.
- Пользователь с ограничениями может быстро переходить между
nav,main,article, заголовками, не прослушивая весь контент подряд. - Улучшает UX для клавиатурной навигации.
-
SEO и богатое индексирование
- Поисковые системы легче определяют:
- где основной контент (
main,article), - где навигация (
nav), - где второстепенная информация (
aside).
- где основной контент (
- Это повышает качество индексации, понимание тематики и потенциально влияет на ранжирование.
- Семантика — основа для корректного использования структурированных данных (schema.org и др.)
- Поисковые системы легче определяют:
-
Машиночитаемость и интеграции
- Семантически правильный HTML легче парсить не только поисковикам, но и любым сервисам:
- парсеры новостей,
- превью ссылок,
- скрейперы,
- внутренние инструменты аналитики.
- Это уменьшает количество хрупких "костылей", завязанных на классы и произвольные структуры.
- Семантически правильный HTML легче парсить не только поисковикам, но и любым сервисам:
-
Лучшая основа для стилей и компонентного подхода
- Явная структура позволяет проще строить дизайн-систему и компонентные библиотеки.
- Семантические элементы можно комбинировать с BEM/utility-классами без потери смысла.
Пример (плохой/нейтральный vs семантический):
Некачественный вариант:
<div class="header">
<div class="menu">...</div>
</div>
<div class="content">
<div class="block">
<h1>Статья</h1>
<p>Текст...</p>
</div>
</div>
<div class="footer">
© 2025
</div>
Семантический вариант:
<header>
<nav>
<!-- ссылки навигации -->
</nav>
</header>
<main>
<article>
<header>
<h1>Статья</h1>
<p>Опубликовано: <time datetime="2025-11-08">8 ноября 2025</time></p>
</header>
<p>Текст статьи...</p>
</article>
</main>
<footer>
<p>© 2025</p>
</footer>
Итог: семантическая разметка — это про смысл, структуру и доступность. Она делает HTML понятным как для людей, так и для машин, улучшает поддержку, SEO, доступность и качество фронтенда в целом.
Вопрос 2. Приведи примеры семантических HTML-элементов.
Таймкод: 00:01:24
Ответ собеседника: неполный. Говорит, что есть стандартные семантические блоки, но не перечисляет их.
Правильный ответ:
Семантические HTML-элементы — это теги, которые явно описывают смысл и роль содержимого, а не его визуальное представление. Они помогают структурировать документ, повышают доступность и улучшают понимание страницы поисковыми системами и парсерами.
Ключевые примеры:
-
html,head,body
Базовая структура документа, задающая корень и разделение метаинформации и содержимого. -
header
Верхний блок страницы или секции: логотип, название, основная навигация, вводная информация. -
nav
Область навигации: меню, ссылки на основные разделы, пагинация. Используется для структурированных навигационных элементов, а не для любых ссылок подряд. -
main
Основной контент страницы, уникальный для данного URL. Должен быть один на страницу. Исключает повторяющиеся элементы (меню, футер, сайдбар и т.п.). -
section
Логический раздел документа, объединенный общей смысловой темой. Обычно содержит собственный заголовок (h2,h3и т.д.). Используется, когда раздел можно назвать. -
article
Самодостаточный фрагмент контента, независимый и потенциально пригодный для отдельного распространения:- пост в блоге,
- новостная заметка,
- карточка товара,
- комментарий/отзыв,
- часть документации.
-
aside
Блок с дополнительной, контекстной информацией:- сайдбар,
- блок "похожие материалы",
- рекламные вставки,
- вспомогательные ссылки.
-
footer
Нижняя часть страницы или секции: копирайт, ссылки, контактная информация, доп. навигация. -
h1–h6
Семантические заголовки уровней, формирующие иерархию документа. -
p
Параграф текста — смысловая текстовая единица. -
ul,ol,li
Списки, отражающие структурированное перечисление элементов. -
figureиfigcaption
Медиа/иллюстрация/диаграмма с подписью. Связывают изображение и текст на семантическом уровне. -
time
Семантика даты и времени, особенно с атрибутомdatetime. -
strong,em
Семантическое выделение важности (strong) и акцента (em), а не просто визуальное жирное/курсив. -
code,pre,kbd,samp
Для представления кода, вводов/выводов, технических примеров (важно в документации и технических статьях).
Краткий пример связанного использования:
<header>
<h1>Документация сервиса</h1>
<nav>
<ul>
<li><a href="/docs/api">API</a></li>
<li><a href="/docs/sdk">SDK</a></li>
</ul>
</nav>
</header>
<main>
<article>
<header>
<h2>Аутентификация по токену</h2>
<p>Обновлено: <time datetime="2025-11-08">8 ноября 2025</time></p>
</header>
<p>Для доступа к API используйте заголовок:</p>
<pre><code>Authorization: Bearer <token></code></pre>
</article>
<aside>
<p>Полезно: проверьте срок жизни токена перед вызовом.</p>
</aside>
</main>
<footer>
<p>© 2025 MyService</p>
</footer>
Такой набор элементов делает документ структурированным, предсказуемым и удобным для анализа как людьми, так и машинами.
Вопрос 3. Какой элемент должен быть непосредственным потомком списка UL?
Таймкод: 00:01:40
Ответ собеседника: правильный. Внутри ul на первом уровне должны быть элементы li.
Правильный ответ:
Элемент неупорядоченного списка ul по спецификации должен содержать в качестве непосредственных потомков элементы списка li (а также допустимы вложенные скрипты/темплейты, но не произвольные блочные элементы напрямую).
Корректная структура:
- Каждый логический пункт списка — это отдельный
li. - Если внутри одного пункта нужен подсписок, он вкладывается внутрь соответствующего
li, а не напрямую вul.
Примеры корректного HTML:
<ul>
<li>Пункт 1</li>
<li>Пункт 2</li>
<li>
Пункт 3 с подсписком
<ul>
<li>Подпункт 3.1</li>
<li>Подпункт 3.2</li>
</ul>
</li>
</ul>
Некорректные варианты (так делать нельзя):
<ul>
<div>Пункт 1</div> <!-- Ошибка: div не может быть прямым потомком ul -->
Текст без li <!-- Ошибка: текст вне li -->
<ul><li>Вложенный</li></ul> <!-- Ошибка: ul не должен быть прямым потомком ul -->
</ul>
Зачем это важно:
- Соответствие спецификации HTML.
- Корректная работа скринридеров и других assistive-технологий.
- Предсказуемое поведение стилевых правил и селекторов.
- Правильное восприятие структуры документа браузерами и парсерами.
Вопрос 4. Какое CSS-свойство задаёт внешний отступ элемента и как ведут себя вертикальные отступы двух соседних блоков 10px и 20px друг под другом?
Таймкод: 00:01:53
Ответ собеседника: правильный. Указал margin для внешних отступов и корректно описал схлопывание: остаётся больший отступ.
Правильный ответ:
Для задания внешних отступов используется свойство margin (и его производные: margin-top, margin-right, margin-bottom, margin-left). Оно определяет расстояние снаружи границ элемента до соседних элементов или краёв контейнера.
Поведение двух вертикальных отступов у соседних блочных элементов (margin collapsing):
- Когда один блок с
margin-bottom: 10pxрасположен над другим блоком сmargin-top: 20px, их вертикальные отступы не суммируются. - Вместо 30px произойдет схлопывание (collapsing margins), и реальный интервал между элементами будет равен максимальному из двух значений.
- В данном примере расстояние станет 20px.
Пример:
<div class="a">Блок A</div>
<div class="b">Блок B</div>
.a {
margin-bottom: 10px;
background: lightblue;
}
.b {
margin-top: 20px;
background: lightgreen;
}
Реальный зазор между Блок A и Блок B будет 20px.
Ключевые моменты про схлопывание вертикальных margin:
- Схлопываются только вертикальные отступы (top/bottom) блочных элементов в нормальном потоке.
- Не схлопываются:
- горизонтальные margin (left/right),
- margin, разделённые границей, паддингом или внутренним контентом между элементами,
- если элементы находятся в разных форматирующих контекстах (например,
overflow: auto/hidden,display: flex/grid,position: absoluteи т.п.).
- У вложенных блоков margin-top дочернего может схлопнуться с margin-top родителя, если нет padding/border/content между ними.
Понимание механики схлопывания важно для предсказуемой верстки и избежания "мистических" отступов, особенно в сложных макетах.
Вопрос 5. Что произойдет с вертикальными отступами между блоками, если их родителю задать display: flex?
Таймкод: 00:02:19
Ответ собеседника: правильный. Указывает, что при display: flex у родителя схлопывания вертикальных отступов не будет.
Правильный ответ:
При установке родителю display: flex (или display: inline-flex) механика схлопывания вертикальных margin между дочерними элементами меняется радикально:
- Flex-контейнер создает новый контекст форматирования.
- Дочерние элементы становятся flex-элементами.
- Для flex-элементов стандартное "collapsing margins" (схлопывание вертикальных отступов) между собой и с родителем не применяется.
Практически это означает:
- Если у первого блока
margin-bottom: 10px, а у второгоmargin-top: 20px, и оба являются дочерними элементами flex-контейнера, расстояние между ними будет:- либо суммой (10px + 20px = 30px), либо
- визуально зависеть от направления flex-оси, но критично: схлопывания до одного максимального значения не произойдет.
- Каждый
marginработает самостоятельно вдоль выбранной оси.
Простой пример:
<div class="parent">
<div class="a">Блок A</div>
<div class="b">Блок B</div>
</div>
Без flex (обычный блочный поток):
.parent {
/* стандартный блочный контейнер */
}
.a {
margin-bottom: 10px;
background: lightblue;
}
.b {
margin-top: 20px;
background: lightgreen;
}
Расстояние между A и B: 20px (схлопывание: берется максимум).
С flex-контейнером:
.parent {
display: flex;
flex-direction: column; /* вертикальное расположение */
}
.a {
margin-bottom: 10px;
}
.b {
margin-top: 20px;
}
Расстояние между A и B: 30px (10px + 20px, без схлопывания).
Ключевые моменты:
- Flex и Grid-контейнеры отключают стандартную модель схлопывания вертикальных отступов между дочерними элементами.
- Это делает поведение отступов более предсказуемым в современных макетах: каждый
marginу flex-элемента реально учитывается. - Поэтому при переходе от классической блочной верстки к flex/grid нужно помнить, что визуальные расстояния изменятся, если код полагался на схлопывание margin.
Вопрос 6. В чём основное отличие flexbox от grid при построении раскладки?
Таймкод: 00:02:39
Ответ собеседника: правильный. Указывает, что flex ориентирован на одно направление, а grid — на двумерную раскладку по строкам и столбцам.
Правильный ответ:
Основное концептуальное отличие:
- Flexbox — инструмент для одномерной раскладки (одна ось: либо строка, либо колонка).
- Grid — инструмент для двумерной раскладки (одновременно строки и колонки, с явным управлением сеткой).
Подробнее.
Flexbox:
- Оперирует главным направлением (main axis) и поперечным (cross axis).
- Основной сценарий:
- выравнивание элементов в строку или колонку,
- управление промежутками,
- перераспределение свободного пространства,
- адаптация к динамическому количеству элементов.
- Раскладка элементов строится "от контента": flex-элементы идут по порядку, занимают место, переносятся (при
flex-wrap) и распределяют свободное пространство по заданным правилам. - Удобен для:
- панелей навигации,
- горизонтальных/вертикальных списков,
- групп кнопок,
- карточек с адаптивной шириной,
- выравнивания элементов по центру/краям/пространству.
Пример:
.nav {
display: flex;
justify-content: space-between;
align-items: center;
}
Grid:
- Оперирует явной сеткой: строки, колонки, явные/неявные треки.
- Позволяет управлять раскладкой по двум измерениям одновременно:
- элемент может быть размещён в конкретной ячейке (row/column),
- может растягиваться на несколько колонок и/или строк.
- Раскладка строится "от контейнера": сначала определяем сетку, затем позиционируем в неё элементы.
- Удобен для:
- сложных макетов страниц,
- панелей с чёткой геометрией,
- дашбордов,
- таблиц карточек с выравниванием по строкам и колонкам.
Пример:
.layout {
display: grid;
grid-template-columns: 200px 1fr 300px;
grid-template-rows: auto 1fr auto;
gap: 16px;
}
/* Можно явно указать, куда что ставить */
.header {
grid-column: 1 / 4;
grid-row: 1;
}
.sidebar {
grid-column: 1;
grid-row: 2;
}
.content {
grid-column: 2;
grid-row: 2;
}
.extra {
grid-column: 3;
grid-row: 2;
}
.footer {
grid-column: 1 / 4;
grid-row: 3;
}
Практические выводы:
- Если задача — расположить элементы в линию, распределить пространство, выровнять по одной оси, использовать автоадаптивное поведение → чаще flex.
- Если нужна полноценная сетка, сложная геометрия, контроль строк/колонок и взаимосвязи элементов в двух измерениях → использовать grid.
- В реальных проектах оба подхода часто комбинируют:
- grid для общей схемы страницы,
- flex внутри отдельных блоков (хедер, карточка, панель действий).
Вопрос 7. Какой механизм в CSS используется для задания разных стилей под различные размеры экранов при адаптивной верстке?
Таймкод: 00:03:11
Ответ собеседника: правильный. Указывает использование media-запросов.
Правильный ответ:
Для адаптивной верстки под разные размеры экранов, плотность пикселей и характеристики устройства используется механизм CSS media-запросов (@media).
Media-запросы позволяют:
- применять разные наборы стилей в зависимости от:
- ширины/высоты окна (
width,height,min-width,max-width), - ориентации (
orientation), - плотности пикселей (
resolution), - типа носителя (
screen,print) и других условий;
- ширины/высоты окна (
- реализовать mobile-first или desktop-first подход без дублирования разметки;
- тонко контролировать поведение сеток (flex, grid), типографики, отступов, отображения/скрытия элементов.
Базовый синтаксис:
@media (max-width: 768px) {
.sidebar {
display: none;
}
.content {
padding: 12px;
}
}
Mobile-first (рекомендуемый подход):
- Сначала пишем стили для мобильных устройств (наименьшая ширина).
- Затем добавляем
min-widthмедиа-запросы для более широких экранов.
/* Базовые стили для мобильных */
.container {
display: flex;
flex-direction: column;
}
/* Планшеты и шире */
@media (min-width: 768px) {
.container {
flex-direction: row;
}
}
/* Десктопы и шире */
@media (min-width: 1200px) {
.container {
gap: 24px;
}
}
Дополнительные примеры условий:
- По ориентации:
@media (orientation: landscape) {
.header {
height: 60px;
}
}
- По плотности пикселей (ретина):
@media (min-resolution: 2dppx) {
.logo {
background-image: url('logo@2x.png');
}
}
Ключевые моменты:
- Media-запросы должны быть логичны и непротиворечивы, важно избегать "магических чисел" без явных дизайн-брейкпоинтов.
- В связке с семантикой, flex/grid и относительными единицами (%, rem, vw/vh) media-запросы образуют основу качественной, предсказуемой адаптивной верстки.
Вопрос 8. В чём отличие var от let и const с точки зрения области видимости?
Таймкод: 00:03:48
Ответ собеседника: неполный. Указывает, что var имеет функциональную область видимости, а let и const — блочную, но не раскрывает отличия между let и const, hoisting и прочие особенности.
Правильный ответ:
Ключевое отличие между var, let и const связано с областью видимости (scoping), поведением при поднятии (hoisting) и возможностью переопределения.
Основные различия.
- Область видимости
-
var- Имеет функциональную область видимости:
- виден внутри всей функции, в которой объявлен,
- если объявлен вне функции — становится свойством глобального объекта (в браузере
window), что загрязняет глобальную область.
- Не поддерживает блочную область видимости:
- объявления внутри
if,for,while,switchи т.п. "выходят" за блок и видны во всей функции.
- объявления внутри
- Имеет функциональную область видимости:
-
letиconst- Обладают блочной областью видимости:
- видны только внутри ближайшего блока
{ ... }, в котором объявлены, - включая блоки условий, циклов,
try/catch, и т.п.
- видны только внутри ближайшего блока
- Не вытекают за пределы блока, что делает код предсказуемее и безопаснее.
- Обладают блочной областью видимости:
Пример различий:
function example() {
if (true) {
var x = 1;
let y = 2;
const z = 3;
}
console.log(x); // 1 — доступен, var виден в пределах функции
console.log(y); // ReferenceError — let виден только в блоке if
console.log(z); // ReferenceError — const виден только в блоке if
}
- Hoisting (поднятие)
Все три — var, let, const — поднимаются интерпретатором, но ведут себя по-разному.
var- Объявление поднимается вверх области видимости (функция или глобальная).
- До фактического места объявления переменная существует со значением
undefined. - Это позволяет "использовать до объявления", но создаёт скрытые баги.
console.log(a); // undefined, а не ошибка
var a = 10;
letиconst- Также поднимаются, но попадают в "temporal dead zone" (TDZ):
- от начала области до строки объявления переменной её использование запрещено.
- Любой доступ до фактического объявления приводит к
ReferenceError.
- Также поднимаются, но попадают в "temporal dead zone" (TDZ):
console.log(b); // ReferenceError
let b = 10;
console.log(c); // ReferenceError
const c = 10;
- Перезапись и изменение значения
var- Можно переобъявить в той же области видимости без ошибки.
- Можно менять значение.
var x = 1;
var x = 2; // допустимо
x = 3; // допустимо
let- Нельзя переобъявить в одной области видимости.
- Можно изменять значение.
let y = 1;
// let y = 2; // SyntaxError — повторное объявление
y = 2; // ок
const- Нельзя переобъявить.
- Нельзя переназначить новое значение.
- При этом, если значение — объект или массив, их внутреннее состояние менять можно (неизменяемость ссылки, а не содержимого).
const z = 1;
// z = 2; // TypeError
const obj = { a: 1 };
obj.a = 2; // допустимо
// obj = {} // TypeError — нельзя переназначить саму ссылку
- Специфика
var, из-за которой его обычно избегают
- Отсутствие блочной области видимости:
- Легко получить "утечку" переменной за пределами блока.
for (var i = 0; i < 3; i++) {}
console.log(i); // 3 — i осталась доступна снаружи цикла
- Связывание с глобальным объектом:
- Объявление
varв глобальной области добавляет свойство вwindow(в браузере), что повышает риск конфликтов.
- Объявление
var a = 10;
console.log(window.a); // 10 в браузере
- Более высокая вероятность трудноотлавливаемых багов из-за hoisting + функциональная область.
Рекомендации практического использования:
const— по умолчанию для значений, ссылка на которые не должна меняться (наиболее безопасный и предсказуемый вариант).let— когда значение переменной должно изменяться (счётчики, промежуточные значения).var— не использовать в современном коде (ES6+), за исключением редких случаев, когда намеренно нужна его специфическая семантика (обычно в легаси-коде).
Итог:
var— функциональная область, hoisting без TDZ, разрешено переобъявление, нет блочного scoping.let— блочная область, hoisting с TDZ, без повторного объявления, значение можно менять.const— блочная область, hoisting с TDZ, без повторного объявления и без изменения ссылки.
Вопрос 9. Какие основные типы областей видимости существуют в JavaScript?
Таймкод: 00:04:06
Ответ собеседника: неполный. Упоминает функциональную и блочную области видимости, глобальную добавляет только после подсказки, без детального раскрытия.
Правильный ответ:
В JavaScript ключевые виды областей видимости:
- Глобальная область видимости
- Функциональная область видимости
- Блочная область видимости
- (Дополнительно для полноты) Лексическая область и модули как надстройка над базовыми моделями
Важно понимать не просто названия, а как движок ищет переменные и как это связано с var, let, const, замыканиями и модулями.
Глобальная область видимости:
- Переменные, объявленные:
- вне функций и модулей с помощью
var,let,const, - или неявно (без объявления — так делать нельзя).
- вне функций и модулей с помощью
- Доступны отовсюду в рамках текущего контекста выполнения:
- в браузере: в окне вкладки / iframe;
- в Node.js: в пределах модуля, но есть нюансы (каждый файл — модуль).
- В браузере:
varв глобальной области становится свойствомwindow,let/const— создают глобальные имена, но не как свойстваwindow.
- Нюанс: чрезмерное использование глобальных переменных:
- усложняет сопровождение,
- повышает риск конфликтов имён,
- делает код хрупким.
Пример:
var a = 1; // в браузере: window.a = 1
let b = 2; // глобальная, но не window.b
const c = 3;
function log() {
console.log(a, b, c); // доступны
}
Функциональная область видимости:
- Создаётся при вызове функции.
- Переменные, объявленные внутри функции с
var,let,const, параметрами и внутренними функциями:- видны только внутри этой функции и её вложенных функций.
varимеет именно функциональную область (а не блочную), поэтому:- объявление с
varвнутриif,forи т.п. по сути относится ко всей функции.
- объявление с
Пример:
function example() {
var x = 10;
if (true) {
var y = 20;
}
console.log(x, y); // 10, 20 — y виден во всей функции
}
console.log(typeof x); // "undefined" — x не доступен снаружи
Для let и const внутри функции область может быть уже блочной (см. ниже), но верхний уровень всё равно — функциональный.
Блочная область видимости:
- Любой блок кода
{ ... }(if, for, while, try/catch, просто фигурные скобки) создаёт блочную область дляletиconst. - Переменные с
let/const:- видны только внутри блока, где объявлены.
varблочной области не имеет — только функциональную/глобальную.
Пример:
if (true) {
let a = 1;
const b = 2;
var c = 3;
}
console.log(typeof a); // "undefined"
console.log(typeof b); // "undefined"
console.log(c); // 3 — var "протёк" за пределы if
Лексическая область видимости и замыкания:
- Лексическая область (lexical scope) — фундаментальный принцип:
- область видимости определяется структурой кода (где объявлена функция), а не местом вызова.
- Функция "помнит" окружение, в котором была создана — это замыкание.
- Это объясняет цепочку поиска идентификаторов: сначала локальная область, затем внешняя (родительская), далее вверх до глобальной.
Пример:
function outer() {
const secret = 42;
function inner() {
console.log(secret); // доступ к переменной из внешней области
}
return inner;
}
const fn = outer();
fn(); // 42 — замыкание
Модульная область (в современных JS):
- В ES-модулях (файлы с
type="module"или.mjs) весь код файла имеет собственную область видимости модуля. - Переменные модуля:
- не попадают в глобальную область,
- экспортируются явно через
export, - импортируются через
import.
- Это решает часть проблем с глобальными переменными.
Пример:
// module.js
const tokenTTL = 3600;
export function getTTL() {
return tokenTTL;
}
// main.js
import { getTTL } from './module.js';
console.log(getTTL());
Итого по вопросу (ключевые типы):
- Глобальная область — доступно везде внутри контекста.
- Функциональная область — внутри функции.
- Блочная область — внутри
{}дляlet/const.
Понимание этих областей критично для:
- корректной работы с переменными,
- избежания конфликтов и утечек,
- правильного использования замыканий,
- предсказуемого поведения в асинхронном коде.
Вопрос 10. Чем отличается function declaration от function expression с точки зрения способа объявления и поведения?
Таймкод: 00:04:27
Ответ собеседника: неполный. Говорит про присвоение функции переменной, но не раскрывает поведение hoisting и нюансы вызова до объявления.
Правильный ответ:
В JavaScript есть два базовых способа объявить функцию:
- function declaration — декларация функции.
- function expression — функциональное выражение (часто через присвоение переменной).
Они синтаксически похожи, но по поведению (hoisting, момент доступности, область видимости) отличаются принципиально.
Основные различия.
- Синтаксис и место в коде
Function declaration:
- Объявление на верхнем уровне или внутри блока/функции как самостоятельный оператор.
function sum(a, b) {
return a + b;
}
Function expression:
- Функция создаётся как выражение и обычно присваивается переменной, передаётся аргументом, возвращается из другой функции и т.п.
const sum = function(a, b) {
return a + b;
};
Может быть:
- именованным function expression,
- анонимным.
const sum = function sumImpl(a, b) {
return a + b;
};
- Hoisting (поднятие) и доступ до объявления
Ключевое практическое отличие.
Function declaration:
- Полностью поднимается (hoisted) в начало своей области видимости (глобальной, функциональной или блочной — в зависимости от контекста в современных спецификациях).
- Доступна для вызова во всём scope с самого начала, даже до строки объявления.
greet(); // "Hi"
function greet() {
console.log("Hi");
}
Function expression:
- Поднимается только как объявление переменной (вместе с
var/let/const), но не как готовая функция. - Фактическое значение (функция) присваивается только в момент выполнения соответствующей строки.
- До этого:
- при
var— переменная есть, но равнаundefined, вызов даст TypeError; - при
let/const— переменная в TDZ, обращение даст ReferenceError.
- при
// Пример с var
console.log(typeof sum); // "undefined"
try {
sum(1, 2); // TypeError: sum is not a function
} catch(e) {
console.log(e.message);
}
var sum = function(a, b) {
return a + b;
};
// Пример с let/const
try {
sum2(1, 2); // ReferenceError (TDZ)
} catch(e) {
console.log(e.message);
}
const sum2 = (a, b) => a + b;
Итого:
- function declaration можно безопасно вызывать до места определения (в рамках области видимости).
- function expression — нельзя, функция "существует" только после выполнения строки присвоения.
- Область видимости и блочные декларации (современный JS)
Ранее function declaration трактовались только как глобальные/функциональные, но в современных реализациях и стандарте:
- function declaration внутри блока
{}:- имеет блочную область видимости (аналогично
let/const, за исключением некоторых нюансов исторической совместимости в старых браузерах), - не должна использоваться как замена
var, если важна предсказуемость.
- имеет блочную область видимости (аналогично
if (true) {
function test() {
return 1;
}
}
console.log(typeof test); // в современных средах чаще "undefined"
Function expression:
- Подчиняется правилам области видимости переменной, которой присвоена (
let/const→ блочная,var→ функциональная/глобальная).
- Именованные function expression
Именованное функциональное выражение:
const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1); // используем внутреннее имя
};
Особенности:
- Имя
factдоступно только внутри тела функции. - Это удобно для рекурсии и лучшей читаемости.
- Внешний код использует
factorial.
- Практические выводы и рекомендации
-
Использовать function declaration:
- для "статических" функций, которые логически описывают API модуля/файла;
- когда удобно, чтобы функции были доступны во всём scope (например, вспомогательные функции внизу файла, но используются выше).
-
Использовать function expression (с
const/let):- для определения функций как значений:
- коллбеки,
- обработчики событий,
- функции высшего порядка,
- методы в объектах,
- когда важен контроль порядка инициализации (например, зависимость от других переменных);
- для избежания неявной магии hoisting: код читается сверху вниз.
- для определения функций как значений:
-
Не полагаться на вызов function expression до присвоения.
-
Избегать
varдля хранения function expression в современном коде. -
Понимать, что отличие в hoisting и области видимости напрямую влияет на привычные баги:
- "is not a function",
- неожиданная доступность/недоступность функций.
Кратко:
- function declaration:
- объявление:
function name(...) { ... } - поднимается целиком, доступна везде в области видимости;
- объявление:
- function expression:
- объявление через выражение (часто
const name = function() {}или стрелочнаяconst name = () => {}), - функция появляется только после выполнения строки, поведение зависит от
var/let/const.
- объявление через выражение (часто
Вопрос 11. Как называется механизм, позволяющий вызывать функцию до строки её определения?
Таймкод: 00:05:07
Ответ собеседника: правильный. Называет механизм hoisting и связывает его с использованием до объявления.
Правильный ответ:
Механизм называется hoisting (поднятие).
Hoisting — это поведение JavaScript-движка, при котором объявления идентификаторов (переменных и функций) логически "поднимаются" в начало своей области видимости до выполнения кода. Важно: поднимаются объявления, а не фактические присвоения.
Ключевые аспекты hoisting:
- Function Declaration
- Объявления функций вида:
поднимаются целиком:
function foo() { ... }- и имя,
- и тело функции.
- Поэтому такие функции можно вызывать до места их текстового объявления.
Пример:
greet(); // Работает
function greet() {
console.log("Hello");
}
- Function Expression и переменные (
var,let,const)
- Для переменных поднимается только объявление, но не значение.
С var:
- Переменная существует с начала области видимости, но равна
undefinedдо присвоения.
console.log(a); // undefined (объявление поднято, значение нет)
var a = 10;
Function expression с var:
foo(); // TypeError: foo is not a function
var foo = function() {};
- Объявление
var fooподнято, но до строки присвоенияfoo===undefined.
С let и const:
- Тоже поднимаются, но попадают в temporal dead zone (TDZ):
- использовать их до строки объявления нельзя — будет
ReferenceError.
- использовать их до строки объявления нельзя — будет
console.log(x); // ReferenceError
let x = 5;
- Практические выводы:
- Возможность вызывать функцию до её определения в коде корректна только для function declaration.
- Для function expression (включая стрелочные функции) доступность зависит от момента присвоения и типа объявления переменной.
- Осознанное понимание hoisting помогает:
- избегать неочевидных багов (
is not a function,undefined), - структурировать код так, чтобы поведение было предсказуемым,
- разделять ответственность: декларации API (function declaration) и локальные/динамические функции (function expression).
- избегать неочевидных багов (
Вопрос 12. Чем стрелочные функции отличаются от обычных функций в части работы с this?
Таймкод: 00:05:40
Ответ собеседника: неполный. Сначала говорит, что стрелочные функции всегда ссылаются на глобальный контекст, после подсказки частично принимает идею про ближайший контекст, но формулирует неточно.
Правильный ответ:
Ключевое отличие стрелочных функций от обычных — в поведении this (а также arguments, super и new.target):
- Лексическое (фиксированное)
thisу стрелочных функций
- Стрелочные функции НЕ создают собственное значение
this. - Вместо этого они лексически захватывают
thisиз внешней области видимости (из того контекста, в котором они были определены, а не вызваны). - Значение
thisвнутри стрелочной функции всегда совпадает сthisвнешней функции/контекста и не меняется:- не зависит от того, как функция вызвана;
- не меняется через
call,apply,bind(дляthis— их влияние игнорируется).
Пример:
const obj = {
value: 42,
regular() {
console.log(this.value);
},
arrow: () => {
console.log(this.value);
}
};
obj.regular(); // 42 — this = obj
obj.arrow(); // undefined (в браузере) — this взят из внешнего лексического окружения (обычно window или undefined в strict)
Здесь:
regular— обычный метод,thisуказывает наobjпри вызовеobj.regular().arrow— стрелочная функция,thisберётся не из вызоваobj.arrow(), а из места объявления (скорее всего — глобальный / модульный контекст), поэтому не указывает наobj.
thisв обычных функциях (function declaration/expression)
- Обычная функция создаёт собственное
this, которое определяется способом вызова:- как метод объекта:
obj.m()→this === obj; - как конструктор:
new F()→this— новый экземпляр; - простой вызов:
f()→ в strictthis === undefined, без strict — глобальный объект; - через
call/apply/bind—thisзадаётся явно.
- как метод объекта:
Пример:
function show() {
console.log(this);
}
const obj = { show };
obj.show(); // this = obj
show(); // this = undefined (strict) или window (non-strict)
show.call({ a: 1 }); // this = { a: 1 }
- Стрелочные функции и методы объектов/классов
- Стрелочные функции НЕ подходят для определения методов, которые должны использовать
thisобъекта.
Неправильно:
const user = {
name: 'Ann',
getName: () => this.name
};
user.getName(); // undefined — this не указывает на user
Правильно:
const user = {
name: 'Ann',
getName() {
return this.name;
}
};
user.getName(); // "Ann"
- Где стрелочные функции полезны с точки зрения
this
Особенно удобны там, где нужно сохранить внешний this:
- В колбэках, внутренних функциях, промисах, обработчиках в методах объектов или классов.
Пример без стрелочной функции:
function Timer() {
this.seconds = 0;
setInterval(function () {
this.seconds++; // this здесь = window/undefined, а не экземпляр Timer
}, 1000);
}
Чтобы исправить, раньше делали:
function Timer() {
this.seconds = 0;
const self = this;
setInterval(function () {
self.seconds++;
}, 1000);
}
Со стрелочной функцией:
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // this берется из Timer, как и ожидалось
}, 1000);
}
Здесь стрелочная функция захватила this конструктора Timer, что и нужно.
- Дополнительные отличия, связанные с контекстом
- Стрелочные функции:
- нельзя использовать как конструкторы:
new ArrowFn()выбросит ошибку; - не имеют собственного
arguments(используютargumentsвнешней функции, если он есть); - хорошо отражают идею "функции как выражения без собственного контекста".
- нельзя использовать как конструкторы:
Это логически дополняет поведение с this: стрелочная функция — это "тонкий" колбэк, не меняющий контекст, а обычная функция — полноценный объект с собственным вызовным контекстом.
Итого по сути вопроса:
- Обычная функция:
thisдинамический, зависит от способа вызова, может переназначатьсяcall/apply/bind. - Стрелочная функция:
thisлексический, один раз захватывается из внешнего контекста при объявлении и не меняется, что делает их идеальными для вложенных колбэков и плохим выбором для методов, зависящих отthisобъекта.
Вопрос 13. Своими словами опиши, что такое замыкание.
Таймкод: 00:06:29
Ответ собеседника: правильный. Приводит пример: внешняя функция с внутренними переменными возвращает внутреннюю функцию, которая после завершения внешней продолжает иметь доступ к этим переменным.
Правильный ответ:
Замыкание — это свойство функции в JavaScript (и не только), при котором она "запоминает" лексическое окружение (переменные, объявленные снаружи неё), существовавшее в момент её создания, и может обращаться к этим переменным даже после того, как внешняя функция завершила выполнение.
Ключевые моменты:
- Лексическое окружение
- JS использует лексическую (статическую) область видимости:
- То, какие переменные доступны, определяется местом объявления функции в коде, а не местом её вызова.
- Когда функция создаётся, вместе с ней фиксируется ссылка на окружение, в котором она была объявлена.
- Это окружение включает:
- параметры внешней функции,
- локальные переменные,
- другие замкнутые значения.
- Что делает замыкание
- Позволяет функции:
- работать с "приватным состоянием",
- инкапсулировать данные,
- создавать фабрики функций,
- реализовывать кеширование, мемоизацию, счётчики, настройки и т.д.
- Важный момент: пока есть хотя бы одна внутренняя функция, которая ссылается на внешние переменные, эти переменные не очищаются сборщиком мусора.
- Базовый пример
function createCounter() {
let count = 0; // переменная из внешнего окружения
return function() {
count++;
return count;
};
}
const counterA = createCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
const counterB = createCounter();
console.log(counterB()); // 1
console.log(counterB()); // 2
Что здесь происходит:
createCounterсоздаётcountи возвращает внутреннюю функцию.- Внутренняя функция образует замыкание над
count. - После завершения
createCounterпеременнаяcountне уничтожается — на неё ссылается возвращённая функция. counterAиcounterBимеют независимые "приватные"count.
- Замыкания и стрелочные функции
Стрелочные функции работают с замыканиями так же, как обычные (лексическое окружение + this):
function makePrefixer(prefix) {
return (value) => `${prefix}_${value}`;
}
const withUser = makePrefixer('user');
console.log(withUser(1)); // user_1
console.log(withUser(2)); // user_2
- Распространённые применения
- Инкапсуляция состояния без классов:
function createStore(initial) {
let state = initial;
return {
get() {
return state;
},
set(next) {
state = next;
}
};
}
const store = createStore({ loggedIn: false });
store.set({ loggedIn: true });
console.log(store.get()); // { loggedIn: true }
- Фабрики функций:
function multiplier(k) {
return function(x) {
return x * k;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(10)); // 20
console.log(triple(10)); // 30
- Мемоизация:
function memoize(fn) {
const cache = new Map();
return function(arg) {
if (cache.has(arg)) {
return cache.get(arg);
}
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
const slowSquare = (x) => {
// имитация тяжёлых вычислений
for (let i = 0; i < 1e7; i++) {}
return x * x;
};
const fastSquare = memoize(slowSquare);
fastSquare(5); // медленно, вычисляет
fastSquare(5); // быстро, из кеша
- Типичная ошибка понимания
- Замыкание — это не только "когда функция возвращает функцию".
- Оно возникает всегда, когда внутренняя функция использует внешние переменные:
- возвращаемая,
- переданная как колбэк,
- сохранённая в структуру данных.
- Важно помнить и про утечки памяти:
- ненужные долгоживущие ссылки через замыкания могут удерживать большие объекты.
Итого: Замыкание — это механизм, при котором функция захватывает и удерживает доступ к переменным внешнего лексического окружения. Это фундамент JS-паттернов: приватные данные, фабрики функций, мемоизация, колбэки и корректная работа с асинхронностью.
Вопрос 14. Что такое прототип в JavaScript и как работает наследование через него?
Таймкод: 00:07:33
Ответ собеседника: правильный. Описывает прототип как объект, от которого другие объекты наследуют свойства через скрытое свойство __proto__, и упоминает, что вершина цепочки — null.
Правильный ответ:
В JavaScript прототип — это механизм, через который объекты наследуют свойства и методы от других объектов. Ядро модели: каждое объектоподобное значение (за редкими исключениями) связано с другим объектом — прототипом, и доступ к свойствам осуществляется через поиск по цепочке прототипов (prototype chain).
Ключевые элементы:
- Прототип и скрытая ссылка
- У каждого объекта есть внутреннее [[Prototype]]-свойство — ссылка на другой объект или
null. - В большинстве сред оно доступно через:
- устаревшее, но наглядное
obj.__proto__, - современные методы:
Object.getPrototypeOf(obj)иObject.setPrototypeOf(obj).
- устаревшее, но наглядное
- Прототип — это не "класс", а обычный объект, из которого другие объекты могут "делиться" функциональностью.
Когда мы обращаемся к свойству:
obj.prop
движок делает:
- Если
propесть у самогоobj(own property) → вернуть его. - Иначе смотрит в прототипе:
Object.getPrototypeOf(obj). - Если там нет → идёт дальше вверх по цепочке.
- Цепочка продолжается до тех пор, пока не достигнет
null. - Если свойство не найдено по всей цепочке →
undefined.
- Цепочка прототипов (prototype chain)
Типичная цепочка:
obj → obj.__proto__ → ... → Object.prototype → null
Object.prototype— корневой объект, где определены базовые методы:toString,hasOwnProperty,valueOfи т.д.
- Вершина цепочки всегда
null, за ним поиска уже нет.
Пример:
const obj = { a: 1 };
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log(obj.toString); // найден в Object.prototype
- Прототипы функций-конструкторов
Функции в JS — тоже объекты. У каждой функции есть свойство prototype, которое используется, когда мы создаём объект через new:
function User(name) {
this.name = name;
}
User.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
const u = new User('Ann');
u.sayHi(); // ищется у объекта u → в его [[Prototype]] → User.prototype
Как это работает:
- Вызов
new User('Ann'):- создаёт пустой объект,
- устанавливает для него [[Prototype]] =
User.prototype, - вызывает
Userкак функцию сthis= этот объект, - возвращает объект (если не возвращено другое объектное значение явно).
Результат:
- Все экземпляры
Userразделяют методы изUser.prototype, без копирования на каждый объект.
- Наследование через прототипы
Можно строить цепочки:
function Animal(name) {
this.name = name;
}
Animal.prototype.say = function() {
console.log(`I'm ${this.name}`);
};
function Dog(name) {
Animal.call(this, name); // "наследуем" инициализацию
}
// Настраиваем прототипную цепочку:
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log('Woof!');
};
const rex = new Dog('Rex');
rex.say(); // "I'm Rex" — из Animal.prototype
rex.bark(); // "Woof!" — из Dog.prototype
Цепочка:
rex→Dog.prototype→Animal.prototype→Object.prototype→null.
Механика:
- Если у
rexнет свойства:- проверяется
Dog.prototype, - если там нет —
Animal.prototype, - далее
Object.prototype, - иначе —
undefined.
- проверяется
- Связь с
classв современном JavaScript
Синтаксис class — это удобная надстройка над прототипным наследованием, а не другая модель:
class Animal {
constructor(name) {
this.name = name;
}
say() {
console.log(`I'm ${this.name}`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof!');
}
}
const rex = new Dog('Rex');
rex.say(); // из Animal.prototype
rex.bark(); // из Dog.prototype
Под капотом:
- Методы класса → в
.prototype. extends→ настройка цепочки прототипов:Dog.prototypeнаследует отAnimal.prototype,- конструкторы также имеют свою цепочку через
Object.setPrototypeOf(Dog, Animal).
- Практические моменты и важные детали
- Добавление методов в прототип:
- эффективно и экономно по памяти,
- изменения в
SomeConstructor.prototype"видят" все экземпляры.
- Сравнение с собственными свойствами:
const obj = { a: 1 };
console.log('a' in obj); // true (own)
console.log('toString' in obj); // true (через прототип)
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('toString')); // false
- Не рекомендуется массово трогать
Object.prototype:- это влияет на все объекты и ломает поведение библиотек и for-in/hasOwnProperty-паттернов.
Итого:
- Прототип — это объект, на который ссылается [[Prototype]] другого объекта.
- Наследование работает через поиск свойств по цепочке прототипов до
Object.prototypeиnull. - Конструкторы,
Object.createиclass/extends— разные уровни абстракции над одной и той же прототипной моделью.
Вопрос 15. Какие нативные методы используются для поиска элементов в DOM?
Таймкод: 00:08:59
Ответ собеседника: неполный. Упоминает querySelector, поиск по классу и getElementById, но не даёт системного перечисления и путается в названиях.
Правильный ответ:
В DOM есть несколько базовых нативных методов для поиска элементов. Их важно знать системно: по типу селектора, по области поиска и по формату возвращаемого результата.
Основные методы поиска:
- Универсальные селекторы (CSS-селекторы)
-
document.querySelector(selector)- Возвращает первый элемент, соответствующий CSS-селектору.
- Если элемент не найден —
null. - Работает с любыми валидными селекторами:
- по тегу:
"div" - по классу:
".item" - по id:
"#main" - по атрибуту:
"[data-id='123']" - сложные селекторы:
".list > li.active a"
- по тегу:
-
document.querySelectorAll(selector)- Возвращает статическую коллекцию (
NodeList) всех элементов, соответствующих селектору. - Итерабельна:
for...of,Array.from,forEach. - Не "живая": не обновляется автоматически при изменении DOM.
- Возвращает статическую коллекцию (
Оба метода доступны не только у document, но и у любого элемента для поиска внутри конкретного поддерева:
const container = document.getElementById('container');
const buttons = container.querySelectorAll('button.primary');
- По id
document.getElementById(id)- Возвращает один элемент по его уникальному
id. - Быстрый и точечный.
- Если не найден —
null. - Работает только у
document, не у произвольных элементов.
- Возвращает один элемент по его уникальному
const el = document.getElementById('app');
- По имени тега
document.getElementsByTagName(tagName)element.getElementsByTagName(tagName)- Возвращает "живую" коллекцию (
HTMLCollection) элементов с указанным тегом. - "Живая" означает: при изменениях DOM коллекция автоматически обновляется.
- Примеры:
"div","span","input","a"и т.д.
- Возвращает "живую" коллекцию (
const divs = document.getElementsByTagName('div');
- По имени класса
document.getElementsByClassName(className)element.getElementsByClassName(className)- Возвращает "живую"
HTMLCollectionвсех элементов с указанным классом. - Можно передать несколько классов через строку:
"btn primary"— элементы, содержащие оба класса.
- Возвращает "живую"
const items = document.getElementsByClassName('list-item');
- По имени (атрибут name)
document.getElementsByName(name)- Возвращает
NodeList(в старых описаниях — похожую коллекцию) элементов с атрибутомname. - Используется часто для форм, радио-кнопок, инпутов.
- Возвращает
const inputs = document.getElementsByName('email');
- Вспомогательные/смежные методы навигации (не поиск по селектору, но важны для работы с DOM):
- По узлам и структуре:
parentNode,children,firstElementChild,lastElementChild,nextElementSibling,previousElementSibling.
- Проверка соответствия селектору:
element.matches(selector)— проверяет, удовлетворяет ли элемент селектору.element.closest(selector)— поднимается вверх по дереву предков и ищет первый элемент, подходящий под селектор.
const btn = event.target.closest('button');
if (btn && btn.matches('.primary')) {
// обработка клика по "primary"-кнопке
}
Практические рекомендации:
- Для большинства современных задач:
querySelector/querySelectorAll— универсальные и читаемые (CSS-селекторы).
- Для точечного доступа:
getElementById— оптимален и семантичен.
- Для оптимизации или поиска внутри конкретного блока:
- вызывайте методы поиска у контейнера, а не у
document— это уменьшает область поиска.
- вызывайте методы поиска у контейнера, а не у
- Осторожно с "живыми" коллекциями (
getElementsBy*):- при больших и часто меняющихся DOM они могут вести себя неожиданно с точки зрения производительности и логики;
- в большинстве случаев удобнее работать со статическими
NodeListизquerySelectorAllи сразу приводить к массиву.
Вопрос 16. В каких состояниях может находиться Promise?
Таймкод: 00:09:25
Ответ собеседника: правильный. Перечисляет pending, успешное выполнение (resolved/fulfilled) и отклонение (rejected).
Правильный ответ:
Promise в JavaScript описывает результат асинхронной операции и имеет строго определённый жизненный цикл. Формально у Promise три состояния:
- pending
- fulfilled
- rejected
Важно понимать не только названия, но и переходы между состояниями.
Подробно:
- pending (ожидание)
- Начальное состояние только что созданного Promise.
- Асинхронная операция ещё не завершена.
- Нет финального значения или причины ошибки.
- Из этого состояния Promise может перейти только один раз:
- в fulfilled,
- или в rejected.
- Пока Promise в pending:
- обработчики
.then(onFulfilled, onRejected)и.catch(onRejected)просто регистрируются и будут вызваны позже.
- обработчики
Пример:
const p = new Promise((resolve, reject) => {
// пока ничего не вызываем — p в состоянии pending
});
- fulfilled (успешно выполнен)
- Означает, что операция завершилась успешно.
- У Promise появляется фиксированное значение результата.
- После перехода в fulfilled:
- состояние и значение становятся иммутабельными,
- вызываются все зарегистрированные обработчики
then(первый коллбэк).
Пример:
const p = new Promise((resolve) => {
setTimeout(() => resolve(42), 100);
});
// Эти обработчики выполнятся после перехода в fulfilled
p.then(value => {
console.log(value); // 42
});
Иногда говорят "resolved":
- В терминах спецификации:
- "resolved" — более широкое понятие: Promise "согласован" с каким-то значением (это может быть:
- обычное значение → fulfilled,
- другой Promise → подчинение его состоянию).
- В повседневной речи часто "resolved" используют как синоним "fulfilled", но строго корректнее различать:
- resolved → приведён к окончательному значению/другому Promise,
- fulfilled → успешно завершён с конкретным значением.
- "resolved" — более широкое понятие: Promise "согласован" с каким-то значением (это может быть:
- rejected (отклонён)
- Означает, что операция завершилась с ошибкой.
- У Promise появляется фиксированная причина ошибки (обычно объект Error, но может быть любое значение).
- После перехода в rejected:
- состояние и причина также иммутабельны,
- вызываются все зарегистрированные обработчики отклонения:
- второй аргумент
then, - или обработчики
.catch()/.then(null, ...), - или
.finally()(без доступа к значению, но срабатывает при любом исходе).
- второй аргумент
Пример:
const p = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Failed')), 100);
});
p.catch(err => {
console.error(err.message); // "Failed"
});
Ключевые свойства модели Promise:
- Однонаправленность:
- переход возможен только из pending в fulfilled или rejected;
- из fulfilled в rejected (или наоборот) перейти нельзя.
- Иммутабельность:
- после перехода состояние и значение/ошибка не меняются.
- Асинхронность вызова обработчиков:
- даже если Promise уже выполнен, колбэки
.then/.catch/.finallyвызываются асинхронно (микротаски), после текущего стека вызовов.
- даже если Promise уже выполнен, колбэки
Эта трёхсостоящая модель делает поведение асинхронного кода предсказуемым и позволяет надёжно выстраивать цепочки .then(), async/await и обработку ошибок.
Вопрос 17. Своими словами опиши, что такое event loop в JavaScript и как он обрабатывает задачи.
Таймкод: 00:09:58
Ответ собеседника: правильный. Описывает event loop как механизм управления задачами, различает синхронные и асинхронные операции, упоминает очереди макротасок и микротасок (например, промисы).
Правильный ответ:
Event loop — это механизм, который координирует выполнение кода, обработку асинхронных операций и взаимодействие с окружением (браузер или Node.js) в однопоточном JavaScript. Он объясняет, почему при одной "нитке" исполнения мы можем обрабатывать множество асинхронных операций без блокировки интерфейса.
Ключевые идеи:
- Однопоточность JS-кода
- JavaScript-движок (например, V8) выполняет код в одном основном потоке.
- В один момент времени выполняется только одна инструкция JS.
- Асинхронность достигается не за счёт нескольких потоков JS, а за счёт:
- API окружения (браузерные Web API, Node.js API),
- очередей задач,
- и работы event loop, который решает, какую задачу выполнить следующей.
- Стек вызовов (call stack)
- Все синхронные функции выполняются в рамках стека вызовов.
- Пока стек не пуст — новые задачи из очередей не берутся.
- Любой "тяжёлый" синхронный код блокирует:
- обновление UI,
- обработку событий,
- выполнение колбэков промисов и таймеров.
- Асинхронные операции и окружение Асинхронные вызовы (таймеры, HTTP-запросы, обработчики событий, промисы) работают так:
- JS-код регистрирует операцию и колбэк:
setTimeout,addEventListener,fetch,Promise.thenи т.д.
- Реальная работа (ожидание таймера, сети, IO) происходит во внешнем окружении, а не в JS-стеке.
- Когда операция завершается, окружение ставит "задачу" в соответствующую очередь.
- Event loop берёт задачи из очередей и превращает их в вызовы JS-функций, когда стек свободен.
- Очереди задач: макротаски и микротаски
Важно различать два основных типа задач:
-
Макротаски (tasks, macrotasks)
- Примеры:
setTimeout,setInterval- обработчики DOM-событий (click, input и т.п.)
setImmediate(Node.js)- некоторые этапы выполнения скриптов, сообщений в
postMessage, XHR callbacks (в зависимости от среды)
- Каждая макротаска — это "крупная единица работы":
- выполнить скрипт,
- обработать событие,
- выполнить таймер.
- Примеры:
-
Микротаски (microtasks)
- Более приоритетные задачи, которые выполняются "между" макротасками, до перехода к следующей.
- Примеры:
- обработчики
.then/.catch/.finallyдляPromise, queueMicrotask,- в Node.js —
process.nextTick(имеет ещё более высокий приоритет).
- обработчики
- Выполняются сразу после текущего куска кода и перед следующей макротаской.
Последовательность работы event loop (упрощённая):
- Взять одну макротаску из очереди:
- выполнить её полностью (синхронный JS-код).
- После завершения:
- выполнить все микротаски из очереди microtasks до конца (пока очередь не опустеет).
- Обновить рендер (в браузере).
- Перейти к следующей макротаске и повторить.
Пример для иллюстрации:
console.log('A');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise');
});
console.log('B');
Порядок вывода:
- Сначала синхронно:
AB
- Затем:
- микротаски (обработчики промисов):
promise
- микротаски (обработчики промисов):
- Потом:
- макротаски (таймеры):
timeout
- макротаски (таймеры):
Итого в консоли:
A
B
promise
timeout
Почему:
setTimeout(..., 0)ставит макротаску.Promise.resolve().then(...)ставит микротаску.- После окончания основного скрипта:
- event loop сначала выполняет микротаски (
promise), - затем берёт следующую макротаску (
timeout).
- event loop сначала выполняет микротаски (
- Практические выводы
- Асинхронный JS предсказуем, если понимать:
- синхронный код — сразу в стеке;
- промисы/
await— микротаски (высокий приоритет); - таймеры/события — макротаски (ниже по приоритету);
- Долгие синхронные операции:
- блокируют event loop;
- задерживают обработку кликов, анимаций, промисов, сетевых ответов.
- Для тяжёлых операций:
- выносить работу в Web Workers, отдельные процессы/сервисы или бэкенд.
Кратко: Event loop — это "диспетчер", который:
- выполняет синхронный код;
- после освобождения стека обрабатывает микротаски;
- затем — макротаски;
- и так по кругу, обеспечивая асинхронное, но упорядоченное выполнение операций в однопоточном окружении.
Вопрос 18. Какие задачи относятся к макротаскам в контексте event loop?
Таймкод: 00:11:20
Ответ собеседника: правильный. После подсказки называет таймеры и обработчики событий как примеры макротасок.
Правильный ответ:
Макротаски (tasks, macrotasks) — это "крупные" единицы работы, которые event loop последовательно берет из очереди задач. Каждая макротаска выполняется целиком, после чего движок перед переходом к следующей макротаске обрабатывает все накопленные микротаски.
К типичным макротаскам относятся:
-
Выполнение основного скрипта:
- начальная загрузка и исполнение JS-файла (
<script>), - каждая отдельная единица исполнения (например, скрипты по
defer,asyncи т.д.).
- начальная загрузка и исполнение JS-файла (
-
Таймеры:
setTimeout(callback, delay)setInterval(callback, delay)После срабатывания таймера колбэк попадает в очередь макротасок.
-
Обработчики событий DOM:
click,input,submit,scroll,keydown,mousemoveи др. Когда событие происходит и обработчик готов к вызову, он ставится как макротаска.
-
Сетевые и другие асинхронные колбэки окружения:
- старые XHR-колбэки,
- некоторые колбэки WebSocket, FileReader и других Web API (зависит от реализации, но концептуально — макротаски).
-
Сообщения и API, явно ставящие задачу:
postMessage(обработка сообщения),MessageChannel(обработка сообщений),- в браузере: часть операций history, некоторые API.
-
В Node.js (специфика окружения):
- I/O callbacks,
setImmediate— отдельная очередь макротасок,- фазы event loop (timers, poll, check и т.д.) — тоже оперируют макротасками.
Главное отличие от микротасок:
- Макротаски:
- формируют "крупные шаги" выполнения: таймер сработал, событие произошло, скрипт исполнился.
- между ними пользовательский интерфейс может быть перерисован.
- Микротаски:
- выполняются сразу после текущего стека и перед следующей макротаской:
Promise.then/catch/finally,queueMicrotask,- (в Node.js)
process.nextTick.
- имеют более высокий приоритет и "дренируются" до конца перед переходом к новой макротаске.
- выполняются сразу после текущего стека и перед следующей макротаской:
Понимание того, какие операции являются макротасками, важно для:
- корректного прогнозирования порядка выполнения кода;
- избежания неожиданной задержки промисов (микротаски) из-за тяжёлых обработчиков макротасок;
- проектирования отзывчивого UI и предсказуемого асинхронного поведения.
Вопрос 19. Сколько основных типов данных в JavaScript и какие из них относятся к примитивам?
Таймкод: 00:11:54
Ответ собеседника: правильный. Упоминает разделение на примитивы и объекты, перечисляет number, boolean, undefined, bigint, symbol и указывает объект как сложный тип.
Правильный ответ:
В современном JavaScript существует восемь основных типов данных, из них семь — примитивные, один — ссылочный (объектный).
Примитивные типы:
-
number- Числа с плавающей точкой двойной точности.
- Включают
NaN,Infinity,-Infinity. - Особенность: нет отдельного типа для целых чисел (до появления
bigint), все —number.
-
bigint- Целые числа произвольной длины.
- Обозначаются суффиксом
n:10n,9007199254740993n. - Нельзя напрямую смешивать с
numberв арифметике без явного приведения.
-
string- Текстовые данные.
- Набор 16-битных кодовых единиц (UTF-16).
- Иммутабельны: операции над строками создают новые значения.
-
boolean- Логические значения:
trueилиfalse.
- Логические значения:
-
undefined- Значение "переменная объявлена, но ей не присвоено значение".
- По умолчанию у неинициализированных переменных и отсутствующих аргументов функции.
-
null- Отдельный примитив, означающий "отсутствие значения" или "пустое значение".
- Исторический баг:
typeof null === 'object', ноnull— не объект, а отдельный примитив.
-
symbol- Уникальные идентификаторы.
- Никогда не равны друг другу, даже с одинаковым описанием.
- Используются для скрытых свойств объектов, протоколов и системных "ключей" (well-known symbols).
Непримитивный (ссылочный) тип:
object- Включает:
- обычные объекты
{}, - массивы
[], - функции,
- даты (
Date), - регулярные выражения (
RegExp), - карты (
Map), множества (Set) и др.
- обычные объекты
- Передаются и сравниваются по ссылке.
- Могут иметь сложную структуру и прототипную цепочку.
- Включает:
Ключевые различия между примитивами и объектами:
- Примитивы:
- неизменяемы (immutable),
- передаются по значению,
- операции создают новые значения.
- Объекты:
- изменяемы (mutable),
- передаются по ссылке,
- несколько переменных могут ссылаться на один и тот же объект.
Для уверенного владения темой важно:
- чётко перечислять все 7 примитивов:
string,number,bigint,boolean,symbol,undefined,null; - отделять их от единственного ссылочного базового типа —
object.
Вопрос 20. Как передаются примитивы и объекты в JavaScript?
Таймкод: 00:12:34
Ответ собеседника: правильный. Примитивы передаются по значению, объекты — по ссылке.
Правильный ответ:
Корректнее формулировать так: в JavaScript при присваивании и передаче в функции всегда передаётся значение. Разница в том, какое это значение:
- для примитивов — само значение;
- для объектов — значение-ссылка на объект в памяти.
Отсюда и практическое различие поведения.
Примитивы (string, number, boolean, null, undefined, bigint, symbol):
- При присвоении переменная получает копию значения.
- Изменение одной переменной не влияет на другую.
Пример:
let a = 10;
let b = a; // копируется значение 10
b = 20;
console.log(a); // 10
console.log(b); // 20
При передаче в функцию примитив также копируется:
function inc(x) {
x = x + 1;
}
let n = 5;
inc(n);
console.log(n); // 5 — исходное значение не изменилось
Объекты (включая массивы и функции):
- Переменная хранит не сам объект, а ссылку на него.
- При присвоении другая переменная получает копию ссылки на тот же объект.
- При изменении через одну переменную изменения видны через другую, так как объект общий.
Пример:
const obj1 = { value: 1 };
const obj2 = obj1; // копируется ссылка на тот же объект
obj2.value = 42;
console.log(obj1.value); // 42 — изменился один и тот же объект
console.log(obj2.value); // 42
Передача объекта в функцию:
- В функцию попадает копия ссылки.
- Сама связь "переменная → ссылка" локальна, но объект за ссылкой общий.
function mutate(o) {
o.value = 100; // меняем объект по ссылке
}
const obj = { value: 1 };
mutate(obj);
console.log(obj.value); // 100 — объект изменён
Если внутри функции переназначить параметр, это не меняет внешнюю переменную:
function replace(o) {
o = { value: 200 }; // меняем локальную ссылку, внешний obj не трогаем
}
const obj = { value: 1 };
replace(obj);
console.log(obj.value); // 1 — ссылка снаружи осталась на старый объект
Краткий вывод:
- Примитивы:
- передаются и присваиваются как независимые значения.
- Объекты:
- передаётся значение-ссылка;
- изменяя содержимое по ссылке, вы изменяете общий объект;
- переназначение ссылки внутри функции не влияет на внешнюю переменную.
Вопрос 21. Опиши основные этапы жизненного цикла компонента в React и как применять их на практике.
Таймкод: 00:12:48
Ответ собеседника: правильный. Указывает этапы (рендер, обновление, размонтирование) и связывает их с useEffect, зависимостями и cleanup для логики при монтировании, обновлении и размонтировании.
Правильный ответ:
В современном React (c функциональными компонентами и хуками) жизненный цикл удобнее всего понимать не как набор разрозненных методов, а как последовательность фаз:
- Монтирование (mount)
- Обновление (update)
- Размонтирование (unmount)
Классические методы (componentDidMount, componentDidUpdate, componentWillUnmount) сегодня в основном переосмыслены через useEffect и другие хуки.
Основные этапы и практическое применение.
- Монтирование (mount)
Что происходит:
- Компонент создаётся и впервые рендерится в DOM.
- Вызывается функция компонента (или конструктор/
renderв классовом). - React строит DOM-дерево на основе JSX и монтирует его.
Для чего использовать фазу монтирования:
- Инициализация внешних ресурсов:
- запросы к API (fetch данных),
- подписки на события (WebSocket, DOM-события, EventEmitter),
- инициализация сторонних библиотек.
- Настройка состояния, которое зависит от внешних источников (после первого рендера).
В функциональном компоненте:
import { useEffect, useState } from 'react';
function UsersList() {
const [users, setUsers] = useState([]);
useEffect(() => {
let cancelled = false;
async function load() {
const res = await fetch('/api/users');
if (!cancelled) {
const data = await res.json();
setUsers(data);
}
}
load();
// cleanup при размонтировании
return () => {
cancelled = true;
};
}, []); // пустой массив зависимостей => эффект только при монтировании
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
Здесь:
useEffect(..., [])логически соответствуетcomponentDidMount(+componentWillUnmountчерез cleanup).
- Обновление (update)
Что происходит:
- Компонент повторно рендерится при изменении:
- пропсов,
- состояния (
useState,useReducer), - контекста (
useContext).
- React сравнивает предыдущий и новый результат (reconciliation) и минимально обновляет DOM.
Практическое применение:
- Реакция на изменение конкретных данных:
- повторные запросы при смене фильтра/ID,
- синхронизация с локальным хранилищем, URL, заголовком страницы и т.п.
- Оптимизация производительности:
- мемоизация (
React.memo,useMemo,useCallback), - избежание лишних побочных эффектов.
- мемоизация (
В функциональном компоненте — через зависимости useEffect:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let aborted = false;
async function load() {
const res = await fetch(`/api/users/${userId}`);
if (!aborted) {
setUser(await res.json());
}
}
load();
return () => {
aborted = true; // отменяем актуальность предыдущего запроса
};
}, [userId]); // эффект вызывается при монтировании и при каждом изменении userId
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
- Поведение:
- при первом рендере: запрос данных;
- при смене
userId: cleanup старого эффекта (например, "отмена"), новый запрос; - это аналог
componentDidMount + componentDidUpdateс зависимостью.
Важно:
- Любой
useEffectвызывается:- после рендера,
- и повторяется при изменении значений из массива зависимостей.
- Ошибка: оставлять массив зависимостей пустым, когда эффект реально зависит от пропсов или состояния → баги и рассинхронизация.
- Размонтирование (unmount)
Что происходит:
- Компонент удаляется из DOM.
- React освобождает связанные ресурсы.
- Важно корректно очистить побочные эффекты.
Практическое применение:
- Отписка от:
- событий (window/document/DOM),
- WebSocket,
- Observable/EventEmitter,
- таймеров (
setInterval,setTimeout), - любых ресурсов, которые могут вызвать утечки памяти или "setState после unmount".
В функциональном компоненте:
- Cleanup-функция, возвращаемая из
useEffect.
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// cleanup — вызывается при размонтировании
return () => {
clearInterval(id);
};
}, []); // ставим таймер один раз
return <div>Seconds: {seconds}</div>;
}
Здесь:
- Без
clearIntervalтаймер продолжил бы работать и пытаться обновлять состояние размонтированного компонента — классический анти-паттерн.
- Жизненный цикл и практика проектирования
Ключевые моменты практичного использования:
- Разделяйте:
- render (чистая функция от props/state),
- side effects (внутри
useEffect/useLayoutEffect).
- Используйте зависимости в
useEffectчестно:- всё, что используется внутри эффекта из внешней области, должно быть в массиве зависимостей,
- иначе — неконсистентное поведение.
- Для тяжёлых вычислений:
useMemoдля мемоизации результатов,useCallbackдля стабилизации колбэков,- но только при реальной пользе (не "по дефолту").
- Для подписок:
- один эффект = одна ответственность:
- один — подписка на resize,
- другой — на WebSocket,
- это упрощает cleanup и отладку.
- один эффект = одна ответственность:
- В сложных случаях:
- собственные хуки (custom hooks) для инкапсуляции повторяющихся жизненных циклов — работы с API, WebSocket, формами и т.д.
- Мост к классовому API (для полноты)
Если сравнивать с классами (важно для чтения легаси-кода):
- Монтирование:
constructorcomponentDidMount
- Обновление:
componentDidUpdateshouldComponentUpdate
- Размонтирование:
componentWillUnmount
В функциональных компонентах всё это выражается комбинацией:
useState/useReducer(управление состоянием),useEffect/useLayoutEffect(эффекты + cleanup),memo/useMemo/useCallback(оптимизации).
Итого: Жизненный цикл компонента в современном React — это управление:
- тем, что рендерится (чистая функция),
- когда и как выполняются побочные эффекты,
- как корректно очищать ресурсы.
useEffectс правильно подобранными зависимостями и cleanup — ключевой инструмент для точного контроля над монтированием, обновлением и размонтированием.
Вопрос 22. В каких случаях React-компонент повторно рендерится?
Таймкод: 00:14:45
Ответ собеседника: правильный. Говорит, что повторный рендер происходит при изменении собственного состояния, обновлении пропсов из родителя и при рендере родителя; дополнительно упоминает влияние React.memo.
Правильный ответ:
Повторный рендер React-компонента происходит, когда React считает, что результат функции/метода рендера потенциально мог измениться. Для функциональных и классовых компонентов базовые триггеры одинаковы.
Ключевые случаи:
- Изменение собственного состояния компонента
- Для функциональных компонентов:
- вызов
setState-подобных хук-функций:setXизuseState,dispatchизuseReducer,- обновление состояния в других кастомных хуках.
- вызов
- Для классовых:
- вызов
this.setState(...).
- вызов
Особенности:
- Если новый стейт строго равен старому (по
Object.is), React может пропустить повторный рендер. - Не мутировать состояние "на месте":
- изменения должны создавать новые значения (иммутабельность), иначе React может не увидеть изменений или вы сами запутаетесь в логике.
Пример (функциональный компонент):
function Counter() {
const [count, setCount] = useState(0);
const inc = () => setCount(c => c + 1); // триггерит ререндер
return <button onClick={inc}>{count}</button>;
}
- Изменение пропсов от родительского компонента
- Если родительский компонент заново отрендерился и передал в дочерний компонента новые пропсы (с точки зрения сравнения), дочерний:
- по умолчанию тоже будет рендериться.
- Даже если значения "логически те же", но ссылки новые (например, новые объекты/функции при каждом рендере) — это воспринимается как изменение пропсов.
Пример:
function Parent({ value }) {
const data = { value }; // новый объект при каждом рендере
return <Child data={data} />;
}
Без оптимизаций Child будет рендериться при каждом рендере Parent, даже если value не менялся, потому что data — новый объект.
- Ререндер родителя (даже при тех же пропсах), если нет оптимизации
- По умолчанию:
- если родитель рендерится, его дети тоже вызываются (их функция/
render) для построения нового дерева.
- если родитель рендерится, его дети тоже вызываются (их функция/
- React сравнивает новое дерево с предыдущим (reconciliation) и минимально обновляет DOM, но сам вызов функции компонента уже считается ререндером на уровне React.
Оптимизация через React.memo:
React.memo(Component)делает поверхностное сравнение пропсов:- если пропсы не изменились (shallow compare), React пропускает повторный рендер компонента.
- Работает только для функциональных компонентов.
- Важно: если передаются новые объекты/функции каждый раз,
memoне поможет без стабилизации (useMemo,useCallback).
const Child = React.memo(function Child({ value }) {
console.log('render Child');
return <div>{value}</div>;
});
- Контекст (useContext / Context API)
- Компонент, использующий
useContext(SomeContext)илиContext.Consumer, повторно рендерится при изменении значения контекста. - Все потребители контекста, подписанные на этот контекст, рендерятся заново, независимо от
React.memo, если толькоmemoне оборачивает и не используется грамотная структура контекстов.
- Изменение ключей (key) у элементов/компонентов
- Если меняется
keyкомпонента в списке:- React воспринимает это как "старый компонент размонтирован, новый смонтирован".
- Это не просто ререндер, а уничтожение одного экземпляра и создание нового (со сбросом состояния).
- Неправильный/нестабильный
key(например, индекс массива при изменяемых списках) приводит к лишним рендерам и багам.
- Внешние факторы и принудительные обновления
- Для классовых компонентов:
this.forceUpdate()→ форсирует ререндер независимо от сравнения пропсов/состояния.
- Для интеграций с внешними сторами (Redux/Zustand/MobX и др.):
- подписка на изменения состояния стора,
- если селектор вернул новое значение → компонент рендерится.
- Что НЕ вызывает ререндер напрямую:
- Мутация объекта/массива в пропсах или состоянии без создания нового значения:
- React смотрит на ссылку, а не "глубокое" содержимое.
- Но если вы потом вызываете
setStateс тем же объектом по ссылке — рендера может не быть.
- Изменение DOM напрямую через
document.querySelectorи т.п.:- React об этом не знает, ререндер по этому поводу не произойдёт.
- Promise/таймер/событие сам по себе:
- только когда внутри колбэка вызывается
setStateили обновляется хранилище.
- только когда внутри колбэка вызывается
Практические рекомендации:
- Явно понимать триггеры:
- изменение state,
- изменение props,
- изменение context.
- Использовать:
- иммутабельные обновления (новые объекты/массивы),
React.memoдля "чистых" компонент, чувствительных к пропсам,useCallback/useMemoдля стабилизации пропсов-функций и вычисляемых значений.
- Не бояться рендеров:
- основной фокус — на корректности и читаемости,
- оптимизации включать точечно, когда замерили реальную проблему.
Вопрос 23. Какими способами можно избежать выполнения тяжёлых вычислений при каждом рендере компонента в React?
Таймкод: 00:15:56
Ответ собеседника: правильный. Предлагает использовать useMemo с функцией вычисления и массивом зависимостей для пересчёта результата только при изменении зависимостей.
Правильный ответ:
useMemo — базовый и правильный ответ, но набор приёмов шире. Цель — не допускать выполнения тяжёлых операций на каждом рендере без необходимости, минимизируя перерасчёты и пересоздание значений.
Основные подходы:
- Мемоизация вычислений через useMemo
- Используется для кэширования результата ресурсоёмкой функции на основе набора зависимостей.
- Вычисление выполняется только:
- при первом рендере,
- при изменении любой зависимости в массиве.
Пример:
import { useMemo } from 'react';
function HeavyComponent({ items }) {
const total = useMemo(() => {
// тяжёлое вычисление, O(n^2), парсинг, агрегации и т.п.
return items.reduce((acc, x) => acc + x.value, 0);
}, [items]);
return <div>Total: {total}</div>;
}
Важно:
useMemoдолжен оборачивать реально тяжёлую/дорогую операцию, а не всё подряд.- Зависимости должны быть корректными: если результат зависит от
items, то:- либо
itemsдолжен быть стабилизирован (см. ниже), - либо принимаем перерасчёт при каждой смене ссылки.
- либо
- Стабилизация пропсов и колбэков: useCallback, useMemo
Тяжёлые вычисления часто спрятаны в дочерних компонентах. Даже с React.memo дочерний компонент будет рендериться, если пропсы каждый раз новые (например, новые функции или объекты).
Способы оптимизации:
useCallbackдля функций:
const handleClick = useCallback(() => {
// логика
}, [/* зависимости */]);
useMemoдля составных пропсов (объекты, массивы):
const config = useMemo(() => ({
pageSize,
filters
}), [pageSize, filters]);
Это:
- уменьшает количество лишних рендеров дочерних компонент,
- косвенно сокращает количество повторных тяжёлых вычислений внутри них.
- Мемоизация самих компонент: React.memo
Если компонент:
- "чистый" (его вывод зависит только от пропсов),
- внутри него есть тяжелые вычисления,
- и пропсы редко меняются,
то разумно обернуть его в React.memo:
const HeavyList = React.memo(function HeavyList({ items }) {
console.log('render HeavyList');
const total = useMemo(() => {
// тяжёлое вычисление
return items.reduce((acc, x) => acc + x.value, 0);
}, [items]);
return <div>{total}</div>;
});
React.memo:
- сравнивает пропсы по поверхностному сравнению,
- если не изменились — пропускает рендер и, соответственно, тяжёлые вычисления.
При сложных пропсах:
- используем
useMemo/useCallbackв родителе, чтобы не генерировать новые объекты/функции на каждый рендер.
- Вынесение тяжёлых вычислений за пределы рендера
Если результат:
- не зависит от
state/props, либо зависит от стабильных, редко меняющихся данных, - или относится к статической конфигурации,
его можно посчитать:
- один раз при модуле,
- или один раз при монтировании.
Примеры:
- Расчёт констант на уровне модуля:
const heavyStaticData = computeOnceOnLoad();
function Component() {
return <Chart data={heavyStaticData} />;
}
- Через
useEffect+useState, если вычисление асинхронное или может блокировать рендер:
function Component({ input }) {
const [result, setResult] = useState(null);
useEffect(() => {
let cancelled = false;
(async () => {
const r = await heavyAsyncCalculation(input);
if (!cancelled) setResult(r);
})();
return () => {
cancelled = true;
};
}, [input]);
if (result == null) return <div>Loading...</div>;
return <div>{result}</div>;
}
Так тяжёлая логика уходит из синхронного рендера.
- Деление вычислений и отложенное выполнение
Для реально тяжёлых задач:
- разбивать вычисления на чанки и выполнять поэтапно через:
setTimeout,requestIdleCallback,requestAnimationFrame,- Web Workers (вынесение вычислений из UI-потока).
Пример с requestIdleCallback (грубый набросок):
function useIdleHeavyCompute(input) {
const [result, setResult] = useState(null);
useEffect(() => {
let id;
function schedule() {
if ('requestIdleCallback' in window) {
id = requestIdleCallback(deadline => {
const r = heavyCalculation(input);
setResult(r);
});
} else {
id = setTimeout(() => {
const r = heavyCalculation(input);
setResult(r);
}, 0);
}
}
schedule();
return () => {
if ('cancelIdleCallback' in window) cancelIdleCallback(id);
else clearTimeout(id);
};
}, [input]);
return result;
}
- Web Workers для CPU-bound задач
Если вычисление:
- действительно тяжёлое (анализ больших структур, криптография, трансформации),
- и блокирует event loop,
правильный путь:
- вынести в Web Worker:
- основной поток остаётся для React и UI,
- worker считает и отправляет результат обратно.
Общий паттерн:
- React-компонент:
- отправляет данные в worker,
- слушает сообщения,
- пишет результат в state.
- Это полностью предотвращает блокировку рендера тяжёлыми вычислениями.
- Разумный баланс
Важно:
- Не злоупотреблять
useMemoиuseCallback:- они сами стоят немного вычислительных ресурсов,
- читать код становится сложнее.
- Оптимизировать:
- когда измерили проблему (profiling: React DevTools, Performance),
- и точно знаете, что вычисление тяжёлое или приводит к каскаду ререндеров.
Краткий практический чек-лист:
- Тяжёлое вычисление в компоненте:
- обернуть в
useMemoс корректными зависимостями.
- обернуть в
- Компонент часто рендерится, но редко меняются пропсы:
- обернуть в
React.memo.
- обернуть в
- Пропсы-посредники (объекты/функции) постоянно создаются:
- стабилизировать через
useMemo/useCallback.
- стабилизировать через
- Очень тяжёлые CPU-задачи:
- вынести из рендера:
useEffect, батчинг, Web Workers, idle-таски.
- вынести из рендера:
Такой подход позволяет избежать ненужных тяжёлых вычислений и сохранить отзывчивость интерфейса даже при сложной логике.
Вопрос 24. На какой архитектуре основан Redux и какие основные сущности в ней используются?
Таймкод: 00:17:03
Ответ собеседника: неполный. Правильно упоминает архитектуру Flux и называет экшены, редьюсеры и состояние, но не даёт полный, структурированный набор сущностей и принципов.
Правильный ответ:
Redux вдохновлён архитектурой Flux, но является более жёсткой и упрощённой реализацией однонаправленного потока данных с рядом ключевых принципов:
- один источник истины (single source of truth);
- состояние только для чтения;
- изменения описываются чистыми функциями.
Базовые сущности и идеи:
- Store (хранилище)
- Единый объект, в котором хранится всё состояние приложения (или логически крупного домена).
- В классическом Redux — один store на всё приложение.
- Отвечает за:
- текущее состояние;
- подписку на изменения;
- диспетчеризацию (dispatch) экшенов.
Интерфейс store:
getState()— получить состояние;dispatch(action)— отправить экшен;subscribe(listener)— подписаться на изменения.
Пример:
import { createStore } from 'redux';
const store = createStore(rootReducer);
console.log(store.getState());
- State (состояние)
- Иммутабельная структура данных, описывающая текущее состояние приложения:
- UI-состояние,
- данные из API,
- флаги загрузки/ошибок и т.д.
- Менять состояние напрямую нельзя:
- только через
dispatch(action)→ reducer → новый state.
- только через
Пример структуры:
const initialState = {
user: { id: null, name: null },
todos: [],
loading: false,
error: null,
};
- Action (действие)
- Простые объекты, описывающие, "что произошло".
- Обязательное поле:
type(строка, обычно неймспейсом/константой). - Могут содержать
payloadс данными.
Примеры:
const increment = { type: 'counter/increment' };
const addTodo = (text) => ({
type: 'todos/add',
payload: { id: crypto.randomUUID(), text },
});
Особенности:
- Action — декларативное описание события, не содержит логики изменения state.
- Последовательный поток: UI/сайд-эффекты → dispatch(action) → reducer.
- Reducer (редьюсер)
- Чистая функция, описывающая, как меняется состояние в ответ на экшен:
- не мутирует аргументы,
- не имеет побочных эффектов,
- детерминирована: один и тот же state + action → один и тот же новый state.
Сигнатура:
(previousState, action) => newState
Пример:
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/increment':
return { ...state, value: state.value + 1 };
case 'counter/decrement':
return { ...state, value: state.value - 1 };
default:
return state;
}
}
Комбинация:
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
counter: counterReducer,
// другие редьюсеры
});
- Dispatch (отправка экшена)
store.dispatch(action)— единственный "официальный" способ инициировать изменение состояния.- Запускает цепочку:
dispatch(action)→ редьюсеры → новыйstate→ уведомление подписчиков (UI).
Пример:
store.dispatch({ type: 'counter/increment' });
console.log(store.getState().counter.value);
- Однонаправленный поток данных (uni-directional data flow)
Ключевой паттерн, общий для Flux и Redux:
- UI или внешнее событие инициирует действие →
dispatch(action). actionпопадает в store → редьюсер(ы) вычисляют новый state.- Обновлённый state передаётся в UI:
- в React: через
useSelector,connectи т.п.
- в React: через
- UI перерисовывается на основе нового состояния.
Схема:
UI → (dispatch action) → Store/Reducers → New State → UI
Нет "обратных" скрытых путей изменения state:
- никакого прямого "set" состояния из компонентов мимо store;
- это делает поток данных предсказуемым и хорошо трассируемым.
- Middleware (промежуточное ПО) — важная дополнительная сущность Redux
Хотя это надстройка, для полноценного ответа стоит упомянуть:
- Middleware перехватывает
dispatch(action)между моментом вызова и редьюсером. - Позволяет:
- реализовать асинхронные экшены,
- логирование,
- трассировку,
- обработку ошибок,
- интеграцию с API.
- Примеры:
redux-thunk— позволяет диспатчить функции (thunk), внутри которых можно выполнять асинхронный код и диспатчить обычные экшены.redux-saga,redux-observable— более сложные модели асинхронного управления.
Пример простого thunk:
const fetchUser = (id) => async (dispatch) => {
dispatch({ type: 'user/fetchStart' });
try {
const res = await fetch(`/api/users/${id}`);
const data = await res.json();
dispatch({ type: 'user/fetchSuccess', payload: data });
} catch (e) {
dispatch({ type: 'user/fetchError', error: e.message });
}
};
- Отличия Redux от "сырого" Flux
Redux:
- один store (в Flux часто несколько);
- нет диспетчера (dispatcher) как отдельной сущности — его роль встроена в
dispatchи редьюсеры; - поощряет чистые редьюсеры и иммутабельность;
- делает поток данных более строгим и предсказуемым.
Краткий итог:
Redux основывается на идее однонаправленного потока данных, вдохновлённой Flux, и использует ключевые сущности:
- Store — единое хранилище состояния.
- State — иммутабельное дерево состояния.
- Action — описание произошедшего.
- Reducer — чистая функция преобразования state на основе action.
- Dispatch — механизм запуска изменений.
- (Дополнительно) Middleware — слой для асинхронности и побочных эффектов.
Эта архитектура делает поведение состояния детерминированным, удобно дебажимым (time-travel, логи экшенов) и предсказуемым в сложных фронтенд-приложениях.
Вопрос 25. Опиши общий цикл движения данных в архитектуре Flux/Redux.
Таймкод: 00:17:25
Ответ собеседника: неполный. Упоминает общий store, неизменяемость и возврат нового состояния, но не даёт чёткой пошаговой последовательности: действие → диспетчеризация → редьюсер → обновление store → уведомление UI.
Правильный ответ:
В архитектуре Flux/Redux ключевой принцип — однонаправленный поток данных. Данные всегда движутся по замкнутому циклу в одном направлении, что делает поведение приложения предсказуемым и легко трассируемым.
Общий цикл (Redux-формулировка):
- Пользователь или внешнее событие инициирует действие
- Действие (action) отправляется (dispatch) в store
- Редьюсеры (reducers) вычисляют новый state на основе action
- Store сохраняет новый state
- Подписчики (UI-компоненты и др.) уведомляются и перечитывают состояние
Разберём по шагам.
- Инициирование действия (Action)
Сценарии:
- пользователь кликнул кнопку,
- пришёл ответ от сервера,
- сработал таймер,
- внутреннее событие приложения.
Компонент или middleware формирует action — обычный объект, описывающий "что произошло":
// Action creator
const addTodo = (title) => ({
type: 'todos/add',
payload: { id: crypto.randomUUID(), title },
});
В UI (например, React-компонент):
import { useDispatch } from 'react-redux';
function AddTodo() {
const dispatch = useDispatch();
const onAdd = () => {
dispatch(addTodo('Buy milk'));
};
return <button onClick={onAdd}>Add</button>;
}
- Диспетчеризация (dispatch)
dispatch(action) — единая точка входа для изменения состояния.
- Компонент, thunk, saga или другая часть системы вызывает:
store.dispatch(action).
- В чистом Redux:
dispatchсинхронно передаёт action в цепочку middleware (если есть),- затем доходит до редьюсеров.
Пример:
store.dispatch({ type: 'todos/add', payload: { id: '1', title: 'Task' } });
- Обработка в редьюсерах (Reducer)
Редьюсер — чистая функция:
(previousState, action) => newState
При каждом dispatch:
- корневой редьюсер (
rootReducer) вызывается с текущим состоянием и action; - вложенные редьюсеры (через
combineReducers) получают свои части state; - редьюсер:
- не мутирует старый state,
- на основе
action.typeвозвращает новый объект состояния.
Пример:
function todosReducer(state = [], action) {
switch (action.type) {
case 'todos/add':
return [...state, action.payload]; // новый массив
case 'todos/remove':
return state.filter(t => t.id !== action.payload.id);
default:
return state;
}
}
const rootReducer = combineReducers({
todos: todosReducer,
// другие редьюсеры
});
Важно:
- иммутабельность критична:
- старый state остаётся неизменным,
- создаётся новый объект/массив,
- это позволяет:
- легко отслеживать изменения,
- реализовывать оптимизации (
===сравнения, time-travel, логирование).
- Обновление store
После выполнения редьюсеров:
- Store сохраняет новый state.
- В Redux:
- если редьюсер вернул тот же объект (по ссылке) для части state — считается, что там изменений нет;
- если вернулся новый объект — это сигнал, что соответствующая часть изменилась.
Можно увидеть:
store.subscribe(() => {
console.log('New state:', store.getState());
});
После dispatch:
- все подписчики вызываются,
- каждый сам решает, как реагировать.
- Уведомление UI и повторный рендер
Фронтенд-слой (например, React через React-Redux):
- Компоненты подписываются на нужные части состояния:
useSelector(state => state.todos)connect(mapStateToProps)в старом API.
- При изменении state:
- селекторы выполняются,
- если выбранные данные изменились:
- соответствующий компонент повторно рендерится.
Пример с React-Redux:
import { useSelector } from 'react-redux';
function TodoList() {
const todos = useSelector(state => state.todos);
return (
<ul>
{todos.map(t => <li key={t.id}>{t.title}</li>)}
</ul>
);
}
Цикл замыкается:
- UI → dispatch(action) → reducers → новый state → UI-селекторы → рендер.
Никаких "скрытых" путей:
- Компонент не меняет store напрямую,
- бизнес-логика изменений — только в редьюсерах,
- это упрощает:
- отладку (логирование каждого action),
- воспроизводимость (time-travel debugging),
- тестирование редьюсеров как чистых функций.
Роль middleware в общем цикле
Хотя вопрос базовый, важно понимать место middleware для полноты картины:
- Middleware располагаются между
dispatchи редьюсерами. - Они могут:
- логировать action и state,
- обрабатывать асинхронность (thunk, saga, observable),
- модифицировать, отменять или проксировать экшены.
Общий поток c middleware:
UI/событие
→ dispatch(action)
→ middleware chain (логирование, асинхронность и т.п.)
→ reducers
→ новый state в store
→ уведомление подписчиков (UI)
Кратко:
- Архитектура Flux/Redux — строго однонаправленный поток:
- Action → Dispatch → Reducer → Store → View.
- Основные свойства:
- один источник истины (store),
- immutability,
- детерминированные редьюсеры,
- полная наблюдаемость изменений (лог экшенов и стейтов),
- простота reasoning'а в больших приложениях.
Вопрос 26. Как с помощью мемоизированных селекторов в Redux снизить количество тяжёлых вычислений при выборке данных из стора?
Таймкод: 00:19:59
Ответ собеседника: неполный. Вспоминает про reselect и createSelector, понимает идею мемоизации при неизменных входах, но объясняет общими словами и без практической конкретики.
Правильный ответ:
Мемоизированные селекторы используются для оптимизации вычислений "на чтение" из Redux-хранилища. Их задача — не пересчитывать тяжёлые производные данные при каждом рендере или каждом изменении стора, а повторно использовать ранее вычисленный результат, если входные данные не изменились.
Базовые идеи:
- Селектор: что это такое
Селектор — это функция, которая:
- принимает состояние (
state) (и опционально props), - возвращает "срез" или производное значение.
Простой селектор:
const selectTodos = (state) => state.todos;
Но в реальных приложениях часто нужны производные данные:
- фильтрация,
- агрегации,
- группировки,
- вычисления на основе нескольких частей стора.
Например:
const selectCompletedTodos = (state) =>
state.todos.filter(todo => todo.completed);
Если этот селектор вызывается часто (много компонентов, частые обновления), то фильтрация на каждом вызове может стать дорогой — особенно при больших массивах и сложной логике.
- Проблема без мемоизации
Без мемоизации:
- при любом обновлении стора (даже не относящемся к
todos) селектор:- будет вычисляться заново,
- вернёт новый массив,
- потенциально триггерит лишние рендеры компонентов, зависящих от этого селектора (шallow-compare видит новую ссылку).
Нам нужно:
- "запомнить" результат,
- пересчитывать только при реальном изменении входных данных.
- Reselect и createSelector
Библиотека reselect (сейчас также интегрирована концептуально в Redux Toolkit) предоставляет createSelector — фабрику мемоизированных селекторов.
Сигнатура (упрощённо):
createSelector(
[inputSelector1, inputSelector2, ...],
resultFunc
)
Где:
- inputSelector'ы — функции, которые выбирают "сырые" данные из state;
- resultFunc — функция, которая на их основе считает производное значение.
Механика:
createSelectorзапоминает:- предыдущие значения всех inputSelector'ов,
- предыдущий результат
resultFunc.
- Если при новом вызове:
- все входы (по
===) те же самые, resultFuncне вызывается,- возвращается закэшированный результат.
- все входы (по
- Если хотя бы один вход изменился:
- вызывается
resultFunc, - результат обновляется и кэшируется.
- вызывается
Пример:
import { createSelector } from 'reselect';
// "сырые" селекторы
const selectTodos = (state) => state.todos;
const selectFilter = (state) => state.filter;
// мемоизированный селектор
export const selectVisibleTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
console.log('Recalculating visible todos...');
switch (filter) {
case 'completed':
return todos.filter(t => t.completed);
case 'active':
return todos.filter(t => !t.completed);
default:
return todos;
}
}
);
Поведение:
- При каждом обновлении стора мы можем вызывать
selectVisibleTodos(state). - Реальное "тяжёлое" вычисление
filter:- выполняется только тогда, когда изменились:
state.todosилиstate.filter(по ссылке),
- при остальных изменениях стора (другие редьюсеры, части state) — вернётся старый результат без перерасчёта.
- выполняется только тогда, когда изменились:
- Это уменьшает:
- количество тяжёлых операций,
- количество лишних ререндеров компонентов, подписанных через
useSelector.
- В связке с React: снижение ререндеров
Компонент:
import { useSelector } from 'react-redux';
import { selectVisibleTodos } from './selectors';
function TodoList() {
const todos = useSelector(selectVisibleTodos);
return (
<ul>
{todos.map(t => <li key={t.id}>{t.text}</li>)}
</ul>
);
}
Механика:
useSelectorпо умолчанию сравнивает результат селектора через строгое сравнение===.- Если
selectVisibleTodos:- вернул то же значение по ссылке (из кэша),
- компонент не будет повторно рендериться.
- Если бы мы без мемоизации делали:
const visible = state.todos.filter(...);
то:
- на каждое изменение state создавался бы новый массив,
===всегда был бы false,- компонент рендерился бы каждый раз.
- Типичные кейсы для мемоизированных селекторов
Использовать createSelector особенно эффективно, когда:
- есть тяжёлые вычисления:
- фильтрация/поиск по большим коллекциям,
- агрегации (sum, groupBy, статистика),
- компоновка сложных структур из нескольких частей state;
- одно и то же производное значение нужно в нескольких компонентах;
- важно минимизировать количество ререндеров при частых обновлениях стора.
Пример более сложного селектора:
const selectUsers = state => state.users;
const selectOrganizations = state => state.organizations;
const selectOrgId = (_, orgId) => orgId;
export const selectUsersByOrg = createSelector(
[selectUsers, selectOrgId],
(users, orgId) => users.filter(u => u.orgId === orgId)
);
Здесь:
- поддерживается мемоизация на последних входных аргументах (для более сложных кейсов можно использовать "многоключевую" или кастомную мемоизацию).
- Ограничения и best practices
createSelector(по умолчанию) кэширует результат только для последнего набора входных значений.- Для большинства UI-кейсов этого достаточно.
- Для более сложного кейсинга (разные аргументы, множество orgId) можно:
- использовать селекторы-фабрики (создавать селектор на компонент/контекст),
- или расширенную мемоизацию.
- Входные селекторы должны быть "чистыми":
- не модифицировать state,
- не иметь побочных эффектов.
- Эффект настоящей оптимизации есть, когда:
- реально есть тяжёлые вычисления или большая нагрузка,
- а не просто "на всякий случай вокруг каждого селектора".
Краткий итог:
- Мемоизированные селекторы (через
reselect/createSelector):- принимают state (и аргументы),
- вычисляют производное значение,
- кэшируют результат на основе входных значений.
- При неизменных входах:
- не выполняют тяжёлые вычисления,
- возвращают прежний объект/массив по той же ссылке.
- В связке с
useSelector/connect:- снижают количество перерасчётов и ререндеров,
- делают работу с большими данными и сложными производными селекторами эффективной и предсказуемой.
Вопрос 27. Почему изменение значения счётчика через useRef не приводит к обновлению интерфейса и как корректно реализовать работу счётчика?
Таймкод: 00:22:53
Ответ собеседника: правильный. Отмечает, что useRef позволяет изменять значение без повторного рендера, и для обновления счётчика нужно использовать useState.
Правильный ответ:
useRef и useState решают принципиально разные задачи в React, и понимание этой разницы критично для корректной работы с UI.
- Почему useRef не вызывает ререндер
useRefвозвращает объект вида:const ref = useRef(initialValue);
// ref = { current: initialValue }- Ключевые свойства:
ref.currentможно изменять как обычное мутируемое поле.- Изменение
ref.current:- не отслеживается React,
- не приводит к повторному рендеру компонента.
useRefзадуман для:- хранения "мутабельного контейнера" между рендерами:
- ID таймера,
- предыдущие значения,
- кэш, временные флаги,
- ссылки на DOM-элементы,
- без влияния на жизненный цикл рендера.
- хранения "мутабельного контейнера" между рендерами:
Пример некорректного счётчика:
function CounterWrong() {
const countRef = useRef(0);
const inc = () => {
countRef.current += 1;
// React не знает, что нужно перерисоваться
};
return (
<div>
<p>Count: {countRef.current}</p>
<button onClick={inc}>+</button>
</div>
);
}
Поведение:
- По клику
countRef.currentменяется, - но UI остаётся прежним, потому что компонент не был перерендерен.
- Как корректно реализовать счётчик: useState
Чтобы UI обновлялся:
- нужно использовать механизм, который говорит React: "состояние изменилось".
- Это
useState(илиuseReducer), который:- хранит значение,
- при изменении через
setStateинициирует ререндер.
Правильный пример счётчика:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const inc = () => {
setCount(prev => prev + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={inc}>+</button>
</div>
);
}
Здесь:
setCount:- меняет состояние,
- триггерит повторный рендер,
- новый
countпопадает в JSX, - UI синхронизирован с логикой.
- Когда уместно комбинировать useState и useRef
Иногда нужно:
- и управлять отображаемым значением,
- и иметь вспомогательное "техническое" значение, которое не должно вызывать рендеры.
Пример: счётчик кликов, но мы хотим логгировать количество кликов в консоль часто, не перегружая UI рендерами:
import { useState, useRef } from 'react';
function SmartCounter() {
const [count, setCount] = useState(0);
const realClicks = useRef(0); // технический счётчик
const inc = () => {
realClicks.current += 1; // не вызывает ререндер
if (realClicks.current % 5 === 0) {
setCount(prev => prev + 5); // UI обновляем порционно
}
};
return (
<div>
<p>Visible count (each 5 clicks): {count}</p>
<button onClick={inc}>Click</button>
</div>
);
}
realClicks.current:- используется как внутренний счётчик (мутабельное состояние),
- не влияет на UI напрямую.
count:- влияет на рендер,
- обновляется через
setCount.
- Краткий практический вывод
useRef:- использовать для:
- DOM-рефов,
- хранения мутабельных значений между рендерами,
- оптимизаций и служебных данных,
- не использовать как механизм управления отображаемым состоянием.
- использовать для:
useState:- использовать для данных, которые должны быть отражены в UI:
- счётчики,
- флаги, инфо из API и т.п.
- использовать для данных, которые должны быть отражены в UI:
Правильная реализация счётчика — через состояние (useState или useReducer). useRef подходит только как дополнение, если нужно хранить вспомогательные значения без триггера рендера.
Вопрос 28. Что не так в реализации выбора цвета и как оптимизировать код, убрав лишнее состояние и useEffect?
Таймкод: 00:25:05
Ответ собеседника: правильный. Замечает дублирование состояния выбранного цвета и соглашается, что достаточно одного состояния и обновления прямо в обработчике без useEffect.
Правильный ответ:
Типичная ошибка при реализации выбора цвета в React — избыточное состояние и лишняя связка через useEffect. Часто встречается такой паттерн:
- есть проп
selectedColor, - есть локальное состояние
color, - есть
useEffect, который синхронизирует одно с другим, - есть обработчик, который сначала меняет одно, потом побочно обновляет другое.
Это приводит к:
- дублированию источников истины (две переменные описывают одно и то же),
- риску рассинхронизации,
- лишним рендерам,
- ненужному усложнению кода.
Как правило, нужно:
-
Определить один источник истины (single source of truth):
- либо управляемый компонент (цвет хранится во внешнем состоянии, передаётся через пропсы и изменяется коллбэком),
- либо локальное состояние внутри компонента (контролируем только там).
-
Убрать
useEffect, который "синхронизирует" два состояния, если они описывают одно и то же.
Разберём два корректных варианта.
Вариант 1: Управляемый компонент (стейт выше по дереву)
Компонент выбора цвета не хранит состояние, он просто:
- показывает текущий цвет из пропсов,
- сообщает наружу о выборе.
function ColorPicker({ value, onChange }) {
const colors = ['red', 'green', 'blue'];
return (
<div>
{colors.map((color) => (
<button
key={color}
style={{
backgroundColor: color,
border: value === color ? '2px solid black' : '1px solid #ccc',
}}
onClick={() => onChange(color)}
>
{color}
</button>
))}
</div>
);
}
// Родитель:
function App() {
const [color, setColor] = useState('red');
return (
<>
<ColorPicker value={color} onChange={setColor} />
<div>Selected: {color}</div>
</>
);
}
Здесь:
- нет дублирования состояния;
- нет
useEffectдля "синхронизации"; - один источник истины — состояние в родителе.
Вариант 2: Локальный стейт, без лишних эффектов
Если компонент сам управляет цветом, достаточно одного useState и прямого обновления в обработчике:
function ColorPicker() {
const [color, setColor] = useState('red');
const colors = ['red', 'green', 'blue'];
const handleSelect = (newColor) => {
setColor(newColor);
};
return (
<div>
{colors.map((c) => (
<button
key={c}
style={{
backgroundColor: c,
border: c === color ? '2px solid black' : '1px solid #ccc',
}}
onClick={() => handleSelect(c)}
>
{c}
</button>
))}
<div>Selected: {color}</div>
</div>
);
}
Нет useEffect, нет промежуточного состояния. Обработчик напрямую меняет стейт.
Чего точно стоит избегать:
// Анти-паттерн
const [selectedColor, setSelectedColor] = useState('red');
const [localColor, setLocalColor] = useState('red');
useEffect(() => {
setLocalColor(selectedColor);
}, [selectedColor]);
useEffect(() => {
onChange(localColor);
}, [localColor]);
Почему это плохо:
- два состояния про одно и то же;
- два эффекта, которые "гоняют" значения туда-сюда;
- легко получить циклы, лаги, рассинхрон.
Итоговая рекомендация:
- Должен быть один источник истины для выбранного цвета.
- Обновление делать:
- либо напрямую в
onClickчерезsetState, - либо через вызов
onChangeв управляемом компоненте.
- либо напрямую в
useEffectиспользовать только для реальных побочных эффектов (логирование, запросы, синхронизация с внешней системой), а не для зеркалирования одного состояния в другое.
Вопрос 29. Почему таймер со стартом и стопом работает некорректно и как исправить остановку интервала?
Таймкод: 00:27:49
Ответ собеседника: правильный. Указывает, что необходимо вызывать clearInterval с id интервала для остановки таймера, и отмечает важность очистки при размонтировании.
Правильный ответ:
Проблемы с таймерами в React обычно связаны с двумя вещами:
- интервал продолжается, потому что его id не сохраняется или неправильно очищается;
- создаётся несколько интервалов (дубликаты), которые не останавливаются корректно;
- используется устаревшая ссылка на состояние (stale state), если интервал объявлен без учёта замыканий.
Ключевые моменты корректной реализации:
- Как работает setInterval / clearInterval
setInterval(fn, delay)возвращает числовой id (в браузере) или объект/число (в разных окружениях).- Чтобы остановить интервал:
- необходимо вызвать
clearInterval(id), гдеid— ровно тот идентификатор, который вернулsetInterval.
- необходимо вызвать
- Если id:
- не сохранён,
- перезаписан,
- живёт только в локальной переменной внутри функции, вы не сможете корректно остановить конкретный интервал.
Неправильный пример (типичный):
function TimerBad() {
let intervalId; // на каждом рендере новый
const start = () => {
intervalId = setInterval(() => {
console.log('tick');
}, 1000);
};
const stop = () => {
clearInterval(intervalId); // может быть undefined или не тот id
};
return (
<>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
Проблемы:
intervalId— обычная переменная функции компонента:- при каждом рендере создаётся новый, старый теряется;
stopможет видеть не тот id илиundefined;
- возможны "залипшие" интервалы, которые продолжают работать.
- Правильное хранение id интервала: useRef
Нужно сохранять id в стабильном контейнере, который переживает рендеры, но не вызывает новый рендер при изменении — это задача useRef:
import { useRef, useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null); // здесь храним id интервала
const start = () => {
if (intervalRef.current !== null) return; // уже запущен, не создаём второй
intervalRef.current = setInterval(() => {
setSeconds((s) => s + 1); // безопасно: используем функциональный setState
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
// Очистка при размонтировании компонента:
useEffect(() => {
return () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<div>{seconds} сек</div>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
Почему это корректно:
intervalRef.current:- один и тот же объект между рендерами,
- надёжно хранит id интервала.
start:- не создаёт новый интервал, если старый ещё работает (проверка на
null); - избегает накопления множества интервалов.
- не создаёт новый интервал, если старый ещё работает (проверка на
stop:- вызывает
clearIntervalс актуальным id; - сбрасывает
intervalRef.current, чтобы отразить, что интервала больше нет.
- вызывает
useEffectcleanup:- при размонтировании компонента:
- очищает интервал,
- предотвращает утечки памяти и попытки вызвать
setStateпосле unmount.
- при размонтировании компонента:
- Частая логическая ошибка: stale state внутри интервала
Даже при правильном clearInterval, часто делают так:
setInterval(() => {
setSeconds(seconds + 1); // захватывается старое значение seconds
}, 1000);
Это неправильно:
- замыкание фиксирует значение
secondsна момент создания интервала, - в дальнейшем прибавляется одно и то же базовое значение.
Правильный вариант — использовать функциональное обновление:
setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
Функция в setSeconds всегда получает актуальное предыдущее значение, независимо от того, когда была создана.
- Итог:
Чтобы таймер со стартом/стопом работал корректно:
- Храните id интервала в
useRef, а не в локальной переменной компонента. - При старте:
- создавайте интервал только если он ещё не запущен.
- При стопе:
- вызывайте
clearIntervalс тем самым id и сбрасывайтеref.
- вызывайте
- Всегда очищайте интервалы при размонтировании (cleanup в
useEffect). - Внутри интервала используйте функциональный
setState, чтобы избежать проблем с устаревшим состоянием.
Такая реализация предсказуема, не создаёт висящих интервалов и корректно управляет жизненным циклом таймера в React.
Вопрос 30. Какой скрытый баг есть в обработчике события resize и как его устранить?
Таймкод: 00:29:19
Ответ собеседника: правильный. После подсказок формулирует, что обработчик события resize нужно удалять при размонтировании/изменении, иначе возникает утечка из-за навешивания слушателя без отписки.
Правильный ответ:
Скрытый баг при работе с window.addEventListener('resize', ...) в React (и не только) — отсутствие отписки от события. Если на каждом монтировании компонента, изменении зависимостей или повторном вызове эффекта навешивать новый обработчик без удаления старого, возникают:
- утечки памяти (висящие ссылки на компоненты и замыкания);
- дублирование логики (один и тот же обработчик вызывается многократно);
- некорректное поведение UI (рост частоты вызовов, "скачки" значений, деградация производительности).
Типичный анти-паттерн:
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// нет removeEventListener — каждый раз добавляем новый обработчик
}, []); // или, что хуже, без массива зависимостей
Если массив зависимостей отсутствует либо зависит от значений, которые часто меняются:
- каждый вызов эффекта добавляет новый listener;
- старый остаётся и продолжает вызываться.
Правильная реализация — всегда чистить обработчик в cleanup-функции эффекта:
import { useEffect, useState } from 'react';
function UseWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// cleanup: снимаем обработчик при размонтировании или перед повторным эффектом
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // зависимости пустые: навешиваем один раз
return <div>Width: {width}</div>;
}
Ключевые моменты:
- Всегда использовать
removeEventListenerс тем же колбэком, который был передан вaddEventListener. - Размещать подписку и отписку внутри одного
useEffect, возвращая функцию очистки:- это гарантирует:
- снятие слушателя при размонтировании,
- корректное обновление слушателя, если завязан на зависимости.
- это гарантирует:
- Если эффект зависит от каких-либо значений (например, текущего breakpoints-конфига), при изменении зависимостей:
- React сначала вызывает cleanup (removeEventListener),
- затем запускает эффект заново с новыми параметрами.
Таким образом устраняется скрытый баг:
- не накапливаются "мертвые" слушатели;
- не происходит экспоненциального роста количества вызовов обработчика;
- состояние компонента и слушатели остаются согласованными по всему жизненному циклу.
Вопрос 31. Был ли у тебя опыт поиска утечек памяти и работы с подобными багами в JavaScript/React?
Таймкод: 00:31:55
Ответ собеседника: правильный. Кратко подтверждает наличие опыта работы с утечками памяти и подобными проблемами.
Правильный ответ:
Корректный расширенный ответ на этот вопрос — не просто "да", а кратко описать подходы, инструменты и типовые кейсы. Это демонстрирует реальное понимание, а не формальное знакомство.
Основные направления и практики при поиске утечек памяти в JavaScript/React:
- Типичные источники утечек памяти
- Неотписанные обработчики событий:
window,document,DOM-элементы,WebSocket,EventEmitter.- В React: отсутствие cleanup в
useEffect.
- Таймеры и интервалы:
setInterval,setTimeout,requestAnimationFrame,requestIdleCallback, не очищенные при размонтировании.
- Долгоживущие замыкания:
- ссылки на крупные объекты, хранящиеся в замыканиях, которые продолжают жить из-за глобальных ссылок или подписок.
- Кэши и глобальные структуры:
Map,Set, объекты-кэши, куда добавляют, но откуда не удаляют.
- Некорректная работа с порталами и сторонними библиотеками:
- когда библиотека создаёт DOM/слушатели вне React-дерева и не чистит их.
- Подход к поиску утечек
- Воспроизводимость:
- найти сценарий, при котором при навигации/открытии/закрытии страницы/модалки использование памяти стабильно растёт и не возвращается.
- Инструменты браузера (Chrome DevTools / аналогичные):
- вкладка Performance:
- записать профили при повторяющихся действиях;
- проверить рост количества нод/слушателей/объектов.
- вкладка Memory:
- heap snapshots до и после повторения сценария (например, 10 раз открыть/закрыть модалку);
- анализ "Detached DOM trees";
- поиск объектов, которые не должны жить, но удерживаются ссылками.
- вкладка Performance:
- Анализ зависимостей:
- провериться, что все эффекты
useEffect/useLayoutEffectимеют корректный cleanup:removeEventListener,clearInterval/clearTimeout,- завершение/отписка от WebSocket/stream/observable.
- провериться, что все эффекты
Пример корректного cleanup в React:
useEffect(() => {
const handler = () => { /* ... */ };
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}, []);
- Практические приёмы и паттерны предотвращения
- Всегда:
- использовать cleanup-функции в эффектах;
- не держать в замыканиях больше, чем необходимо;
- аккуратно работать с глобальными синглтонами и кэшами;
- следить за тем, что компоненты, которые должны "умирать", реально размонтируются, а не остаются скрытыми только визуально.
- Для асинхронных операций:
- отменять запросы или помечать результаты как неактуальные:
useEffect(() => {
let cancelled = false;
(async () => {
const res = await fetch('/api/data');
if (!cancelled) {
// безопасно обновляем состояние
}
})();
return () => {
cancelled = true;
};
}, []);
- В сложных случаях:
- использовать профилировщик React DevTools для поиска "висящих" компонент, которые не размонтируются;
- проверять, нет ли постоянных перерендеров, держащих ссылки на большие структуры.
- Краткая форма хорошего ответа на интервью
Если отвечать компактно:
- Да, был опыт поиска и исправления утечек памяти в JS/React.
- Типовые кейсы:
- неотписанные события, таймеры, WebSocket'ы;
- утечки через замыкания и кэши.
- Инструменты:
- Chrome DevTools (Memory/Performance, heap snapshots),
- React DevTools для анализа дерева компонент.
- Практика:
- всегда делаю cleanup в
useEffect, - слежу за тем, чтобы долгоживущие структуры не удерживали лишние ссылки,
- проверяю жизненный цикл компонент при навигации и повторяющихся сценариях.
- всегда делаю cleanup в
Такой ответ демонстрирует не только факт опыта, но и понимание методологии и инструментов.
Вопрос 32. Определи порядок выполнения и вывод значений переменной в примере с Promise, setTimeout и then, учитывая работу event loop.
Таймкод: 00:33:20
Ответ собеседника: правильный. Пошагово описывает: сначала синхронный вывод (5), затем после изменения переменной (25), потом микрозадача then (30), затем макрозадача setTimeout с итоговым значением; после подсказки учитывает влияние вложенного setTimeout внутри Promise.
Правильный ответ:
Корректный разбор таких задач должен опираться на чёткое понимание:
- что выполняется синхронно,
- что попадает в очередь микрозадач (microtasks),
- что попадает в очередь макрозадач (macrotasks),
- в каком порядке event loop обрабатывает эти очереди.
Разберём типичный пример (упрощённый, но показательный). Предположим такой код:
let x = 5;
console.log(x);
setTimeout(() => {
console.log('timeout 1', x);
}, 0);
const p = new Promise((resolve) => {
x = 25;
console.log(x);
setTimeout(() => {
x = 35;
console.log('timeout 2', x);
resolve();
}, 0);
});
p.then(() => {
x = 30;
console.log('then', x);
});
console.log('end', x);
Давайте по шагам.
- Синхронный код, один call stack
Все синхронные операции выполняются сразу, до любых then/setTimeout:
-
Инициализация:
let x = 5;
-
console.log(x);- выводит
5.
- выводит
-
setTimeout(..., 0);- колбэк планируется в очередь макротасок (таймеров), не выполняется сейчас.
-
Создание
Promise:В executor промиса (функция в
new Promise((resolve) => { ... })) всё внутри выполняется синхронно:x = 25;console.log(x);- выводит
25.
- выводит
- Внутренний
setTimeout:- планирует второй таймер (макротаску) — колбэк с
x = 35; console.log(...); resolve();
- планирует второй таймер (макротаску) — колбэк с
Важно: сам
resolveещё не вызван (он будет внутри второго таймера), значит promise покаpending. -
p.then(...):- обработчик
thenрегистрируется как микрозадача, но активируется только после перехода промиса вfulfilled. - Прямо сейчас не вызывается.
- обработчик
-
console.log('end', x);- в этот момент
xуже 25, - выводит:
end 25.
- в этот момент
Итог синхронной фазы (порядок вывода):
525end 25
- Переход к макротаскам (таймеры)
После завершения всего синхронного кода:
- Event loop:
- берёт первую макротаску из очереди таймеров.
Первая макротаска: внешний setTimeout(..., 0):
setTimeout(() => {
console.log('timeout 1', x);
}, 0);
- В этот момент
xвсё ещё25. - Лог:
timeout 1 25.
Далее следующая макротаска: внутренний setTimeout внутри промиса:
setTimeout(() => {
x = 35;
console.log('timeout 2', x);
resolve();
}, 0);
Выполняем:
x = 35;console.log('timeout 2', x);- выводит:
timeout 2 35.
- выводит:
resolve();- переводит промис
pв состояниеfulfilled. - Это ставит выполнение
.then(...)в очередь микрозадач.
- переводит промис
Важно:
- Обработчик
thenне вызывается немедленно внутри таймера. - Он попадает в microtask queue и будет выполнен после окончания текущей макротаски, но до следующей.
- Обработка микрозадач
После завершения текущей макротаски (второго setTimeout):
- Event loop:
- выполняет все микрозадачи из очереди microtasks.
Микрозадача — p.then(...):
p.then(() => {
x = 30;
console.log('then', x);
});
Выполняем:
x = 30;console.log('then', x);- выводит:
then 30.
- выводит:
- Итоговый порядок вывода
Собираем последовательность:
5(синхронно)25(синхронно внутри executor промиса)end 25(синхронно)timeout 1 25(первая макротаска — внешний setTimeout)timeout 2 35(вторая макротаска — внутренний setTimeout в промисе)then 30(микрозадача — обработчик промиса после resolve)
Ключевые принципы, которые нужно показать в ответе:
- Весь код вне колбэков промисов и таймеров выполняется синхронно, сверху вниз.
- Executor
new Promise(...)— синхронный: всё внутри него выполняется сразу. setTimeoutвсегда создаёт макротаску (task), её колбэк выполнится после завершения текущего стека.resolveпромиса:- не вызывает
.thenсразу; - ставит выполнение
.thenв очередь микрозадач.
- не вызывает
- Порядок:
- завершить текущий call stack (синхронный код),
- взять одну макротаску,
- после неё выполнить все микрозадачи,
- затем следующая макротаска, и так далее.
Хороший ответ на интервью:
- Чётко разделяет:
- синхронный код,
setTimeout(макротаски),Promise.then(микротаски).
- Пошагово проговаривает:
- значения переменной на каждом этапе,
- когда именно происходит
resolve, - почему
thenвыполняется после соответствующегоsetTimeout, а не раньше.
