Топ вопросов для JS-собеса
1. Циклические ссылки
Какие потенциальные проблемы сопутствуют возникновеню циклических ссылок?
Циклические ссылки в JavaScript возникают, когда два или более объекта ссылаются друг на друга напрямую или через цепочку свойств.
- 🔋 Утечки памяти (Memory Leaks)
- Проблема: Старые браузеры (с алгоритмом подсчета ссылок) не могли удалить такие объекты.
- Итог: Память засоряется, приложение начинает тормозить и в итоге аварийно завершает работу.
- Нюанс: Современный алгоритм Mark-and-Sweep справляется с этим, если объекты недостижимы от корня (window/global). Но если хоть один объект в цикле «жив», вся цепочка останется в памяти навсегда.
- 💥 Ошибки сериализации в JSON
- Проблема: Метод
JSON.stringify()не умеет работать с циклическими структурами. - Итог: При попытке конвертировать такой объект в строку код упадет с критической ошибкой: TypeError: Converting circular structure to JSON.
- 🌀 Бесконечная рекурсия при обходе
- Проблема: При попытке глубокого копирования (например, своей функцией), валидации или выводе объекта в лог алгоритм будет бесконечно ходить по кругу.
- Итог: Переполнение стека вызовов (RangeError: Maximum call stack size exceeded) и падение приложения.
- 🕸️ Сложность поддержки и отладки
- Проблема: Структура данных теряет прозрачность. При чтении логов в консоли трудно понять, где истинный конец данных.
- Итог: Разработчикам сложно предсказать поведение системы, возрастает риск случайно изменить связанные данные в неожиданном месте.
- 🔒 Проблемы с паттернами иммутабельности
- Проблема: В современных фреймворках (React, Redux) принято создавать новые копии объектов при изменении данных, а не менять старые.
- Итог: Наличие циклов делает иммутабельное обновление структуры практически невозможным или крайне ресурсоемким.
Вот компактный структурированный конспект для подготовки к собеседованию. Каждому пункту подобран точный визуальный якорь для быстрого запоминания:
5 проблем при возникновении циклических ссылок в JS
🧠💧 Утечки памяти (Memory Leaks)
Современный алгоритм Mark-and-Sweep очистит изолированный цикл, только если он недостижим от корня (window/global). Но если хотя бы один объект из этого цикла остается доступным в коде, вся цепочка навечно застревает в памяти, постепенно забивая ресурсы устройства.
💥📄 Ошибки сериализации в JSON Метод JSON.stringify() технически не способен обрабатывать циклические структуры данных. При попытке преобразовать такой объект в строку код мгновенно упадет с критической ошибкой TypeError: Converting circular structure to JSON.
🌀♾️ Бесконечная рекурсия при обходе
Любые стандартные алгоритмы глубокого копирования, валидации или сериализации данных будут бесконечно ходить по кругу. Это гарантированно приведет к переполнению стека вызовов (RangeError: Maximum call stack size exceeded) и падению приложения.
🕸️🔍 Сложность поддержки и отладки
Структура данных теряет прозрачность и линейность. Разработчику становится крайне трудно читать логи в консоли, отслеживать архитектурные связи и предсказывать поведение системы. Сильно возрастает риск случайно изменить данные в неожиданном месте.
🔒🧱 Проблемы с паттернами иммутабельности
В современных фреймворках (React, Redux) запрещено мутировать состояние напрямую. Наличие циклов делает невозможным поверхностное копирование через спред (...), ломает сравнение ссылок для перерендера и требует сложного кэширования при глубоком обновлении.
2. 5 проблем при хранении JWT в Local Storage
Почеу хранение JWT в LocalStorage небезопасно?
5 проблем при хранении JWT в Local Storage
🥷🔓 Тотальная уязвимость к XSS-атакам (Cross-Site Scripting)
Любой JavaScript-код на вашей странице имеет прямой доступ к localStorage. Если злоумышленник сможет внедрить свой скрипт (через уязвимость в input, вредоносную npm-зависимость или сторонний аналитический виджет), он одной строчкой localStorage.getItem('token') украдет токен.
📡🛑 Отсутствие сетевой защиты (No Network Flags)
В отличие от Cookie, localStorage — это просто изолированное хранилище на клиенте. Вы не можете настроить для него флаг Secure (который запрещает отправку данных по незащищенному HTTP) или ограничить доступ на уровне сетевых запросов браузера.
⏳❌ Отсутствие автоматического истечения срока (No Auto-Expiry)
Записи в localStorage хранятся вечно, пока пользователь или ваш код вручную их не сотрет. Если пользователь залогинился в интернет-кафе и просто закрыл вкладку, токен останется лежать в браузере компьютера, доступный любому следующему посетителю.
🌎❌ Географическая изоляция домена (No Subdomain Sharing)
localStorage жестко привязан к конкретному протоколу и домену (Origin). Если ваше фронтенд-приложение лежит на ://app.site.com, а панель управления на ://admin.site.com, вы не сможете штатными средствами расшарить токен из localStorage между этими поддоменами. Придется писать сложные обходные пути (через iframe и postMessage).
🕵️♂️📊 Раскрытие личных данных пользователя (Data Exposure)
JWT-токен не зашифрован, он просто закодирован в Base64. Любой человек, получивший физический доступ к устройству или открывший вкладку Application в DevTools, может легко декодировать токен и прочитать все зашитые туда данные: роли, права, ID и email пользователя
Как защититься? (Чек-лист для собеседования)
Если вас спросят, как минимизировать риски XSS, назовите эти 4 уровня защиты:
- Никогда не хранить JWT в localStorage (использовать HttpOnly куки).
- Экранирование и санитизация (Sanitization): Всегда очищать данные от пользователей на бэкенде и фронтенде (использовать библиотеки вроде DOMPurify). Никогда не вставлять сырой HTML через
dangerouslySetInnerHTMLв React илиv-htmlв Vue без строгой проверки. - CSP (Content Security Policy): Настроить HTTP-заголовки безопасности, которые строго запрещают браузеру загружать скрипты со сторонних неизвестных доменов и выполнять встроенный (inline) JS-код.
- Аудит зависимостей: Регулярно запускать npm audit или использовать инструменты вроде Snyk для проверки уязвимостей в сторонних пакетах.
Почему куки защищают от XSS, но создают угрозу CSRF-атаки, и как с ней бороться?
Куки с флагом HttpOnly полностью закрывают доступ для JavaScript, что делает XSS-атаки для кражи токена бесполезными. Однако они открывают дверь для другой классической угрозы — CSRF (Cross-Site Request Forgery / Межсайтная подделка запроса).
Суть проблемы в автоматизме: браузер прикрепляет куки к каждому запросу на сервер, который их выпустил, независимо от того, кто этот запрос инициировал.
Главный инструмент защиты: Флаг SameSite
Чтобы защититься от автоматической отправки кук со сторонних ресурсов, в современных браузерах был разработан атрибут SameSite. Он настраивается на сервере при выдаче куки и может принимать три значения:
SameSite=Strict(Максимальная защита)
- Как работает: Браузер никогда не отправит куку, если запрос идет со стороннего сайта.
- Проблема: Если друг пришлет вам ссылку на ваш личный кабинет в мессенджере, и вы кликните по ней, вы перейдете на сайт разлогиненным. Браузер заблокирует куку, так как переход совершен со стороннего сайта (из мессенджера). Придется обновлять страницу.
SameSite=Lax(Баланс безопасности и удобства — стандарт по умолчанию)
- Как работает: Браузер блокирует куки для скрытых запросов (через
fetch,axios, теги<img>или<iframe>). Но разрешает отправку кук при безопасной ручной навигации пользователя — когда он кликает по обычной ссылке<a href="...">. - Итог: Защищает от скрытых POST/PUT запросов хакера, но сохраняет пользователя залогиненным при переходе из поисковиков или соцсетей.
SameSite=None(Свободный доступ)
- Как работает: Куки отправляются всегда и везде.
- Важно: Требует обязательного наличия флага Secure (работа только по HTTPS). Используется редко, в основном для сторонних виджетов или iframe-интеграций.
Дополнительные методы защиты (для собеса)
Если интервьюер спросит: "А что делать, если старые браузеры пользователя не поддерживают SameSite?", назовите два классических механизма:
- CSRF-токены (Anti-CSRF Tokens): При загрузке страницы сервер генерирует случайный уникальный ключ и отдает его фронтенду (например, в скрытом HTML-теге или мета-теге). При отправке любого изменяющего запроса (POST/PUT/DELETE) фронтенд обязан прикрепить этот токен в кастомный HTTP-заголовок (например,
X-CSRF-Token). Хакер со своего сайта не может прочитать этот токен на вашем сайте, поэтому его поддельный запрос сервер отклонит. - Кастомные заголовки: Браузеры запрещают сторонним сайтам отправлять сложные запросы с нестандартными заголовками (вроде
X-Requested-With) без предварительной проверки CORS (Preflight request). Наличие такого обязательного заголовка на бэкенде автоматически ломает базовую CSRF-атаку.
Финал: Сравнение для шпаргалки
- JWT в localStorage ➡️ Уязвим для XSS (хакер может прочитать и украсть токен через JS)
- JWT в HttpOnly Cookie ➡️ Защищен от XSS, но уязвим для CSRF (хакер не может украсть токен, но может отправить запрос за вас) Защита — флаг
SameSite=Lax
Мы разобрали фундаментальную триаду веб-безопасности фронтенда (XSS, CSRF, Same-Origin Policy). Хотите перейти к теме архитектуры BFF (Backend-for-Frontend), которая сейчас считается лучшим решением для безопасности SPA (React/Vue/Angular), или разберем что-то другое?
Паттерн BFF (Backend-for-Frontend) — это современный золотой стандарт архитектуры безопасности для одностраничных приложений (SPA на React, Vue, Angular), особенно когда они работают в связке с микросервисами.
Главная идея BFF в контексте безопасности: полностью убрать безопасность и токены из клиентского JavaScript и перенести их на выделенный сервер-прослойку.
🛡️ 4 главных преимущества BFF для безопасности (Плюсы для собеседования)
- 🚫 С нулевым доступом для JS (Zero JS Token Access): JavaScript в браузере вообще «не знает» о существовании JWT-токенов. Хакер при XSS-атаке физически не может украсть токен доступа, потому что его нет на клиенте.
- 🛡️ Идеальная защита от CSRF: Поскольку BFF находится на том же домене, что и SPA (или на доверенном поддомене), куки между ними защищены флагами SameSite=Lax/Strict. Скрытые запросы с чужих сайтов блокируются браузером.
- 🔒 Скрытие архитектуры бэкенда: SPA общается только с одной точкой (BFF). Адреса внутренних микросервисов, структура базы данных и внутренние API-ключи полностью скрыты внутри безопасного контура сети.
- 🔀 Оптимизация данных: Помимо безопасности, BFF может агрегировать данные. Если для экрана профиля нужно вызвать 3 микросервиса, SPA делает 1 запрос к BFF, а BFF делает 3 быстрых запроса внутри сети и отдает фронтенду один чистый, готовый JSON.
⚠️ Обратная сторона BFF (О чем спросит Senior-интервьюер)
Если вы предложите BFF на собеседовании, вас обязательно спросят: "А в чем минусы? Почему все так не делают?". Будьте готовы назвать эти пункты:
- Усложнение инфраструктуры: Появляется еще один сервер, который нужно деплоить, мониторить, масштабировать и поддерживать.
- Stateful-нагрузка: BFF должен где-то хранить сессии или токены пользователей (например, в базе данных Redis). Это усложняет горизонтальное масштабирование сервера по сравнению с классическими Stateless JWT.
- Дополнительная задержка (Latency): Запрос проходит двойной путь: Браузер ➡️ BFF ➡️ Микросервис. Если сеть настроена плохо, это может замедлить работу приложения.
Итоговый чек-лист эволюции безопасности фронтенда:
- Local Storage (Плохо: уязвимо для XSS).
- HttpOnly Cookies (Лучше: защищено от XSS, но уязвимо для CSRF, сложно работать с микросервисами на разных доменах).
- BFF + In-Memory + Cookies (Отлично: абсолютный стандарт безопасности для крупных современных SPA).
3. Расскажи про Dependency Injection
❌ Пример без DI
Представьте класс автомобиля, которому для работы нужен движок. Без DI мы создаем двигатель прямо внутри конструктора машины:
class Engine {
start() { return "Врум-врум!"; }
}
class Car {
constructor() {
// ЖЕСТКАЯ СВЯЗЬ: Машина сама создает конкретный движок
this.engine = new Engine();
}
drive() {
console.log(this.engine.start());
}
}
const myCar = new Car();Анализ: Почему это плохо? (список проблем для собеса)
- Невозможно протестировать: Вы не можете изолированно протестировать класс
Car. Если классEngineделает реальные запросы в базу данных или сеть, ваши юнит-тестыCarтоже полезут в сеть. Вы не сможете подменитьEngineна фейковый объект (Mock). - Нулевая гибкость: Если завтра вам понадобится создать
ElectricEngine(электромотор), придется переписывать код внутри самого классаCar.
✅ Решение: Код с DI
Мы переписываем класс так, чтобы он принимал готовый двигатель как аргумент (через конструктор):
class Car {
// Зависимость ВНЕДРЯЕТСЯ извне
constructor(engine) {
this.engine = engine;
}
drive() {
console.log(this.engine.start());
}
}
// Теперь мы управляем созданием объектов снаружи:
const gasEngine = new Engine();
const petrolCar = new Car(gasEngine); // Внедряем бензиновый двигатель
const electricEngine = new ElectricEngine();
const tesla = new Car(electricEngine); // Легко внедрили электромотор!3 основных типа внедрения зависимостей
В зависимости от того, как именно объект получает данные, выделяют три способа:
- Constructor Injection (Через конструктор): Самый частый и правильный способ (пример выше). Зависимости передаются в момент создания объекта. Без них объект просто не создастся.
- Setter Injection (Через сеттер / метод): Зависимость передается через специальный метод после создания объекта. Полезно, если зависимость необязательна или может меняться на лету.
const car = new Car(); car.setEngine(v8Engine); // Внедрение через сеттер - Interface / Property Injection (Через свойства): Прямое присвоение в поле объекта. Используется редко, так как нарушает инкапсуляцию.
Что такое DI-контейнер (Inversion of Control Container)?
Вручную передавать объекты через конструкторы (new Car(new Engine(new Fuel(new Oil())))) на больших проектах превращается в «ад конструкторов». Чтобы автоматизировать этот процесс, используют DI-контейнеры.
DI-контейнер — это специальный инструмент (библиотека), который берет на себя всю рутину:
- Вы регистрируете в нем классы: «Вот класс Engine, а вот класс Car».
- Когда вам нужна машина, вы просите её у контейнера:
const car = container.get(Car). - Контейнер сам видит, что Car нужен Engine, автоматически создает
new Engine(), затем создаетnew Car(engine)и возвращает вам готовый собранный объект.
Плюсы и минусы DI для ответа на собеседовании
Главные преимущества (Плюсы):
- 🧪 Легкое тестирование: В тестах вместо реального сервиса платежей или БД можно за одну секунду подкинуть «заглушку» (Mock).
- 🧱 Слабая связанность (Loose Coupling): Классы не зависят от конкретных реализаций друг друга, они зависят только от интерфейсов (контрактов).
- 🔄 Переиспользование кода: Один и тот же сервис (например, Logger) можно легко внедрить в десятки разных классов.
Обратная сторона (Минусы):
- 📈 Повышение порога входа: Новичкам бывает трудно сходу понять, откуда берутся объекты, если в коде нет явных вызовов new.
- 🔍 Сложность отладки: Поиск того, какая именно реализация интерфейса сейчас внедрилась в код, может занять время в больших проектах.
4. Концепция иммутабельности
Иммутабельность (неизменяемость) — это концепция в программировании, при которой объект или переменная не могут быть изменены после своего создания. Вместо модификации существующего объекта всегда создается его новая копия с измененными данными.
В JavaScript по умолчанию большинство объектов (массивы, функции, объекты) являются мутабельными (изменяемыми).
🧱 Зачем это нужно?
- Предсказуемость: Код становится легче читать и тестировать. Функция не изменит внешние данные случайно.
- Отслеживание изменений: Легко понять, что данные изменились. Достаточно сравнить ссылки (
oldObj !== newObj), а не проверять каждое свойство внутри. - Безопасность в UI: Такие библиотеки, как React, используют иммутабельность для быстрого определения необходимости перерисовки (Рендера) компонентов.
- История состояний: Позволяет легко реализовать функции отмены и повтора действий (Undo/Redo).
У концепции иммутабельности есть несколько весомых минусов, из-за которых её не используют слепо во всех проектах.
📉 Основные недостатки
- Расход памяти: Каждый раз при изменении данных создается новая копия объекта. Если объект огромный (например, массив на 100 000 элементов), постоянное копирование быстро забивает оперативную память.
- Падение производительности: Копирование данных тратит такты процессора. Частая сборка мусора (Garbage Collection) для удаления старых, больше ненужных копий объектов может вызывать микрофризы в работе приложения.
- Сложнее синтаксис для вложенных структур: Если у вас глубокая вложенность объектов, код их обновления становится громоздким. Приходится «разворачивать» каждый уровень через spread-оператор (
{ ...obj, profile: { ...obj.profile, settings: { ... } } }). - Порог входа и риск ошибок: Разработчик должен постоянно помнить, какие методы массивов мутируют оригинал (push, sort, splice), а какие нет. Случайное использование не того метода ломает всю логику иммутабельности.
- Избыточность для простых задач: В небольших скриптах, утилитах или локальных циклах мутация переменных экономит время написания кода и работает быстрее.
🛠️ Как эти минусы компенсируют на практике?
- Структурный общий доступ (Structural Sharing): Специальные библиотеки (например, Immer или Immutable.js) не копируют объект целиком. Они создают новую ссылку только для изменившейся ветки дерева данных, а неизменившиеся части переиспользуют из старого объекта.
- Библиотека Immer: Она позволяет писать привычный «мутирующий» код внутри специальной функции-обертки, но автоматически превращает его в иммутабельный под капотом.
Immer
Библиотека Immer решает проблему громоздкого синтаксиса, позволяя вам писать обычный, привычный «мутирующий» код. При этом сам оригинал объекта остается в полной безопасности, а на выходе получается чистая иммутабельная копия. Вся магия происходит благодаря встроенному в JavaScript механизму Proxy, который перехватывает ваши изменения.
🔍 Проблема: Глубокая вложенность БЕЗ Immer
Представьте, что у нас есть профиль пользователя, и нам нужно изменить значение darkTheme на true.
const user = {
id: 1,
info: {
name: 'Алексей',
settings: {
theme: 'light',
notifications: true
}
}
};
// Чтобы сохранить иммутабельность, нужно "развернуть" каждый уровень:
const updatedUser = {
...user,
info: {
...user.info,
settings: {
...user.info.settings,
theme: 'dark' // Ради этой строчки пришлось писать всё выше
}
}
};Минусы: Код тяжело читать, легко забыть написать ... на каком-то уровне и случайно потерять часть данных.
✨ Решение: Как эту задачу решает Immer
С Immer вы используете функцию produce. Она принимает два аргумента: исходный объект и функцию-мутатор. Внутри этой функции вы работаете с так называемым draft (черновиком) так, будто это обычный мутабельный объект.
import { produce } from 'immer';
const user = {
id: 1,
info: {
name: 'Алексей',
settings: {
theme: 'light',
notifications: true
}
}
};
// Просто меняем свойство напрямую в черновике!
const updatedUser = produce(user, (draft) => {
draft.info.settings.theme = 'dark';
});
console.log(user.info.settings.theme); // 'light' (Оригинал не изменился!)
console.log(updatedUser.info.settings.theme); // 'dark' (Новый объект создан)💎 Почему это круто?
- Никаких мутаций: Исходный объект user гарантированно защищен.
- Знакомый синтаксис: Вы можете использовать любые привычные методы, включая мутирующие методы массивов (push, pop, splice, shift).
- Умная память (Structural Sharing): Если в объекте user были другие ветки (например, draft.orders), которые вы не трогали, Immer не будет их копировать. Новый объект updatedUser просто сошлется на старые участки памяти для неизмененных данных.
5. Поведенческое интервью (Behavioral Interview)
Для Senior-разработчика в продуктовых компаниях — это проверка вашей зрелости как инженера и командного игрока. Рекрутеры и тимлиды ищут маркеры токсичности, безответственности или неумения договариваться. Идеальный инструмент здесь — методология STAR (Situation, Task, Action, Result).
Главное правило
«Я не ищу виноватых, я решаю проблему бизнеса и делаю выводы на будущее».
Вопрос 1
«Расскажите про ваш самый крупный технический факап на продакшене. Что произошло и как вы это решили?»
Что на самом деле проверяют:
Умение брать на себя ответственность, стрессоустойчивость и способность выстраивать процессы, чтобы ошибка не повторилась. Не говорите, что у вас не было факапов — это звучит как ложь.
Идеальный ответ по STAR:
Вопрос 2
«Как вы поступаете, если дизайнер нарисовал красивый, но абсолютно нереализуемый или слишком дорогой в разработке интерфейс?»
Что на самом деле проверяют:
Умение находить компромиссы между UX/дизайном и инженерной стоимостью фичи, навыки коммуникации.
Вопрос 3
«У вас горит релиз, а бэкенд-команда задерживает API или выдает его в совершенно неудобном для фронтенда формате. Ваши действия?»
Что на самом деле проверяют:
Проактивность. Умение не сидеть сложа руки и не перекладывать вину на смежные команды.
Главный секрет прохождения Behavioral-секции:
Какую бы историю вы ни рассказывали, всегда держите в голове три правила:
- Говорите «МЫ», когда хвалите за успех, и говорите «Я», когда берете ответственность за действия в кризисе.
- Не проявляйте эмоций к прошлым коллегам («бэкендеры были некомпетентны»). Описывайте ситуацию как сухое бизнес-обстоятельство («у бэкенд-команды был высокий фокус на других задачах»).
- Всегда заканчивайте блок STAR фразой о том, какой системный вывод вы сделали, чтобы это не повторилось.
Пример из личного опыта
Situation
На одном из прошлых проектов мы столкнулись с жесткими инфраструктурными ограничениями со стороны заказчика. Организация полноценного деплоя и предоставление нам продакшен-подобных стендов (инстансов) затянулись почти на два года — мы дважды по полгода ждали готовности инфраструктуры. При этом проект представлял собой микрофронтенд-архитектуру, где наше приложение должно было работать внутри фреймов (iframe) хост-платформы.
Почему этот рассказ продаст вас как Senior+ / Lead:
- Использование MSW и Swagger: Показывает, что вы умеете готовить современный стек для автономной работы команды (отличный маркер DX — Developer Experience).
- Фокус на системных решениях: Вы не просто сидели и ждали деплоя, жалуясь на админов. Вы создали инструмент отладки до того, как всё упало. Это главный признак зрелого инженера.
- Спокойный тон: Описывая задержку в полгода, вы не критикуете компанию, а преподносите это как «инфраструктурные ограничения заказчика». Это показывает вашу корпоративную дипломатию.
Этот кейс идеально зайдет как ответ на вопросы:
- «Расскажите о проекте со сложной архитектурой или процессами».
- «Как вы организуете работу, если смежные команды сдвигают сроки?»
- «Расскажите о вашей технической гордости на проекте».
