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

РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ / Middle - Senior FRONTEND разработчик - ГАЗПРОМ

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

Сегодня мы разберем живое техническое собеседование по фронтенду, в котором кандидат уверенно проходит базовые темы по 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 — подвал страницы или секции.
  • h1h6 — заголовки уровней, задают иерархию документа.
  • figure / figcaption — медиа с подписью.
  • time — временные метки.
  • mark, strong, em, code и др. — уточняют смысл текста (акцент, важность, код и т.п.).

Задачи, которые решает семантическая разметка:

  1. Структурированность и понятность документа

    • Код становится самодокументируемым: по HTML можно понять структуру и назначение блоков без CSS.
    • Упрощает поддержку, рефакторинг и командную разработку — меньше "магических" div без смысла.
  2. Доступность (a11y)

    • Скринридеры и другие assistive-технологии используют семантические теги и заголовки для построения карты документа.
    • Пользователь с ограничениями может быстро переходить между nav, main, article, заголовками, не прослушивая весь контент подряд.
    • Улучшает UX для клавиатурной навигации.
  3. SEO и богатое индексирование

    • Поисковые системы легче определяют:
      • где основной контент (main, article),
      • где навигация (nav),
      • где второстепенная информация (aside).
    • Это повышает качество индексации, понимание тематики и потенциально влияет на ранжирование.
    • Семантика — основа для корректного использования структурированных данных (schema.org и др.)
  4. Машиночитаемость и интеграции

    • Семантически правильный HTML легче парсить не только поисковикам, но и любым сервисам:
      • парсеры новостей,
      • превью ссылок,
      • скрейперы,
      • внутренние инструменты аналитики.
    • Это уменьшает количество хрупких "костылей", завязанных на классы и произвольные структуры.
  5. Лучшая основа для стилей и компонентного подхода

    • Явная структура позволяет проще строить дизайн-систему и компонентные библиотеки.
    • Семантические элементы можно комбинировать с 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
    Нижняя часть страницы или секции: копирайт, ссылки, контактная информация, доп. навигация.

  • h1h6
    Семантические заголовки уровней, формирующие иерархию документа.

  • 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 &lt;token&gt;</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) и возможностью переопределения.

Основные различия.

  1. Область видимости
  • 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
}
  1. Hoisting (поднятие)

Все три — var, let, const — поднимаются интерпретатором, но ведут себя по-разному.

  • var
    • Объявление поднимается вверх области видимости (функция или глобальная).
    • До фактического места объявления переменная существует со значением undefined.
    • Это позволяет "использовать до объявления", но создаёт скрытые баги.
console.log(a); // undefined, а не ошибка
var a = 10;
  • let и const
    • Также поднимаются, но попадают в "temporal dead zone" (TDZ):
      • от начала области до строки объявления переменной её использование запрещено.
      • Любой доступ до фактического объявления приводит к ReferenceError.
console.log(b); // ReferenceError
let b = 10;

console.log(c); // ReferenceError
const c = 10;
  1. Перезапись и изменение значения
  • 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 — нельзя переназначить саму ссылку
  1. Специфика 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 ключевые виды областей видимости:

  1. Глобальная область видимости
  2. Функциональная область видимости
  3. Блочная область видимости
  4. (Дополнительно для полноты) Лексическая область и модули как надстройка над базовыми моделями

Важно понимать не просто названия, а как движок ищет переменные и как это связано с 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, момент доступности, область видимости) отличаются принципиально.

Основные различия.

  1. Синтаксис и место в коде

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;
};
  1. 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 — нельзя, функция "существует" только после выполнения строки присвоения.
  1. Область видимости и блочные декларации (современный JS)

Ранее function declaration трактовались только как глобальные/функциональные, но в современных реализациях и стандарте:

  • function declaration внутри блока {}:
    • имеет блочную область видимости (аналогично let/const, за исключением некоторых нюансов исторической совместимости в старых браузерах),
    • не должна использоваться как замена var, если важна предсказуемость.
if (true) {
function test() {
return 1;
}
}

console.log(typeof test); // в современных средах чаще "undefined"

Function expression:

  • Подчиняется правилам области видимости переменной, которой присвоена (let/const → блочная, var → функциональная/глобальная).
  1. Именованные function expression

Именованное функциональное выражение:

const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1); // используем внутреннее имя
};

Особенности:

  • Имя fact доступно только внутри тела функции.
  • Это удобно для рекурсии и лучшей читаемости.
  • Внешний код использует factorial.
  1. Практические выводы и рекомендации
  • Использовать 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:

  1. Function Declaration
  • Объявления функций вида:
    function foo() { ... }
    поднимаются целиком:
    • и имя,
    • и тело функции.
  • Поэтому такие функции можно вызывать до места их текстового объявления.

Пример:

greet(); // Работает

function greet() {
console.log("Hello");
}
  1. 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;
  1. Практические выводы:
  • Возможность вызывать функцию до её определения в коде корректна только для 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):

  1. Лексическое (фиксированное) 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.
  1. this в обычных функциях (function declaration/expression)
  • Обычная функция создаёт собственное this, которое определяется способом вызова:
    • как метод объекта: obj.m()this === obj;
    • как конструктор: new F()this — новый экземпляр;
    • простой вызов: f() → в strict this === undefined, без strict — глобальный объект;
    • через call/apply/bindthis задаётся явно.

Пример:

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 }
  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"
  1. Где стрелочные функции полезны с точки зрения 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, что и нужно.

  1. Дополнительные отличия, связанные с контекстом
  • Стрелочные функции:
    • нельзя использовать как конструкторы: new ArrowFn() выбросит ошибку;
    • не имеют собственного arguments (используют arguments внешней функции, если он есть);
    • хорошо отражают идею "функции как выражения без собственного контекста".

Это логически дополняет поведение с this: стрелочная функция — это "тонкий" колбэк, не меняющий контекст, а обычная функция — полноценный объект с собственным вызовным контекстом.

Итого по сути вопроса:

  • Обычная функция: this динамический, зависит от способа вызова, может переназначаться call/apply/bind.
  • Стрелочная функция: this лексический, один раз захватывается из внешнего контекста при объявлении и не меняется, что делает их идеальными для вложенных колбэков и плохим выбором для методов, зависящих от this объекта.

Вопрос 13. Своими словами опиши, что такое замыкание.

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

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

Правильный ответ:
Замыкание — это свойство функции в JavaScript (и не только), при котором она "запоминает" лексическое окружение (переменные, объявленные снаружи неё), существовавшее в момент её создания, и может обращаться к этим переменным даже после того, как внешняя функция завершила выполнение.

Ключевые моменты:

  1. Лексическое окружение
  • JS использует лексическую (статическую) область видимости:
    • То, какие переменные доступны, определяется местом объявления функции в коде, а не местом её вызова.
  • Когда функция создаётся, вместе с ней фиксируется ссылка на окружение, в котором она была объявлена.
  • Это окружение включает:
    • параметры внешней функции,
    • локальные переменные,
    • другие замкнутые значения.
  1. Что делает замыкание
  • Позволяет функции:
    • работать с "приватным состоянием",
    • инкапсулировать данные,
    • создавать фабрики функций,
    • реализовывать кеширование, мемоизацию, счётчики, настройки и т.д.
  • Важный момент: пока есть хотя бы одна внутренняя функция, которая ссылается на внешние переменные, эти переменные не очищаются сборщиком мусора.
  1. Базовый пример
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.
  1. Замыкания и стрелочные функции

Стрелочные функции работают с замыканиями так же, как обычные (лексическое окружение + this):

function makePrefixer(prefix) {
return (value) => `${prefix}_${value}`;
}

const withUser = makePrefixer('user');
console.log(withUser(1)); // user_1
console.log(withUser(2)); // user_2
  1. Распространённые применения
  • Инкапсуляция состояния без классов:
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); // быстро, из кеша
  1. Типичная ошибка понимания
  • Замыкание — это не только "когда функция возвращает функцию".
  • Оно возникает всегда, когда внутренняя функция использует внешние переменные:
    • возвращаемая,
    • переданная как колбэк,
    • сохранённая в структуру данных.
  • Важно помнить и про утечки памяти:
    • ненужные долгоживущие ссылки через замыкания могут удерживать большие объекты.

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

Вопрос 14. Что такое прототип в JavaScript и как работает наследование через него?

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

Ответ собеседника: правильный. Описывает прототип как объект, от которого другие объекты наследуют свойства через скрытое свойство __proto__, и упоминает, что вершина цепочки — null.

Правильный ответ:
В JavaScript прототип — это механизм, через который объекты наследуют свойства и методы от других объектов. Ядро модели: каждое объектоподобное значение (за редкими исключениями) связано с другим объектом — прототипом, и доступ к свойствам осуществляется через поиск по цепочке прототипов (prototype chain).

Ключевые элементы:

  1. Прототип и скрытая ссылка
  • У каждого объекта есть внутреннее [[Prototype]]-свойство — ссылка на другой объект или null.
  • В большинстве сред оно доступно через:
    • устаревшее, но наглядное obj.__proto__,
    • современные методы: Object.getPrototypeOf(obj) и Object.setPrototypeOf(obj).
  • Прототип — это не "класс", а обычный объект, из которого другие объекты могут "делиться" функциональностью.

Когда мы обращаемся к свойству:

obj.prop

движок делает:

  • Если prop есть у самого obj (own property) → вернуть его.
  • Иначе смотрит в прототипе: Object.getPrototypeOf(obj).
  • Если там нет → идёт дальше вверх по цепочке.
  • Цепочка продолжается до тех пор, пока не достигнет null.
  • Если свойство не найдено по всей цепочке → undefined.
  1. Цепочка прототипов (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
  1. Прототипы функций-конструкторов

Функции в 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, без копирования на каждый объект.
  1. Наследование через прототипы

Можно строить цепочки:

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

Цепочка:

  • rexDog.prototypeAnimal.prototypeObject.prototypenull.

Механика:

  • Если у rex нет свойства:
    • проверяется Dog.prototype,
    • если там нет — Animal.prototype,
    • далее Object.prototype,
    • иначе — undefined.
  1. Связь с 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).
  1. Практические моменты и важные детали
  • Добавление методов в прототип:
    • эффективно и экономно по памяти,
    • изменения в 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 есть несколько базовых нативных методов для поиска элементов. Их важно знать системно: по типу селектора, по области поиска и по формату возвращаемого результата.

Основные методы поиска:

  1. Универсальные селекторы (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');
  1. По id
  • document.getElementById(id)
    • Возвращает один элемент по его уникальному id.
    • Быстрый и точечный.
    • Если не найден — null.
    • Работает только у document, не у произвольных элементов.
const el = document.getElementById('app');
  1. По имени тега
  • document.getElementsByTagName(tagName)
  • element.getElementsByTagName(tagName)
    • Возвращает "живую" коллекцию (HTMLCollection) элементов с указанным тегом.
    • "Живая" означает: при изменениях DOM коллекция автоматически обновляется.
    • Примеры:
      • "div", "span", "input", "a" и т.д.
const divs = document.getElementsByTagName('div');
  1. По имени класса
  • document.getElementsByClassName(className)
  • element.getElementsByClassName(className)
    • Возвращает "живую" HTMLCollection всех элементов с указанным классом.
    • Можно передать несколько классов через строку: "btn primary" — элементы, содержащие оба класса.
const items = document.getElementsByClassName('list-item');
  1. По имени (атрибут name)
  • document.getElementsByName(name)
    • Возвращает NodeList (в старых описаниях — похожую коллекцию) элементов с атрибутом name.
    • Используется часто для форм, радио-кнопок, инпутов.
const inputs = document.getElementsByName('email');
  1. Вспомогательные/смежные методы навигации (не поиск по селектору, но важны для работы с 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 три состояния:

  1. pending
  2. fulfilled
  3. rejected

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

Подробно:

  1. pending (ожидание)
  • Начальное состояние только что созданного Promise.
  • Асинхронная операция ещё не завершена.
  • Нет финального значения или причины ошибки.
  • Из этого состояния Promise может перейти только один раз:
    • в fulfilled,
    • или в rejected.
  • Пока Promise в pending:
    • обработчики .then(onFulfilled, onRejected) и .catch(onRejected) просто регистрируются и будут вызваны позже.

Пример:

const p = new Promise((resolve, reject) => {
// пока ничего не вызываем — p в состоянии pending
});
  1. 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 → успешно завершён с конкретным значением.
  1. 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 вызываются асинхронно (микротаски), после текущего стека вызовов.

Эта трёхсостоящая модель делает поведение асинхронного кода предсказуемым и позволяет надёжно выстраивать цепочки .then(), async/await и обработку ошибок.

Вопрос 17. Своими словами опиши, что такое event loop в JavaScript и как он обрабатывает задачи.

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

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

Правильный ответ:
Event loop — это механизм, который координирует выполнение кода, обработку асинхронных операций и взаимодействие с окружением (браузер или Node.js) в однопоточном JavaScript. Он объясняет, почему при одной "нитке" исполнения мы можем обрабатывать множество асинхронных операций без блокировки интерфейса.

Ключевые идеи:

  1. Однопоточность JS-кода
  • JavaScript-движок (например, V8) выполняет код в одном основном потоке.
  • В один момент времени выполняется только одна инструкция JS.
  • Асинхронность достигается не за счёт нескольких потоков JS, а за счёт:
    • API окружения (браузерные Web API, Node.js API),
    • очередей задач,
    • и работы event loop, который решает, какую задачу выполнить следующей.
  1. Стек вызовов (call stack)
  • Все синхронные функции выполняются в рамках стека вызовов.
  • Пока стек не пуст — новые задачи из очередей не берутся.
  • Любой "тяжёлый" синхронный код блокирует:
    • обновление UI,
    • обработку событий,
    • выполнение колбэков промисов и таймеров.
  1. Асинхронные операции и окружение Асинхронные вызовы (таймеры, HTTP-запросы, обработчики событий, промисы) работают так:
  • JS-код регистрирует операцию и колбэк:
    • setTimeout, addEventListener, fetch, Promise.then и т.д.
  • Реальная работа (ожидание таймера, сети, IO) происходит во внешнем окружении, а не в JS-стеке.
  • Когда операция завершается, окружение ставит "задачу" в соответствующую очередь.
  • Event loop берёт задачи из очередей и превращает их в вызовы JS-функций, когда стек свободен.
  1. Очереди задач: макротаски и микротаски

Важно различать два основных типа задач:

  • Макротаски (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 (упрощённая):

  1. Взять одну макротаску из очереди:
    • выполнить её полностью (синхронный JS-код).
  2. После завершения:
    • выполнить все микротаски из очереди microtasks до конца (пока очередь не опустеет).
  3. Обновить рендер (в браузере).
  4. Перейти к следующей макротаске и повторить.

Пример для иллюстрации:

console.log('A');

setTimeout(() => {
console.log('timeout');
}, 0);

Promise.resolve()
.then(() => {
console.log('promise');
});

console.log('B');

Порядок вывода:

  • Сначала синхронно:
    • A
    • B
  • Затем:
    • микротаски (обработчики промисов): promise
  • Потом:
    • макротаски (таймеры): timeout

Итого в консоли:

A
B
promise
timeout

Почему:

  • setTimeout(..., 0) ставит макротаску.
  • Promise.resolve().then(...) ставит микротаску.
  • После окончания основного скрипта:
    • event loop сначала выполняет микротаски (promise),
    • затем берёт следующую макротаску (timeout).
  1. Практические выводы
  • Асинхронный JS предсказуем, если понимать:
    • синхронный код — сразу в стеке;
    • промисы/await — микротаски (высокий приоритет);
    • таймеры/события — макротаски (ниже по приоритету);
  • Долгие синхронные операции:
    • блокируют event loop;
    • задерживают обработку кликов, анимаций, промисов, сетевых ответов.
  • Для тяжёлых операций:
    • выносить работу в Web Workers, отдельные процессы/сервисы или бэкенд.

Кратко: Event loop — это "диспетчер", который:

  • выполняет синхронный код;
  • после освобождения стека обрабатывает микротаски;
  • затем — макротаски;
  • и так по кругу, обеспечивая асинхронное, но упорядоченное выполнение операций в однопоточном окружении.

Вопрос 18. Какие задачи относятся к макротаскам в контексте event loop?

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

Ответ собеседника: правильный. После подсказки называет таймеры и обработчики событий как примеры макротасок.

Правильный ответ:
Макротаски (tasks, macrotasks) — это "крупные" единицы работы, которые event loop последовательно берет из очереди задач. Каждая макротаска выполняется целиком, после чего движок перед переходом к следующей макротаске обрабатывает все накопленные микротаски.

К типичным макротаскам относятся:

  • Выполнение основного скрипта:

    • начальная загрузка и исполнение JS-файла (<script>),
    • каждая отдельная единица исполнения (например, скрипты по defer, async и т.д.).
  • Таймеры:

    • 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 функциональными компонентами и хуками) жизненный цикл удобнее всего понимать не как набор разрозненных методов, а как последовательность фаз:

  1. Монтирование (mount)
  2. Обновление (update)
  3. Размонтирование (unmount)

Классические методы (componentDidMount, componentDidUpdate, componentWillUnmount) сегодня в основном переосмыслены через useEffect и другие хуки.

Основные этапы и практическое применение.

  1. Монтирование (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).
  1. Обновление (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 вызывается:
    • после рендера,
    • и повторяется при изменении значений из массива зависимостей.
  • Ошибка: оставлять массив зависимостей пустым, когда эффект реально зависит от пропсов или состояния → баги и рассинхронизация.
  1. Размонтирование (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 таймер продолжил бы работать и пытаться обновлять состояние размонтированного компонента — классический анти-паттерн.
  1. Жизненный цикл и практика проектирования

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

  • Разделяйте:
    • render (чистая функция от props/state),
    • side effects (внутри useEffect/useLayoutEffect).
  • Используйте зависимости в useEffect честно:
    • всё, что используется внутри эффекта из внешней области, должно быть в массиве зависимостей,
    • иначе — неконсистентное поведение.
  • Для тяжёлых вычислений:
    • useMemo для мемоизации результатов,
    • useCallback для стабилизации колбэков,
    • но только при реальной пользе (не "по дефолту").
  • Для подписок:
    • один эффект = одна ответственность:
      • один — подписка на resize,
      • другой — на WebSocket,
      • это упрощает cleanup и отладку.
  • В сложных случаях:
    • собственные хуки (custom hooks) для инкапсуляции повторяющихся жизненных циклов — работы с API, WebSocket, формами и т.д.
  1. Мост к классовому API (для полноты)

Если сравнивать с классами (важно для чтения легаси-кода):

  • Монтирование:
    • constructor
    • componentDidMount
  • Обновление:
    • componentDidUpdate
    • shouldComponentUpdate
  • Размонтирование:
    • componentWillUnmount

В функциональных компонентах всё это выражается комбинацией:

  • useState / useReducer (управление состоянием),
  • useEffect / useLayoutEffect (эффекты + cleanup),
  • memo/useMemo/useCallback (оптимизации).

Итого: Жизненный цикл компонента в современном React — это управление:

  • тем, что рендерится (чистая функция),
  • когда и как выполняются побочные эффекты,
  • как корректно очищать ресурсы. useEffect с правильно подобранными зависимостями и cleanup — ключевой инструмент для точного контроля над монтированием, обновлением и размонтированием.

Вопрос 22. В каких случаях React-компонент повторно рендерится?

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

Ответ собеседника: правильный. Говорит, что повторный рендер происходит при изменении собственного состояния, обновлении пропсов из родителя и при рендере родителя; дополнительно упоминает влияние React.memo.

Правильный ответ:
Повторный рендер React-компонента происходит, когда React считает, что результат функции/метода рендера потенциально мог измениться. Для функциональных и классовых компонентов базовые триггеры одинаковы.

Ключевые случаи:

  1. Изменение собственного состояния компонента
  • Для функциональных компонентов:
    • вызов 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>;
}
  1. Изменение пропсов от родительского компонента
  • Если родительский компонент заново отрендерился и передал в дочерний компонента новые пропсы (с точки зрения сравнения), дочерний:
    • по умолчанию тоже будет рендериться.
  • Даже если значения "логически те же", но ссылки новые (например, новые объекты/функции при каждом рендере) — это воспринимается как изменение пропсов.

Пример:

function Parent({ value }) {
const data = { value }; // новый объект при каждом рендере

return <Child data={data} />;
}

Без оптимизаций Child будет рендериться при каждом рендере Parent, даже если value не менялся, потому что data — новый объект.

  1. Ререндер родителя (даже при тех же пропсах), если нет оптимизации
  • По умолчанию:
    • если родитель рендерится, его дети тоже вызываются (их функция/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>;
});
  1. Контекст (useContext / Context API)
  • Компонент, использующий useContext(SomeContext) или Context.Consumer, повторно рендерится при изменении значения контекста.
  • Все потребители контекста, подписанные на этот контекст, рендерятся заново, независимо от React.memo, если только memo не оборачивает и не используется грамотная структура контекстов.
  1. Изменение ключей (key) у элементов/компонентов
  • Если меняется key компонента в списке:
    • React воспринимает это как "старый компонент размонтирован, новый смонтирован".
    • Это не просто ререндер, а уничтожение одного экземпляра и создание нового (со сбросом состояния).
  • Неправильный/нестабильный key (например, индекс массива при изменяемых списках) приводит к лишним рендерам и багам.
  1. Внешние факторы и принудительные обновления
  • Для классовых компонентов:
    • this.forceUpdate() → форсирует ререндер независимо от сравнения пропсов/состояния.
  • Для интеграций с внешними сторами (Redux/Zustand/MobX и др.):
    • подписка на изменения состояния стора,
    • если селектор вернул новое значение → компонент рендерится.
  1. Что НЕ вызывает ререндер напрямую:
  • Мутация объекта/массива в пропсах или состоянии без создания нового значения:
    • React смотрит на ссылку, а не "глубокое" содержимое.
    • Но если вы потом вызываете setState с тем же объектом по ссылке — рендера может не быть.
  • Изменение DOM напрямую через document.querySelector и т.п.:
    • React об этом не знает, ререндер по этому поводу не произойдёт.
  • Promise/таймер/событие сам по себе:
    • только когда внутри колбэка вызывается setState или обновляется хранилище.

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

  • Явно понимать триггеры:
    • изменение state,
    • изменение props,
    • изменение context.
  • Использовать:
    • иммутабельные обновления (новые объекты/массивы),
    • React.memo для "чистых" компонент, чувствительных к пропсам,
    • useCallback / useMemo для стабилизации пропсов-функций и вычисляемых значений.
  • Не бояться рендеров:
    • основной фокус — на корректности и читаемости,
    • оптимизации включать точечно, когда замерили реальную проблему.

Вопрос 23. Какими способами можно избежать выполнения тяжёлых вычислений при каждом рендере компонента в React?

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

Ответ собеседника: правильный. Предлагает использовать useMemo с функцией вычисления и массивом зависимостей для пересчёта результата только при изменении зависимостей.

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

Основные подходы:

  1. Мемоизация вычислений через 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 должен быть стабилизирован (см. ниже),
    • либо принимаем перерасчёт при каждой смене ссылки.
  1. Стабилизация пропсов и колбэков: useCallback, useMemo

Тяжёлые вычисления часто спрятаны в дочерних компонентах. Даже с React.memo дочерний компонент будет рендериться, если пропсы каждый раз новые (например, новые функции или объекты).

Способы оптимизации:

  • useCallback для функций:
const handleClick = useCallback(() => {
// логика
}, [/* зависимости */]);
  • useMemo для составных пропсов (объекты, массивы):
const config = useMemo(() => ({
pageSize,
filters
}), [pageSize, filters]);

Это:

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

Если результат:

  • не зависит от 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>;
}

Так тяжёлая логика уходит из синхронного рендера.

  1. Деление вычислений и отложенное выполнение

Для реально тяжёлых задач:

  • разбивать вычисления на чанки и выполнять поэтапно через:
    • 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;
}
  1. Web Workers для CPU-bound задач

Если вычисление:

  • действительно тяжёлое (анализ больших структур, криптография, трансформации),
  • и блокирует event loop,

правильный путь:

  • вынести в Web Worker:
    • основной поток остаётся для React и UI,
    • worker считает и отправляет результат обратно.

Общий паттерн:

  • React-компонент:
    • отправляет данные в worker,
    • слушает сообщения,
    • пишет результат в state.
  • Это полностью предотвращает блокировку рендера тяжёлыми вычислениями.
  1. Разумный баланс

Важно:

  • Не злоупотреблять 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);
  • состояние только для чтения;
  • изменения описываются чистыми функциями.

Базовые сущности и идеи:

  1. Store (хранилище)
  • Единый объект, в котором хранится всё состояние приложения (или логически крупного домена).
  • В классическом Redux — один store на всё приложение.
  • Отвечает за:
    • текущее состояние;
    • подписку на изменения;
    • диспетчеризацию (dispatch) экшенов.

Интерфейс store:

  • getState() — получить состояние;
  • dispatch(action) — отправить экшен;
  • subscribe(listener) — подписаться на изменения.

Пример:

import { createStore } from 'redux';

const store = createStore(rootReducer);

console.log(store.getState());
  1. State (состояние)
  • Иммутабельная структура данных, описывающая текущее состояние приложения:
    • UI-состояние,
    • данные из API,
    • флаги загрузки/ошибок и т.д.
  • Менять состояние напрямую нельзя:
    • только через dispatch(action) → reducer → новый state.

Пример структуры:

const initialState = {
user: { id: null, name: null },
todos: [],
loading: false,
error: null,
};
  1. 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.
  1. 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,
// другие редьюсеры
});
  1. Dispatch (отправка экшена)
  • store.dispatch(action) — единственный "официальный" способ инициировать изменение состояния.
  • Запускает цепочку:
    • dispatch(action) → редьюсеры → новый state → уведомление подписчиков (UI).

Пример:

store.dispatch({ type: 'counter/increment' });
console.log(store.getState().counter.value);
  1. Однонаправленный поток данных (uni-directional data flow)

Ключевой паттерн, общий для Flux и Redux:

  • UI или внешнее событие инициирует действие → dispatch(action).
  • action попадает в store → редьюсер(ы) вычисляют новый state.
  • Обновлённый state передаётся в UI:
    • в React: через useSelector, connect и т.п.
  • UI перерисовывается на основе нового состояния.

Схема:

UI → (dispatch action) → Store/Reducers → New State → UI

Нет "обратных" скрытых путей изменения state:

  • никакого прямого "set" состояния из компонентов мимо store;
  • это делает поток данных предсказуемым и хорошо трассируемым.
  1. 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 });
}
};
  1. Отличия 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-формулировка):

  1. Пользователь или внешнее событие инициирует действие
  2. Действие (action) отправляется (dispatch) в store
  3. Редьюсеры (reducers) вычисляют новый state на основе action
  4. Store сохраняет новый state
  5. Подписчики (UI-компоненты и др.) уведомляются и перечитывают состояние

Разберём по шагам.

  1. Инициирование действия (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>;
}
  1. Диспетчеризация (dispatch)

dispatch(action) — единая точка входа для изменения состояния.

  • Компонент, thunk, saga или другая часть системы вызывает:
    • store.dispatch(action).
  • В чистом Redux:
    • dispatch синхронно передаёт action в цепочку middleware (если есть),
    • затем доходит до редьюсеров.

Пример:

store.dispatch({ type: 'todos/add', payload: { id: '1', title: 'Task' } });
  1. Обработка в редьюсерах (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, логирование).
  1. Обновление store

После выполнения редьюсеров:

  • Store сохраняет новый state.
  • В Redux:
    • если редьюсер вернул тот же объект (по ссылке) для части state — считается, что там изменений нет;
    • если вернулся новый объект — это сигнал, что соответствующая часть изменилась.

Можно увидеть:

store.subscribe(() => {
console.log('New state:', store.getState());
});

После dispatch:

  • все подписчики вызываются,
  • каждый сам решает, как реагировать.
  1. Уведомление 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-хранилища. Их задача — не пересчитывать тяжёлые производные данные при каждом рендере или каждом изменении стора, а повторно использовать ранее вычисленный результат, если входные данные не изменились.

Базовые идеи:

  1. Селектор: что это такое

Селектор — это функция, которая:

  • принимает состояние (state) (и опционально props),
  • возвращает "срез" или производное значение.

Простой селектор:

const selectTodos = (state) => state.todos;

Но в реальных приложениях часто нужны производные данные:

  • фильтрация,
  • агрегации,
  • группировки,
  • вычисления на основе нескольких частей стора.

Например:

const selectCompletedTodos = (state) =>
state.todos.filter(todo => todo.completed);

Если этот селектор вызывается часто (много компонентов, частые обновления), то фильтрация на каждом вызове может стать дорогой — особенно при больших массивах и сложной логике.

  1. Проблема без мемоизации

Без мемоизации:

  • при любом обновлении стора (даже не относящемся к todos) селектор:
    • будет вычисляться заново,
    • вернёт новый массив,
    • потенциально триггерит лишние рендеры компонентов, зависящих от этого селектора (шallow-compare видит новую ссылку).

Нам нужно:

  • "запомнить" результат,
  • пересчитывать только при реальном изменении входных данных.
  1. 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.
  1. В связке с 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,
  • компонент рендерился бы каждый раз.
  1. Типичные кейсы для мемоизированных селекторов

Использовать 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)
);

Здесь:

  • поддерживается мемоизация на последних входных аргументах (для более сложных кейсов можно использовать "многоключевую" или кастомную мемоизацию).
  1. Ограничения и best practices
  • createSelector (по умолчанию) кэширует результат только для последнего набора входных значений.
    • Для большинства UI-кейсов этого достаточно.
    • Для более сложного кейсинга (разные аргументы, множество orgId) можно:
      • использовать селекторы-фабрики (создавать селектор на компонент/контекст),
      • или расширенную мемоизацию.
  • Входные селекторы должны быть "чистыми":
    • не модифицировать state,
    • не иметь побочных эффектов.
  • Эффект настоящей оптимизации есть, когда:
    • реально есть тяжёлые вычисления или большая нагрузка,
    • а не просто "на всякий случай вокруг каждого селектора".

Краткий итог:

  • Мемоизированные селекторы (через reselect/createSelector):
    • принимают state (и аргументы),
    • вычисляют производное значение,
    • кэшируют результат на основе входных значений.
  • При неизменных входах:
    • не выполняют тяжёлые вычисления,
    • возвращают прежний объект/массив по той же ссылке.
  • В связке с useSelector/connect:
    • снижают количество перерасчётов и ререндеров,
    • делают работу с большими данными и сложными производными селекторами эффективной и предсказуемой.

Вопрос 27. Почему изменение значения счётчика через useRef не приводит к обновлению интерфейса и как корректно реализовать работу счётчика?

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

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

Правильный ответ:
useRef и useState решают принципиально разные задачи в React, и понимание этой разницы критично для корректной работы с UI.

  1. Почему 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 остаётся прежним, потому что компонент не был перерендерен.
  1. Как корректно реализовать счётчик: 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 синхронизирован с логикой.
  1. Когда уместно комбинировать 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.
  1. Краткий практический вывод
  • useRef:
    • использовать для:
      • DOM-рефов,
      • хранения мутабельных значений между рендерами,
      • оптимизаций и служебных данных,
    • не использовать как механизм управления отображаемым состоянием.
  • useState:
    • использовать для данных, которые должны быть отражены в UI:
      • счётчики,
      • флаги, инфо из API и т.п.

Правильная реализация счётчика — через состояние (useState или useReducer). useRef подходит только как дополнение, если нужно хранить вспомогательные значения без триггера рендера.

Вопрос 28. Что не так в реализации выбора цвета и как оптимизировать код, убрав лишнее состояние и useEffect?

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

Ответ собеседника: правильный. Замечает дублирование состояния выбранного цвета и соглашается, что достаточно одного состояния и обновления прямо в обработчике без useEffect.

Правильный ответ:
Типичная ошибка при реализации выбора цвета в React — избыточное состояние и лишняя связка через useEffect. Часто встречается такой паттерн:

  • есть проп selectedColor,
  • есть локальное состояние color,
  • есть useEffect, который синхронизирует одно с другим,
  • есть обработчик, который сначала меняет одно, потом побочно обновляет другое.

Это приводит к:

  • дублированию источников истины (две переменные описывают одно и то же),
  • риску рассинхронизации,
  • лишним рендерам,
  • ненужному усложнению кода.

Как правило, нужно:

  1. Определить один источник истины (single source of truth):

    • либо управляемый компонент (цвет хранится во внешнем состоянии, передаётся через пропсы и изменяется коллбэком),
    • либо локальное состояние внутри компонента (контролируем только там).
  2. Убрать 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), если интервал объявлен без учёта замыканий.

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

  1. Как работает 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;
  • возможны "залипшие" интервалы, которые продолжают работать.
  1. Правильное хранение 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, чтобы отразить, что интервала больше нет.
  • useEffect cleanup:
    • при размонтировании компонента:
      • очищает интервал,
      • предотвращает утечки памяти и попытки вызвать setState после unmount.
  1. Частая логическая ошибка: stale state внутри интервала

Даже при правильном clearInterval, часто делают так:

setInterval(() => {
setSeconds(seconds + 1); // захватывается старое значение seconds
}, 1000);

Это неправильно:

  • замыкание фиксирует значение seconds на момент создания интервала,
  • в дальнейшем прибавляется одно и то же базовое значение.

Правильный вариант — использовать функциональное обновление:

setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);

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

  1. Итог:

Чтобы таймер со стартом/стопом работал корректно:

  • Храните 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:

  1. Типичные источники утечек памяти
  • Неотписанные обработчики событий:
    • window, document, DOM-элементы, WebSocket, EventEmitter.
    • В React: отсутствие cleanup в useEffect.
  • Таймеры и интервалы:
    • setInterval, setTimeout, requestAnimationFrame, requestIdleCallback, не очищенные при размонтировании.
  • Долгоживущие замыкания:
    • ссылки на крупные объекты, хранящиеся в замыканиях, которые продолжают жить из-за глобальных ссылок или подписок.
  • Кэши и глобальные структуры:
    • Map, Set, объекты-кэши, куда добавляют, но откуда не удаляют.
  • Некорректная работа с порталами и сторонними библиотеками:
    • когда библиотека создаёт DOM/слушатели вне React-дерева и не чистит их.
  1. Подход к поиску утечек
  • Воспроизводимость:
    • найти сценарий, при котором при навигации/открытии/закрытии страницы/модалки использование памяти стабильно растёт и не возвращается.
  • Инструменты браузера (Chrome DevTools / аналогичные):
    • вкладка Performance:
      • записать профили при повторяющихся действиях;
      • проверить рост количества нод/слушателей/объектов.
    • вкладка Memory:
      • heap snapshots до и после повторения сценария (например, 10 раз открыть/закрыть модалку);
      • анализ "Detached DOM trees";
      • поиск объектов, которые не должны жить, но удерживаются ссылками.
  • Анализ зависимостей:
    • провериться, что все эффекты useEffect/useLayoutEffect имеют корректный cleanup:
      • removeEventListener,
      • clearInterval/clearTimeout,
      • завершение/отписка от WebSocket/stream/observable.

Пример корректного cleanup в React:

useEffect(() => {
const handler = () => { /* ... */ };
window.addEventListener('resize', handler);

return () => {
window.removeEventListener('resize', handler);
};
}, []);
  1. Практические приёмы и паттерны предотвращения
  • Всегда:
    • использовать cleanup-функции в эффектах;
    • не держать в замыканиях больше, чем необходимо;
    • аккуратно работать с глобальными синглтонами и кэшами;
    • следить за тем, что компоненты, которые должны "умирать", реально размонтируются, а не остаются скрытыми только визуально.
  • Для асинхронных операций:
    • отменять запросы или помечать результаты как неактуальные:
useEffect(() => {
let cancelled = false;

(async () => {
const res = await fetch('/api/data');
if (!cancelled) {
// безопасно обновляем состояние
}
})();

return () => {
cancelled = true;
};
}, []);
  • В сложных случаях:
    • использовать профилировщик React DevTools для поиска "висящих" компонент, которые не размонтируются;
    • проверять, нет ли постоянных перерендеров, держащих ссылки на большие структуры.
  1. Краткая форма хорошего ответа на интервью

Если отвечать компактно:

  • Да, был опыт поиска и исправления утечек памяти в JS/React.
  • Типовые кейсы:
    • неотписанные события, таймеры, WebSocket'ы;
    • утечки через замыкания и кэши.
  • Инструменты:
    • Chrome DevTools (Memory/Performance, heap snapshots),
    • React DevTools для анализа дерева компонент.
  • Практика:
    • всегда делаю cleanup в useEffect,
    • слежу за тем, чтобы долгоживущие структуры не удерживали лишние ссылки,
    • проверяю жизненный цикл компонент при навигации и повторяющихся сценариях.

Такой ответ демонстрирует не только факт опыта, но и понимание методологии и инструментов.

Вопрос 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);

Давайте по шагам.

  1. Синхронный код, один 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.

Итог синхронной фазы (порядок вывода):

  • 5
  • 25
  • end 25
  1. Переход к макротаскам (таймеры)

После завершения всего синхронного кода:

  • 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 и будет выполнен после окончания текущей макротаски, но до следующей.
  1. Обработка микрозадач

После завершения текущей макротаски (второго setTimeout):

  • Event loop:
    • выполняет все микрозадачи из очереди microtasks.

Микрозадача — p.then(...):

p.then(() => {
x = 30;
console.log('then', x);
});

Выполняем:

  • x = 30;
  • console.log('then', x);
    • выводит: then 30.
  1. Итоговый порядок вывода

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

  • 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, а не раньше.