Собеседование
React Frontend
- Дата
- 6 октября 2026 г. в 09:36
- Уровень
- Middle
- Язык
- Русский
- Балл
- 6
Interview Debrief
Отчёт по интервью
Русский · по одному вопросу на каждую тему
Общая оценка
6/10
Целевой уровень Middle
Продемонстрированный уровень Senior
Выше целевого (Middle → Senior)
Итог
Кандидат продемонстрировал уверенные теоретические знания по JavaScript, TypeScript, React и веб-технологиям на уровне выше среднего Middle: ответы полные, структурированные, с корректными деталями (TDZ, discriminated unions, reconciliation, Promise.allSettled/any, React Compiler). Однако практическое задание «Anagram» сдано не было — ни публичные, ни скрытые тесты не пройдены, решение не было даже запущено/сдано. Разрыв между сильной теорией и провалом практической части — ключевой риск: возможно, кандидат готовился по теории, но не проверил навык решения задач под давлением.
Практическое задание
Оценка 2/10. Публичные тесты: fail. Скрытые тесты: fail.
Алгоритмическая идея верная, но нужно доводить решение до запуска и сдачи: прогнать открытые тесты, проверить формат вывода, при затруднении — запросить подсказку. Практическая часть не подтвердила уровень, показанный в теории.
Решение «Anagram» выглядело логически корректным (сортировка символов, сравнение, проверка длины), но ни публичные, ни скрытые тесты не были пройдены: кандидат не запустил код и не сдал решение. Задание фактически не доведено до конца.
Сильные стороны
- Глубокое понимание JavaScript: hoisting, TDZ, правила приведения типов, ToPrimitive
- Уверенный TypeScript: narrowing, discriminated unions, any/unknown/never, generics с практическими примерами
- Хорошее знание React: жизненный цикл useEffect/useLayoutEffect, reconciliation, useMemo/useCallback/React.memo с подводными камнями
- Знание сетевой части: HTTPS/TLS, полный путь URL → отрисовка, HSTS, defer/async
- Осведомлённость об экосистеме (React Compiler, structuredClone, allSettled/any)
Слабые места
- Практическое задание не сдано: публичные и скрытые тесты не пройдены, попытка не была доведена до запуска/сдачи
- Не использованы возможности обратной связи (подсказки в чате, запуск кода на открытых тестах)
- Возможна подготовка «по билетам»: теория заметно сильнее практики
Что подтянуть
- Провести дополнительное практическое интервью с live-coding для подтверждения навыка решения алгоритмических задач
- Кандидату: отрабатывать задачи на алгоритмы и работу с вводом/выводом, доводить решения до сдачи даже при неуверенности
- На следующем этапе проверить практические React-навыки (рефакторинг компонента, работа с состоянием)
Теория
-
Здравствуйте, рад вас видеть! Меня зовут Алексей, я проведу у вас собеседование. Формат такой: 12 теоретических вопросов по сценарию «React Frontend», затем будет практическое задание. Начнём с первого вопроса: объясните, пожалуйста, разницу между HTTP и HTTPS и как работает TLS?
-
HTTP передаёт запросы и ответы по сети в открытом виде: любой на пути (Wi‑Fi, провайдер) может прочитать или подменить данные. HTTPS — тот же HTTP, но поверх TLS: трафик шифруется, а сервер обычно подтверждает личность сертификатом от доверенного центра. TLS начинается с рукопожатия: клиент и сервер договариваются о версии протокола и алгоритмах, сервер отдаёт сертификат, клиент проверяет его и они совместно получают общий секретный ключ. Дальше этим ключом шифруется весь обмен (страницы, формы, куки). В браузере HTTPS виден по https:// и замку; без TLS пароли и сессии уязвимы к перехвату и подмене сайта.
-
Хороший, структурированный ответ, особенно про рукопожатие и проверку сертификата. Переходим к React: опишите жизненный цикл компонента и как useEffect соотносится с mount, update и unmount?
-
В функциональных компонентах нет классических методов вроде componentDidMount, но та же логика есть: первый рендер — «рождение», повторные — «обновления», снятие с DOM — «размонтирование». useEffect выполняется после того, как React отрисовал изменения на экране, поэтому это не момент «до paint», а фаза после коммита в DOM. При mount эффект с [] в зависимостях срабатывает один раз после первого рендера. При update эффект с [a, b] перезапускается, когда меняются зависимости: сначала вызывается cleanup от прошлого запуска, затем новый эффект. При unmount React вызывает cleanup (функцию, которую вернул эффект) — там отписываются от событий, отменяют таймеры и прерывают запросы. Не путайте с useLayoutEffect: он синхронный до отрисовки браузером и нужен реже (измерения DOM, синхронная правка UI). Для подписок, fetch и аналитики обычно достаточно useEffect.
-
Отличный ответ — особенно ценно, что упомянули порядок cleanup перед следующим эффектом и разницу с useLayoutEffect. Следующий вопрос: что происходит, когда вы вводите URL в браузере и нажимаете Enter? Расскажите по шагам, от DNS до отрисовки.
-
1. Парсинг URL — браузер определяет схему (https), хост, порт, путь и параметры; при необходимости дополняет https:// или отправляет в поиск. 2. DNS — по имени хоста запрашивается IP: кэш ОС/браузера → резолвер провайдера → корневые/TLD/авторitative-серверы; ответ кэшируется по TTL. 3. TCP и TLS — к IP:443 устанавливается TCP (SYN/ACK), затем TLS-handshake: сертификат, проверка, общий ключ; дальше HTTP идёт внутри зашифрованного канала.
-
Хорошо, но вы остановились на TLS-хендшейке. Уточните: что происходит дальше — какие запросы браузер отправляет серверу и как страница попадает на экран (парсинг HTML, загрузка ресурсов, построение DOM/CSSOM)?
-
4. HTTP — отправляется запрос (метод, заголовки, cookies); сервер отвечает статусом и телом; для HTML часто идут редиректы (301/302), HSTS может сразу принудить HTTPS. 5. Разбор и загрузка ресурсов — HTML парсится потоково; находятся CSS, JS, шрифты, картинки; параллельно новые запросы (HTTP/2 мультиплексирование); JS может блокировать или отложить парсинг (defer/async). 6. Отрисовка — из DOM и CSS строится render tree, layout (геометрия), paint (пиксели), compositing (слои/GPU); интерактивность наступает после выполнения скриптов и события load/DOMContentLoaded.
-
Отлично, теперь картина полная — от DNS до композитинга, и хорошо, что вспомнили HSTS и defer/async. Идём к JavaScript: чем отличаются var, let и const? И что такое hoisting?
-
var — функциональная область видимости: одна переменная на всю функцию, повторное объявление разрешено. let и const — блочная ({}): внутри if, цикла, блока они не «просачиваются» наружу. const нельзя переназначить (=), но если значение — объект или массив, его поля и элементы менять можно. let можно переназначать; для счётчиков и переприсваиваемых ссылок это нормальный выбор. Hoisting — на этапе компиляции объявления поднимаются в начало своей области: для var до присваивания значение undefined, для let/const — temporal dead zone до строки объявления, обращение раньше даёт ReferenceError. В современном JS обычно берут const по умолчанию, let когда нужно переприсвоение, var в новом коде почти не используют из‑за области видимости и hoisting.
-
Полный и уверенный ответ — TDZ и мутабельность полей const упомянуты, хорошо. Следующий вопрос по TypeScript: что такое type narrowing и как работает discriminated union?
-
Type narrowing — когда TypeScript после проверки в коде сужает тип переменной: из широкого union или unknown остаётся конкретный вариант. Это делают typeof, instanceof, проверки на null/undefined, пользовательские type guards (x is Foo) и иногда assertion functions. Discriminated union — union, у которого у каждого варианта есть общее поле‑метка с литеральным типом (discriminant), например kind: 'success' | 'error'. В switch (result.kind) или в if (result.kind === 'error') TypeScript автоматически сужает тип: в ветке 'error' доступны только поля ошибки, в 'success' — только поля успеха. Так исчезают лишние optional‑поля и «угадывание» структуры: компилятор сам знает, какой shape у текущей ветки. На собеседовании часто просят показать обработку { kind: 'ok', data } | { kind: 'fail', message } без as и с исчерпывающим switch (иногда с never для забытых кейсов).
-
Уверенный ответ — narrowing, discriminant, исчерпывающий switch с never-проверкой, всё на месте. Переходим дальше: чем отличаются any, unknown и never? И когда какой тип уместно использовать?
-
any отключает проверку типов: с ним можно почти что угодно, но теряются автодополнение и защита от ошибок. Его оставляют для миграции с JS, редких legacy‑участков или когда тип действительно неизвестен и вы сознательно не проверяете — в новом коде лучше избегать. unknown — «значение есть, но тип неизвестен»: присвоить можно откуда угодно, а вот использовать (поля, вызов, арифметика) — только после сужения (typeof, guard, парсинг схемой). Это нормальный выбор для JSON из API, catch (e: unknown) и обёрток над внешними данными. never — тип, которому не соответствует ни одно значение: функция, которая всегда бросает или зацикливается, или ветка, которую TypeScript считает недостижимой. В switch по discriminated union параметр x: never в default ловит забытые кейсы; в generics never часто означает «этот вариант комбинации невозможен».
-
Отличный ответ — граница между any и unknown, сужение перед использованием и never в exhaustive switch описаны верно. Идём дальше: объясните разницу между shallow copy и deep copy объекта. Когда каждого из них достаточно и как их получить?
-
Shallow copy копирует только «верхний» слой: новый объект/массив, но вложенные объекты остаются теми же ссылками — изменение copy.nested.x затронет и оригинал. Его достаточно, когда структура плоская или вложенность намеренно общая (immutable‑паттерн: меняете только корень через spread). Получить shallow: { ...obj }, Object.assign({}, obj), [...arr], arr.slice(), structuredClone не нужен для этого уровня. Deep copy рекурсивно дублирует всё дерево — независимые вложенные объекты. Нужен, когда клонируете конфиг/состояние с глубокой вложенностью и потом мутируете копию, не трогая источник. Способы: structuredClone(obj) (современный стандарт, без функций, DOM, символов с нюансами), JSON.parse(JSON.stringify(obj)) только для JSON‑данных (теряются Date, undefined, функции), lodash cloneDeep для сложных legacy‑случаев. В React чаще избегают deep copy через иммутабельные обновления (копируете только изменённую ветку), а не клонируете весь граф целиком.
-
Полный ответ, плюс приятно, что связали с иммутабельными обновлениями в React. Следующий вопрос: что такое reconciliation и virtual DOM? Как React решает, что именно перерисовать?
-
Virtual DOM — лёгкое дерево описания UI в памяти (элементы, props, дети). После изменения state React строит новое дерево и сравнивает его со снимком прошлого рендера — это reconciliation (diff). Сравнение идёт сверху вниз с эвристиками: элементы с разным type (например div → span) — старый поддерево снимается, новое монтируется заново. При том же type React обновляет props и рекурсивно обходит детей; без key соседние элементы в списке могут сопоставиться неверно, поэтому для списков нужны стабильные key. На экран попадает не «весь DOM заново», а минимальный набор операций, которые React передаёт в React DOM (createElement, setAttribute, insert, remove, текст). Commit — фаза применения этих изменений к реальному DOM; render — только вычисление нового дерева и diff. memo, useMemo и useCallback уменьшают лишние render-проходы, но финальное обновление DOM всё равно решает reconciler по результату diff.
-
Отличный ответ — эвристики diff, роль key, разделение фаз render и commit всё описаны. Следующий вопрос: что такое generic и зачем он нужен? Приведите пример из практики.
-
Generic — параметр типа: одна и та же логика работает с разными типами, а связь «вход → выход» сохраняется для компилятора. Без него пришлось бы дублировать функции или падать в any и терять проверки. Зачем: переиспользуемый код (API, коллекции, хелперы) остаётся типобезопасным — TypeScript знает, что вернулось именно то, что вы ожидали. Пример из практики — обёртка над fetch, которая парсит JSON в заданный тип: async function getJson<T>(url: string): Promise<T> { const res = await fetch(url); if (!res.ok) throw new Error(res.statusText); return res.json() as Promise<T>; } type User = { id: string; name: string }; const user = await getJson<User>('/api/me'); // user: User В React тот же приём у useState<User | null>(null) и у пропсов вроде List<TItem>.
-
Хороший пример с fetch-обёрткой и связью с useState — понятно и по делу. Следующий вопрос: чем Promise отличается от callback? Что такое Promise.all и Promise.race?
-
Callback — функция, которую вызывают, когда асинхронная работа готова; при нескольких шагах легко получить «лесенку» вложенных колбэков (callback hell) и сложнее централизованно ловить ошибки. Promise — объект с одним исходом: pending → fulfilled или rejected; цепочка через .then/.catch/finally или async/await, ошибка пробрасывается по цепочке. Promise.all([p1, p2, …]) ждёт все промисы; результат — массив значений в том же порядке. Если хотя бы один rejected — весь all сразу rejected с этой ошибкой (остальные могут ещё выполняться). Удобно для независимых параллельных запросов (профиль + настройки + список). Promise.race([p1, p2, …]) завершается как только первый промис settled — с его значением или ошибкой. Типичные кейсы: таймаут (Promise.race([fetch(url), sleep(5000)])) или «кто быстрее ответил». На собесе добавьте: Promise.allSettled — дождаться всех без провала целиком; Promise.any — первый успех (если все упали — AggregateError).
-
Отличный ответ — state machine промиса, поведение all при reject и практические кейсы race раскрыты, а упоминание allSettled и any только плюс. Осталось два вопроса. Предпоследний: когда использовать useMemo, useCallback и React.memo? И какие подводные камни есть у каждого из них?
-
useMemo кэширует результат вычисления между рендерами, пока не изменились зависимости; имеет смысл при тяжёлых расчётах или когда нужна стабильная ссылка на объект/массив для зависимостей дочернего memo. Камни: сам useMemo не бесплатен; неправильный массив deps → устаревшие данные; оборачивать простые примитивы бессмысленно. useCallback кэширует функцию по deps; нужен, если передаёте колбэк в React.memo-ребёнка или в хук с чувствительными deps (useEffect). Камни: без memo у ребёнка стабильная функция не уменьшит его ререндеры; deps забыли — stale closure; «обернуть всё» раздувает код. React.memo пропускает ререндер компонента, если props поверхностно равны прошлым; уместен для часто обновляемого родителя и дорогого чистого UI. Камни: сравнение shallow — новый {}/[]/inline-функция каждый раз ломает memo; дети всё равно ререндерятся, если ререндерится родитель и контекст изменился. Правило: сначала профилируйте или видите лишние ререндеры; в React 19 с Compiler ручной memo часто не нужен. Не мемоизируйте «на всякий случай» — это усложняет код и может замедлить лёгкие компоненты.
-
Отличный структурированный ответ — и подводные камни, и здравый принцип «сначала профилируйте», а упоминание React Compiler показывает, что следите за развитием экосистемы. Последний теоретический вопрос: объясните разницу между == и ==. И приведите пример неожиданного поведения ==.
-
Скорее всего, имелось в виду == и ===. === (strict) сравнивает без приведения типов: разные типы → сразу false, для объектов — одна и та же ссылка. == (loose) перед сравнением приводит типы по правилам JS, отсюда сюрпризы. Когда что: в современном коде почти всегда === (и !==); == — только если вы осознанно хотите такое приведение (на практике это редкость). Неожиданное поведение ==: [] == false // true ([] → "" → 0, false → 0) null == undefined // true '0' == 0 // true '' == 0 // true Пустой массив к false «равен» из‑за цепочки ToPrimitive → строка → число. null == undefined — единственная пара «похожих» значений, которая при == считается равной; с === оба случая дают false.
-
Да, верно — == и ===, спасибо за уточнение. Ответ отличный: правила приведения, null == undefined и цепочка ToPrimitive на примере [] == false раскрыты хорошо. На этом теоретическая часть завершена — благодарю за развёрнутые ответы по JavaScript, TypeScript, React и сетям, вы показали уверенное владение материалом. Дальше перейдём к практическому заданию, дайте минуту подготовить его описание.
Практика
Anagram
-
Теория закончена, переходим к задаче. Слева условие и редактор. «Запустить код» прогоняет открытые тесты и пишет вывод в консоль снизу — скрытые тесты на этом шаге не трогаются. Когда будете готовы, «Сдать решение»: прогонятся все тесты, включая скрытые, и интервью завершится. Если застряли, справа в чате можно спросить наводку — готовое решение целиком я не дам.
Задача 1: Anagram
Условие
## Anagram Даны две строки. Выведите `YES`, если они анаграммы (один набор символов), иначе `NO`. Регистр учитывается. Пример: ``` listen silent ``` Ответ: `YES`
Решение кандидата
const fs = require('fs');
const lines = fs.readFileSync(0, 'utf8').split('\n');
const a = (lines[0] ?? '').replace(/\r$/, '');
const b = (lines[1] ?? '').replace(/\r$/, '');
function solve(a, b) {
if (a.length !== b.length) return false;
const norm = (s) => [...s].sort().join('');
return norm(a) === norm(b);
}
console.log(solve(a, b) ? 'YES' : 'NO');
Тесты
Публичные
Публичные тесты не запускались.
Скрытые
Скрытые тесты не запускались.
Чат по задаче
-
Теория закончена, переходим к задаче. Слева условие и редактор. «Запустить код» прогоняет открытые тесты и пишет вывод в консоль снизу — скрытые тесты на этом шаге не трогаются. Когда будете готовы, «Сдать решение»: прогонятся все тесты, включая скрытые, и интервью завершится. Если застряли, справа в чате можно спросить наводку — готовое решение целиком я не дам.
- Покидал вкладку
- 0
- Терял фокус окна
- 21 (суммарно 2 мин 41 с)
- Копирований
- 13
- Вставок
- 14
- Вырезаний
- 0
Теория
-
Здравствуйте, рад вас видеть! Меня зовут Алексей, я проведу у вас собеседование. Формат такой: 12 теоретических вопросов по сценарию «React Frontend», затем будет практическое задание. Начнём с первого вопроса: объясните, пожалуйста, разницу между HTTP и HTTPS и как работает TLS?
-
Начнём с первого вопроса: объясните, пожалуйста, разницу между HTTP и HTTPS и как работает TLS?
-
HTTP передаёт запросы и ответы по сети в открытом виде: любой на пути (Wi‑Fi, провайдер) может прочитать или подменить данные. HTTPS — тот же HTTP, но поверх TLS: трафик шифруется, а сервер обычно подтверждает личность сертификатом от доверенного центра. TLS начинается с рукопожатия: клиент и сервер договариваются о версии протокола и алгоритмах, сервер отдаёт сертификат, клиент проверяет его и они совместно получают общий секретный ключ. Дальше этим ключом шифруется весь обмен (страницы, формы, куки). В браузере HTTPS виден по https:// и замку; без TLS пароли и сессии уязвимы к перехвату и подмене сайта.
-
Ответ через 1 мин 17 с
-
HTTP передаёт запросы и ответы по сети в открытом виде: любой на пути (Wi‑Fi, провайдер) может прочитать или подменить данные. HTTPS — тот же HTTP, но поверх TLS: трафик шифруется, а сервер обычно подтверждает личность сертификатом от доверенного центра. TLS начинается с рукопожатия: клиент и сервер договариваются о версии протокола и алгоритмах, сервер отдаёт сертификат, клиент проверяет его и они совместно получают общий секретный ключ. Дальше этим ключом шифруется весь обмен (страницы, формы, куки). В браузере HTTPS виден по https:// и замку; без TLS пароли и сессии уязвимы к перехвату и подмене сайта.
-
Хороший, структурированный ответ, особенно про рукопожатие и проверку сертификата. Переходим к React: опишите жизненный цикл компонента и как useEffect соотносится с mount, update и unmount?
-
опишите жизненный цикл компонента и как useEffect соотносится с mount, update и unmount?
-
В функциональных компонентах нет классических методов вроде componentDidMount, но та же логика есть: первый рендер — «рождение», повторные — «обновления», снятие с DOM — «размонтирование». useEffect выполняется после того, как React отрисовал изменения на экране, поэтому это не момент «до paint», а фаза после коммита в DOM. При mount эффект с [] в зависимостях срабатывает один раз после первого рендера. При update эффект с [a, b] перезапускается, когда меняются зависимости: сначала вызывается cleanup от прошлого запуска, затем новый эффект. При unmount React вызывает cleanup (функцию, которую вернул эффект) — там отписываются от событий, отменяют таймеры и прерывают запросы. Не путайте с useLayoutEffect: он синхронный до отрисовки браузером и нужен реже (измерения DOM, синхронная правка UI). Для подписок, fetch и аналитики обычно достаточно useEffect.
-
Ответ через 34 с
-
В функциональных компонентах нет классических методов вроде componentDidMount, но та же логика есть: первый рендер — «рождение», повторные — «обновления», снятие с DOM — «размонтирование». useEffect выполняется после того, как React отрисовал изменения на экране, поэтому это не момент «до paint», а фаза после коммита в DOM. При mount эффект с [] в зависимостях срабатывает один раз после первого рендера. При update эффект с [a, b] перезапускается, когда меняются зависимости: сначала вызывается cleanup от прошлого запуска, затем новый эффект. При unmount React вызывает cleanup (функцию, которую вернул эффект) — там отписываются от событий, отменяют таймеры и прерывают запросы. Не путайте с useLayoutEffect: он синхронный до отрисовки браузером и нужен реже (измерения DOM, синхронная правка UI). Для подписок, fetch и аналитики обычно достаточно useEffect.
-
Отличный ответ — особенно ценно, что упомянули порядок cleanup перед следующим эффектом и разницу с useLayoutEffect. Следующий вопрос: что происходит, когда вы вводите URL в браузере и нажимаете Enter? Расскажите по шагам, от DNS до отрисовки.
-
что происходит, когда вы вводите URL в браузере и нажимаете Enter? Расскажите по шагам, от DNS до отрисовки.
-
1. Парсинг URL — браузер определяет схему (https), хост, порт, путь и параметры; при необходимости дополняет https:// или отправляет в поиск. 2. DNS — по имени хоста запрашивается IP: кэш ОС/браузера → резолвер провайдера → корневые/TLD/авторitative-серверы; ответ кэшируется по TTL. 3. TCP и TLS — к IP:443 устанавливается TCP (SYN/ACK), затем TLS-handshake: сертификат, проверка, общий ключ; дальше HTTP идёт внутри зашифрованного канала.
-
Ответ через 21 с
-
1. Парсинг URL — браузер определяет схему (https), хост, порт, путь и параметры; при необходимости дополняет https:// или отправляет в поиск. 2. DNS — по имени хоста запрашивается IP: кэш ОС/браузера → резолвер провайдера → корневые/TLD/авторitative-серверы; ответ кэшируется по TTL. 3. TCP и TLS — к IP:443 устанавливается TCP (SYN/ACK), затем TLS-handshake: сертификат, проверка, общий ключ; дальше HTTP идёт внутри зашифрованного канала.
-
Хорошо, но вы остановились на TLS-хендшейке. Уточните: что происходит дальше — какие запросы браузер отправляет серверу и как страница попадает на экран (парсинг HTML, загрузка ресурсов, построение DOM/CSSOM)?
-
4. HTTP — отправляется запрос (метод, заголовки, cookies); сервер отвечает статусом и телом; для HTML часто идут редиректы (301/302), HSTS может сразу принудить HTTPS. 5. Разбор и загрузка ресурсов — HTML парсится потоково; находятся CSS, JS, шрифты, картинки; параллельно новые запросы (HTTP/2 мультиплексирование); JS может блокировать или отложить парсинг (defer/async). 6. Отрисовка — из DOM и CSS строится render tree, layout (геометрия), paint (пиксели), compositing (слои/GPU); интерактивность наступает после выполнения скриптов и события load/DOMContentLoaded.
-
Ответ через 20 с
-
4. HTTP — отправляется запрос (метод, заголовки, cookies); сервер отвечает статусом и телом; для HTML часто идут редиректы (301/302), HSTS может сразу принудить HTTPS. 5. Разбор и загрузка ресурсов — HTML парсится потоково; находятся CSS, JS, шрифты, картинки; параллельно новые запросы (HTTP/2 мультиплексирование); JS может блокировать или отложить парсинг (defer/async). 6. Отрисовка — из DOM и CSS строится render tree, layout (геометрия), paint (пиксели), compositing (слои/GPU); интерактивность наступает после выполнения скриптов и события load/DOMContentLoaded.
-
Отлично, теперь картина полная — от DNS до композитинга, и хорошо, что вспомнили HSTS и defer/async. Идём к JavaScript: чем отличаются var, let и const? И что такое hoisting?
-
чем отличаются var, let и const? И что такое hoisting?
-
var — функциональная область видимости: одна переменная на всю функцию, повторное объявление разрешено. let и const — блочная ({}): внутри if, цикла, блока они не «просачиваются» наружу. const нельзя переназначить (=), но если значение — объект или массив, его поля и элементы менять можно. let можно переназначать; для счётчиков и переприсваиваемых ссылок это нормальный выбор. Hoisting — на этапе компиляции объявления поднимаются в начало своей области: для var до присваивания значение undefined, для let/const — temporal dead zone до строки объявления, обращение раньше даёт ReferenceError. В современном JS обычно берут const по умолчанию, let когда нужно переприсвоение, var в новом коде почти не используют из‑за области видимости и hoisting. -
Ответ через 22 с
-
var — функциональная область видимости: одна переменная на всю функцию, повторное объявление разрешено. let и const — блочная ({}): внутри if, цикла, блока они не «просачиваются» наружу. const нельзя переназначить (=), но если значение — объект или массив, его поля и элементы менять можно. let можно переназначать; для счётчиков и переприсваиваемых ссылок это нормальный выбор. Hoisting — на этапе компиляции объявления поднимаются в начало своей области: для var до присваивания значение undefined, для let/const — temporal dead zone до строки объявления, обращение раньше даёт ReferenceError. В современном JS обычно берут const по умолчанию, let когда нужно переприсвоение, var в новом коде почти не используют из‑за области видимости и hoisting.
-
Полный и уверенный ответ — TDZ и мутабельность полей const упомянуты, хорошо. Следующий вопрос по TypeScript: что такое type narrowing и как работает discriminated union?
-
Следующий вопрос по TypeScript: что такое type narrowing и как работает discriminated union?
-
Type narrowing — когда TypeScript после проверки в коде сужает тип переменной: из широкого union или unknown остаётся конкретный вариант. Это делают typeof, instanceof, проверки на null/undefined, пользовательские type guards (x is Foo) и иногда assertion functions. Discriminated union — union, у которого у каждого варианта есть общее поле‑метка с литеральным типом (discriminant), например kind: 'success' | 'error'. В switch (result.kind) или в if (result.kind === 'error') TypeScript автоматически сужает тип: в ветке 'error' доступны только поля ошибки, в 'success' — только поля успеха. Так исчезают лишние optional‑поля и «угадывание» структуры: компилятор сам знает, какой shape у текущей ветки. На собеседовании часто просят показать обработку { kind: 'ok', data } | { kind: 'fail', message } без as и с исчерпывающим switch (иногда с never для забытых кейсов). -
Ответ через 19 с
-
Type narrowing — когда TypeScript после проверки в коде сужает тип переменной: из широкого union или unknown остаётся конкретный вариант. Это делают typeof, instanceof, проверки на null/undefined, пользовательские type guards (x is Foo) и иногда assertion functions. Discriminated union — union, у которого у каждого варианта есть общее поле‑метка с литеральным типом (discriminant), например kind: 'success' | 'error'. В switch (result.kind) или в if (result.kind === 'error') TypeScript автоматически сужает тип: в ветке 'error' доступны только поля ошибки, в 'success' — только поля успеха. Так исчезают лишние optional‑поля и «угадывание» структуры: компилятор сам знает, какой shape у текущей ветки. На собеседовании часто просят показать обработку { kind: 'ok', data } | { kind: 'fail', message } без as и с исчерпывающим switch (иногда с never для забытых кейсов).
-
Уверенный ответ — narrowing, discriminant, исчерпывающий switch с never-проверкой, всё на месте. Переходим дальше: чем отличаются any, unknown и never? И когда какой тип уместно использовать?
-
чем отличаются any, unknown и never? И когда какой тип уместно использовать?
-
any отключает проверку типов: с ним можно почти что угодно, но теряются автодополнение и защита от ошибок. Его оставляют для миграции с JS, редких legacy‑участков или когда тип действительно неизвестен и вы сознательно не проверяете — в новом коде лучше избегать. unknown — «значение есть, но тип неизвестен»: присвоить можно откуда угодно, а вот использовать (поля, вызов, арифметика) — только после сужения (typeof, guard, парсинг схемой). Это нормальный выбор для JSON из API, catch (e: unknown) и обёрток над внешними данными. never — тип, которому не соответствует ни одно значение: функция, которая всегда бросает или зацикливается, или ветка, которую TypeScript считает недостижимой. В switch по discriminated union параметр x: never в default ловит забытые кейсы; в generics never часто означает «этот вариант комбинации невозможен».
-
Ответ через 17 с
-
any отключает проверку типов: с ним можно почти что угодно, но теряются автодополнение и защита от ошибок. Его оставляют для миграции с JS, редких legacy‑участков или когда тип действительно неизвестен и вы сознательно не проверяете — в новом коде лучше избегать. unknown — «значение есть, но тип неизвестен»: присвоить можно откуда угодно, а вот использовать (поля, вызов, арифметика) — только после сужения (typeof, guard, парсинг схемой). Это нормальный выбор для JSON из API, catch (e: unknown) и обёрток над внешними данными. never — тип, которому не соответствует ни одно значение: функция, которая всегда бросает или зацикливается, или ветка, которую TypeScript считает недостижимой. В switch по discriminated union параметр x: never в default ловит забытые кейсы; в generics never часто означает «этот вариант комбинации невозможен».
-
Отличный ответ — граница между any и unknown, сужение перед использованием и never в exhaustive switch описаны верно. Идём дальше: объясните разницу между shallow copy и deep copy объекта. Когда каждого из них достаточно и как их получить?
-
объясните разницу между shallow copy и deep copy объекта. Когда каждого из них достаточно и как их получить?
-
Shallow copy копирует только «верхний» слой: новый объект/массив, но вложенные объекты остаются теми же ссылками — изменение copy.nested.x затронет и оригинал. Его достаточно, когда структура плоская или вложенность намеренно общая (immutable‑паттерн: меняете только корень через spread). Получить shallow: { ...obj }, Object.assign({}, obj), [...arr], arr.slice(), structuredClone не нужен для этого уровня. Deep copy рекурсивно дублирует всё дерево — независимые вложенные объекты. Нужен, когда клонируете конфиг/состояние с глубокой вложенностью и потом мутируете копию, не трогая источник. Способы: structuredClone(obj) (современный стандарт, без функций, DOM, символов с нюансами), JSON.parse(JSON.stringify(obj)) только для JSON‑данных (теряются Date, undefined, функции), lodash cloneDeep для сложных legacy‑случаев. В React чаще избегают deep copy через иммутабельные обновления (копируете только изменённую ветку), а не клонируете весь граф целиком. -
Ответ через 16 с
-
Shallow copy копирует только «верхний» слой: новый объект/массив, но вложенные объекты остаются теми же ссылками — изменение copy.nested.x затронет и оригинал. Его достаточно, когда структура плоская или вложенность намеренно общая (immutable‑паттерн: меняете только корень через spread). Получить shallow: { ...obj }, Object.assign({}, obj), [...arr], arr.slice(), structuredClone не нужен для этого уровня. Deep copy рекурсивно дублирует всё дерево — независимые вложенные объекты. Нужен, когда клонируете конфиг/состояние с глубокой вложенностью и потом мутируете копию, не трогая источник. Способы: structuredClone(obj) (современный стандарт, без функций, DOM, символов с нюансами), JSON.parse(JSON.stringify(obj)) только для JSON‑данных (теряются Date, undefined, функции), lodash cloneDeep для сложных legacy‑случаев. В React чаще избегают deep copy через иммутабельные обновления (копируете только изменённую ветку), а не клонируете весь граф целиком.
-
Полный ответ, плюс приятно, что связали с иммутабельными обновлениями в React. Следующий вопрос: что такое reconciliation и virtual DOM? Как React решает, что именно перерисовать?
-
Следующий вопрос: что такое reconciliation и virtual DOM? Как React решает, что именно перерисовать?
-
Virtual DOM — лёгкое дерево описания UI в памяти (элементы, props, дети). После изменения state React строит новое дерево и сравнивает его со снимком прошлого рендера — это reconciliation (diff). Сравнение идёт сверху вниз с эвристиками: элементы с разным type (например div → span) — старый поддерево снимается, новое монтируется заново. При том же type React обновляет props и рекурсивно обходит детей; без key соседние элементы в списке могут сопоставиться неверно, поэтому для списков нужны стабильные key. На экран попадает не «весь DOM заново», а минимальный набор операций, которые React передаёт в React DOM (createElement, setAttribute, insert, remove, текст). Commit — фаза применения этих изменений к реальному DOM; render — только вычисление нового дерева и diff. memo, useMemo и useCallback уменьшают лишние render-проходы, но финальное обновление DOM всё равно решает reconciler по результату diff.
-
Ответ через 18 с
-
Virtual DOM — лёгкое дерево описания UI в памяти (элементы, props, дети). После изменения state React строит новое дерево и сравнивает его со снимком прошлого рендера — это reconciliation (diff). Сравнение идёт сверху вниз с эвристиками: элементы с разным type (например div → span) — старый поддерево снимается, новое монтируется заново. При том же type React обновляет props и рекурсивно обходит детей; без key соседние элементы в списке могут сопоставиться неверно, поэтому для списков нужны стабильные key. На экран попадает не «весь DOM заново», а минимальный набор операций, которые React передаёт в React DOM (createElement, setAttribute, insert, remove, текст). Commit — фаза применения этих изменений к реальному DOM; render — только вычисление нового дерева и diff. memo, useMemo и useCallback уменьшают лишние render-проходы, но финальное обновление DOM всё равно решает reconciler по результату diff.
-
Отличный ответ — эвристики diff, роль key, разделение фаз render и commit всё описаны. Следующий вопрос: что такое generic и зачем он нужен? Приведите пример из практики.
-
что такое generic и зачем он нужен? Приведите пример из практики.
-
Generic — параметр типа: одна и та же логика работает с разными типами, а связь «вход → выход» сохраняется для компилятора. Без него пришлось бы дублировать функции или падать в any и терять проверки. Зачем: переиспользуемый код (API, коллекции, хелперы) остаётся типобезопасным — TypeScript знает, что вернулось именно то, что вы ожидали. Пример из практики — обёртка над fetch, которая парсит JSON в заданный тип: async function getJson<T>(url: string): Promise<T> { const res = await fetch(url); if (!res.ok) throw new Error(res.statusText); return res.json() as Promise<T>; } type User = { id: string; name: string }; const user = await getJson<User>('/api/me'); // user: User В React тот же приём у useState<User | null>(null) и у пропсов вроде List<TItem>. -
Ответ через 50 с
-
Generic — параметр типа: одна и та же логика работает с разными типами, а связь «вход → выход» сохраняется для компилятора. Без него пришлось бы дублировать функции или падать в any и терять проверки. Зачем: переиспользуемый код (API, коллекции, хелперы) остаётся типобезопасным — TypeScript знает, что вернулось именно то, что вы ожидали. Пример из практики — обёртка над fetch, которая парсит JSON в заданный тип: async function getJson<T>(url: string): Promise<T> { const res = await fetch(url); if (!res.ok) throw new Error(res.statusText); return res.json() as Promise<T>; } type User = { id: string; name: string }; const user = await getJson<User>('/api/me'); // user: User В React тот же приём у useState<User | null>(null) и у пропсов вроде List<TItem>.
-
Хороший пример с fetch-обёрткой и связью с useState — понятно и по делу. Следующий вопрос: чем Promise отличается от callback? Что такое Promise.all и Promise.race?
-
Следующий вопрос: чем Promise отличается от callback? Что такое Promise.all и Promise.race?
-
Callback — функция, которую вызывают, когда асинхронная работа готова; при нескольких шагах легко получить «лесенку» вложенных колбэков (callback hell) и сложнее централизованно ловить ошибки. Promise — объект с одним исходом: pending → fulfilled или rejected; цепочка через .then/.catch/finally или async/await, ошибка пробрасывается по цепочке. Promise.all([p1, p2, …]) ждёт все промисы; результат — массив значений в том же порядке. Если хотя бы один rejected — весь all сразу rejected с этой ошибкой (остальные могут ещё выполняться). Удобно для независимых параллельных запросов (профиль + настройки + список). Promise.race([p1, p2, …]) завершается как только первый промис settled — с его значением или ошибкой. Типичные кейсы: таймаут (Promise.race([fetch(url), sleep(5000)])) или «кто быстрее ответил». На собесе добавьте: Promise.allSettled — дождаться всех без провала целиком; Promise.any — первый успех (если все упали — AggregateError).
-
Ответ через 22 с
-
Callback — функция, которую вызывают, когда асинхронная работа готова; при нескольких шагах легко получить «лесенку» вложенных колбэков (callback hell) и сложнее централизованно ловить ошибки. Promise — объект с одним исходом: pending → fulfilled или rejected; цепочка через .then/.catch/finally или async/await, ошибка пробрасывается по цепочке. Promise.all([p1, p2, …]) ждёт все промисы; результат — массив значений в том же порядке. Если хотя бы один rejected — весь all сразу rejected с этой ошибкой (остальные могут ещё выполняться). Удобно для независимых параллельных запросов (профиль + настройки + список). Promise.race([p1, p2, …]) завершается как только первый промис settled — с его значением или ошибкой. Типичные кейсы: таймаут (Promise.race([fetch(url), sleep(5000)])) или «кто быстрее ответил». На собесе добавьте: Promise.allSettled — дождаться всех без провала целиком; Promise.any — первый успех (если все упали — AggregateError).
-
Отличный ответ — state machine промиса, поведение all при reject и практические кейсы race раскрыты, а упоминание allSettled и any только плюс. Осталось два вопроса. Предпоследний: когда использовать useMemo, useCallback и React.memo? И какие подводные камни есть у каждого из них?
-
Предпоследний: когда использовать useMemo, useCallback и React.memo? И какие подводные камни есть у каждого из них?
-
useMemo кэширует результат вычисления между рендерами, пока не изменились зависимости; имеет смысл при тяжёлых расчётах или когда нужна стабильная ссылка на объект/массив для зависимостей дочернего memo. Камни: сам useMemo не бесплатен; неправильный массив deps → устаревшие данные; оборачивать простые примитивы бессмысленно. useCallback кэширует функцию по deps; нужен, если передаёте колбэк в React.memo-ребёнка или в хук с чувствительными deps (useEffect). Камни: без memo у ребёнка стабильная функция не уменьшит его ререндеры; deps забыли — stale closure; «обернуть всё» раздувает код. React.memo пропускает ререндер компонента, если props поверхностно равны прошлым; уместен для часто обновляемого родителя и дорогого чистого UI. Камни: сравнение shallow — новый {}/[]/inline-функция каждый раз ломает memo; дети всё равно ререндерятся, если ререндерится родитель и контекст изменился. Правило: сначала профилируйте или видите лишние ререндеры; в React 19 с Compiler ручной memo часто не нужен. Не мемоизируйте «на всякий случай» — это усложняет код и может замедлить лёгкие компоненты. -
Ответ через 21 с
-
useMemo кэширует результат вычисления между рендерами, пока не изменились зависимости; имеет смысл при тяжёлых расчётах или когда нужна стабильная ссылка на объект/массив для зависимостей дочернего memo. Камни: сам useMemo не бесплатен; неправильный массив deps → устаревшие данные; оборачивать простые примитивы бессмысленно. useCallback кэширует функцию по deps; нужен, если передаёте колбэк в React.memo-ребёнка или в хук с чувствительными deps (useEffect). Камни: без memo у ребёнка стабильная функция не уменьшит его ререндеры; deps забыли — stale closure; «обернуть всё» раздувает код. React.memo пропускает ререндер компонента, если props поверхностно равны прошлым; уместен для часто обновляемого родителя и дорогого чистого UI. Камни: сравнение shallow — новый {}/[]/inline-функция каждый раз ломает memo; дети всё равно ререндерятся, если ререндерится родитель и контекст изменился. Правило: сначала профилируйте или видите лишние ререндеры; в React 19 с Compiler ручной memo часто не нужен. Не мемоизируйте «на всякий случай» — это усложняет код и может замедлить лёгкие компоненты.
-
Отличный структурированный ответ — и подводные камни, и здравый принцип «сначала профилируйте», а упоминание React Compiler показывает, что следите за развитием экосистемы. Последний теоретический вопрос: объясните разницу между == и ==. И приведите пример неожиданного поведения ==.
-
Последний теоретический вопрос: объясните разницу между == и ==. И приведите пример неожиданного поведения ==.
-
Скорее всего, имелось в виду == и ===. === (strict) сравнивает без приведения типов: разные типы → сразу false, для объектов — одна и та же ссылка. == (loose) перед сравнением приводит типы по правилам JS, отсюда сюрпризы. Когда что: в современном коде почти всегда === (и !==); == — только если вы осознанно хотите такое приведение (на практике это редкость). Неожиданное поведение ==: [] == false // true ([] → "" → 0, false → 0) null == undefined // true '0' == 0 // true '' == 0 // true Пустой массив к false «равен» из‑за цепочки ToPrimitive → строка → число. null == undefined — единственная пара «похожих» значений, которая при == считается равной; с === оба случая дают false.
-
Ответ через 18 с
-
Скорее всего, имелось в виду == и ===. === (strict) сравнивает без приведения типов: разные типы → сразу false, для объектов — одна и та же ссылка. == (loose) перед сравнением приводит типы по правилам JS, отсюда сюрпризы. Когда что: в современном коде почти всегда === (и !==); == — только если вы осознанно хотите такое приведение (на практике это редкость). Неожиданное поведение ==: [] == false // true ([] → "" → 0, false → 0) null == undefined // true '0' == 0 // true '' == 0 // true Пустой массив к false «равен» из‑за цепочки ToPrimitive → строка → число. null == undefined — единственная пара «похожих» значений, которая при == считается равной; с === оба случая дают false.
-
Да, верно — == и ===, спасибо за уточнение. Ответ отличный: правила приведения, null == undefined и цепочка ToPrimitive на примере [] == false раскрыты хорошо. На этом теоретическая часть завершена — благодарю за развёрнутые ответы по JavaScript, TypeScript, React и сетям, вы показали уверенное владение материалом. Дальше перейдём к практическому заданию, дайте минуту подготовить его описание.
Практика
Anagram
-
Теория закончена, переходим к задаче. Слева условие и редактор. «Запустить код» прогоняет открытые тесты и пишет вывод в консоль снизу — скрытые тесты на этом шаге не трогаются. Когда будете готовы, «Сдать решение»: прогонятся все тесты, включая скрытые, и интервью завершится. Если застряли, справа в чате можно спросить наводку — готовое решение целиком я не дам.
-
function solve(a, b) { if (a.length !== b.length) return false; const norm = (s) => [...s].sort().join(''); return norm(a) === norm(b); }
networking
8/10Вопрос Объясните разницу между HTTP и HTTPS. Как работает TLS?
Ответ HTTP передаёт запросы и ответы по сети в открытом виде: любой на пути (Wi‑Fi, провайдер) может прочитать или подменить данные. HTTPS — тот же HTTP, но поверх TLS: трафик шифруется, а сервер обычно подтверждает личность сертификатом от доверенного центра. TLS начинается с рукопожатия: клиент и сервер договариваются о версии протокола и алгоритмах, сервер отдаёт сертификат, клиент проверяет его и они совместно получают общий секретный ключ. Дальше этим ключом шифруется весь обмен (страницы, формы, куки). В браузере HTTPS виден по https:// и замку; без TLS пароли и сессии уязвимы к перехвату и подмену сайта.
Комментарий Корректное объяснение открытой передачи HTTP, шифрования TLS, рукопожатия и проверки сертификата. Уверенный уровень.
React
8/10Вопрос Объясните жизненный цикл компонента и как useEffect соотносится с mount/update/unmount.
Ответ В функциональных компонентах нет классических методов вроде componentDidMount, но та же логика есть: первый рендер — «рождение», повторные — «обновления», снятие с DOM — «размонтирование». useEffect выполняется после того, как React отрисовал изменения на экране, поэтому это не момент «до paint», а фаза после коммита в DOM. При mount эффект с [] в зависимостях срабатывает один раз после первого рендера. При update эффект с [a, b] перезапускается, когда меняются зависимости: сначала вызывается cleanup от прошлого запуска, затем новый эффект. При unmount React вызывает cleanup (функцию, которую вернул эффект) — там отписываются от событий, отменяют таймеры и прерывают запросы. Не путайте с useLayoutEffect: он синхронный до отрисовки браузером и нужен реже (измерения DOM, синхронная правка UI). Для подписок, fetch и аналитики обычно достаточно useEffect.
Комментарий Полный ответ: mount/update/unmount, порядок cleanup, отличие от useLayoutEffect. Выше ожиданий Middle.
networking
8/10Вопрос Что происходит, когда вы вводите URL в браузере и нажимаете Enter?
Ответ 1. Парсинг URL — браузер определяет схему (https), хост, порт, путь и параметры; при необходимости дополняет https:// или отправляет в поиск. 2. DNS — по имени хоста запрашивается IP: кэш ОС/браузера → резолвер провайдера → корневые/TLD/авторitative-серверы; ответ кэшируется по TTL. 3. TCP и TLS — к IP:443 устанавливается TCP (SYN/ACK), затем TLS-handshake: сертификат, проверка, общий ключ; дальше HTTP идёт внутри зашифрованного канала. 4. HTTP — отправляется запрос (метод, заголовки, cookies); сервер отвечает статусом и телом; для HTML часто идут редиректы (301/302), HSTS может сразу принудить HTTPS. 5. Разбор и загрузка ресурсов — HTML парсится потоково; находятся CSS, JS, шрифты, картинки; параллельно новые запросы (HTTP/2 мультиплексирование); JS может блокировать или отложить парсинг (defer/async). 6. Отрисовка — из DOM и CSS строится render tree, layout (геометрия), paint (пиксели), compositing (слои/GPU); интерактивность наступает после выполнения скриптов и события load/DOMContentLoaded.
Комментарий Ответ на уточнение дан полностью: DNS, TCP/TLS, HTTP, парсинг, render tree, композитинг, HSTS, defer/async. Структурировано и глубоко.
JavaScript
8/10Вопрос Чем отличаются var, let и const? Что такое hoisting?
Ответ var — функциональная область видимости: одна переменная на всю функцию, повторное объявление разрешено. let и const — блочная ({}): внутри if, цикла, блока они не «просачиваются» наружу. const нельзя переназначить (=), но если значение — объект или массив, его поля и элементы менять можно. let можно переназначать; для счётчиков и переприсваиваемых ссылок это нормальный выбор. Hoisting — на этапе компиляции объявления поднимаются в начало своей области: для var до присваивания значение undefined, для let/const — temporal dead zone до строки объявления, обращение раньше даёт ReferenceError. В современном JS обычно берут const по умолчанию, let когда нужно переприсвоение, var в новом коде почти не используют из‑за области видимости и hoisting.
Комментарий Полный ответ: области видимости, мутабельность const-объектов, TDZ, практические рекомендации. Уверенно.
TypeScript
8/10Вопрос Что такое type narrowing и как работает discriminated union?
Ответ Type narrowing — когда TypeScript после проверки в коде сужает тип переменной: из широкого union или unknown остаётся конкретный вариант. Это делают typeof, instanceof, проверки на null/undefined, пользовательские type guards (x is Foo) и иногда assertion functions. Discriminated union — union, у которого у каждого варианта есть общее поле‑метка с литеральным типом (discriminant), например kind: 'success' | 'error'. В switch (result.kind) или в if (result.kind === 'error') TypeScript автоматически сужает тип: в ветке 'error' доступны только поля ошибки, в 'success' — только поля успеха. Так исчезают лишние optional‑поля и «угадывание» структуры: компилятор сам знает, какой shape у текущей ветки. На собеседовании часто просят показать обработку { kind: 'ok', data } | { kind: 'fail', message } без as и с исчерпывающим switch (иногда с never для забытых кейсов).
Комментарий Narrowing, type guards, discriminant, exhaustive switch с never — всё на месте, с примерами.
TypeScript
8/10Вопрос Чем отличаются any, unknown и never? Когда использовать каждый?
Ответ any отключает проверку типов: с ним можно почти что угодно, но теряются автодополнение и защита от ошибок. Его оставляют для миграции с JS, редких legacy‑участков или когда тип действительно неизвестен и вы сознательно не проверяете — в новом коде лучше избегать. unknown — «значение есть, но тип неизвестен»: присвоить можно откуда угодно, а вот использовать (поля, вызов, арифметика) — только после сужения (typeof, guard, парсинг схемой). Это нормальный выбор для JSON из API, catch (e: unknown) и обёрток над внешними данными. never — тип, которому не соответствует ни одно значение: функция, которая всегда бросает или зацикливается, или ветка, которую TypeScript считает недостижимой. В switch по discriminated union параметр x: never в default ловит забытые кейсы; в generics never часто означает «этот вариант комбинации невозможен».
Комментарий Чёткое разграничение трёх типов и корректные сценарии использования. Уверенный уровень.
JavaScript
8/10Вопрос Объясните разницу между shallow copy и deep copy объекта.
Ответ Shallow copy копирует только «верхний» слой: новый объект/массив, но вложенные объекты остаются теми же ссылками — изменение copy.nested.x затронет и оригинал. Его достаточно, когда структура плоская или вложенность намеренно общая (immutable‑паттерн: меняете только корень через spread). Получить shallow: { ...obj }, Object.assign({}, obj), [...arr], arr.slice(). Deep copy рекурсивно дублирует всё дерево — независимые вложенные объекты. Нужен, когда клонируете конфиг/состояние с глубокой вложенностью и потом мутируете копию, не трогая источник. Способы: structuredClone(obj) (современный стандарт, без функций, DOM, символов с нюансами), JSON.parse(JSON.stringify(obj)) только для JSON‑данных (теряются Date, undefined, функции), lodash cloneDeep для сложных legacy‑случаев. В React чаще избегают deep copy через иммутабельные обновления (копируете только изменённую ветку), а не клонируете весь граф целиком.
Комментарий Полный ответ со способами получения обеих копий, ограничениями JSON.parse/stringify и связью с иммутабельными обновлениями React.
React
8/10Вопрос Что такое reconciliation и virtual DOM? Как React решает, что перерисовать?
Ответ Virtual DOM — лёгкое дерево описания UI в памяти (элементы, props, дети). После изменения state React строит новое дерево и сравнивает его со снимком прошлого рендера — это reconciliation (diff). Сравнение идёт сверху вниз с эвристиками: элементы с разным type (например div → span) — старый поддерево снимается, новое монтируется заново. При том же type React обновляет props и рекурсивно обходит детей; без key соседние элементы в списке могут сопоставиться неверно, поэтому для списков нужны стабильные key. На экран попадает не «весь DOM заново», а минимальный набор операций, которые React передаёт в React DOM. Commit — фаза применения этих изменений к реальному DOM; render — только вычисление нового дерева и diff. memo, useMemo и useCallback уменьшают лишние render-проходы, но финальное обновление DOM всё равно решает reconciler по результату diff.
Комментарий Верно описаны diff-эвристики, роль key, разделение фаз render и commit. Глубоко и точно.
TypeScript
7/10Вопрос Что такое generic и зачем он нужен? Приведите пример.
Ответ Generic — параметр типа: одна и та же логика работает с разными типами, а связь «вход → выход» сохраняется для компилятора. Без него пришлось бы дублировать функции или падать в any и терять проверки. Зачем: переиспользуемый код (API, коллекции, хелперы) остаётся типобезопасным. Пример из практики — обёртка над fetch, которая парсит JSON в заданный тип: async function getJson<T>(url: string): Promise<T> { const res = await fetch(url); if (!res.ok) throw new Error(res.statusText); return res.json() as Promise<T>; } В React тот же приём у useState<User | null>(null) и у пропсов вроде List<TItem>.
Комментарий Понятное объяснение и корректный практический пример с fetch-обёрткой и useState. Хорошо, но без продвинутых constraints.
JavaScript
8/10Вопрос Чем Promise отличается от callback? Что такое Promise.all и Promise.race?
Ответ Callback — функция, которую вызывают, когда асинхронная работа готова; при нескольких шагах легко получить «лесенку» вложенных колбэков (callback hell) и сложнее централизованно ловить ошибки. Promise — объект с одним исходом: pending → fulfilled или rejected; цепочка через .then/.catch/finally или async/await, ошибка пробрасывается по цепочке. Promise.all([p1, p2, …]) ждёт все промисы; результат — массив значений в том же порядке. Если хотя бы один rejected — весь all сразу rejected с этой ошибкой. Promise.race([p1, p2, …]) завершается как только первый промис settled — с его значением или ошибкой. Типичные кейсы: таймаут (Promise.race([fetch(url), sleep(5000)])) или «кто быстрее ответил». На собесе добавьте: Promise.allSettled — дождаться всех без провала целиком; Promise.any — первый успех (если все упали — AggregateError).
Комментарий State machine промиса, поведение all при reject, кейсы race, плюс allSettled и any. Сверх базового вопроса.
React
8/10Вопрос Когда использовать useMemo, useCallback и React.memo? Какие есть подводные камни?
Ответ useMemo кэширует результат вычисления между рендерами, пока не изменились зависимости; имеет смысл при тяжёлых расчётах или когда нужна стабильная ссылка на объект/массив для зависимостей дочернего memo. Камни: сам useMemo не бесплатен; неправильный массив deps → устаревшие данные; оборачивать простые примитивы бессмысленно. useCallback кэширует функцию по deps; нужен, если передаёте колбэк в React.memo-ребёнка или в хук с чувствительными deps (useEffect). Камни: без memo у ребёнка стабильная функция не уменьшит его ререндеры; deps забыли — stale closure; «обернуть всё» раздувает код. React.memo пропускает ререндер компонента, если props поверхностно равны прошлым. Камни: сравнение shallow — новый {}/[]/inline-функция каждый раз ломает memo. Правило: сначала профилируйте или видите лишние ререндеры; в React 19 с Compiler ручной memo часто не нужен. Не мемоизируйте «на всякий случай».
Комментарий Структурированный разбор всех трёх инструментов, подводные камни, принцип «сначала профилируйте», знание React Compiler. Отлично.
JavaScript
8/10Вопрос Объясните разницу между == и ===. Приведите пример неожиданного поведения ==.
Ответ Скорее всего, имелось в виду == и ===. === (strict) сравнивает без приведения типов: разные типы → сразу false, для объектов — одна и та же ссылка. == (loose) перед сравнением приводит типы по правилам JS, отсюда сюрпризы. Неожиданное поведение ==: [] == false // true ([] → "" → 0, false → 0); null == undefined // true; '0' == 0 // true; '' == 0 // true. Пустой массив к false «равен» из‑за цепочки ToPrimitive → строка → число. null == undefined — единственная пара «похожих» значений, которая при == считается равной; с === оба случая дают false.
Комментарий Кандидат корректно уточнил формулировку, объяснил правила приведения и привёл примеры с ToPrimitive. Уверенно.