Топ вопросов для JS-собеса
1. Концепция иммутабельности
Иммутабельность (неизменяемость) — это концепция в программировании, при которой объект или переменная не могут быть изменены после своего создания. Вместо модификации существующего объекта всегда создается его новая копия с измененными данными.
Иммутабельность — это не просто случайное решение, а прямой ответ на хаос в управлении данными, который царил во фронтенде в 2013–2015 годах.
В то время стандартом был фреймворк AngularJS (Angular 1). В нём использовалось двустороннее связывание данных (Two-way data binding) и мутабельный подход. Изменение в интерфейсе автоматически меняло модель, а изменение в модели — интерфейс. В больших приложениях это приводило к «эффекту домино»: вы меняли модель в одном углу экрана, она неконтролируемо мутировала данные в другом, и приложение превращалось в нераспутываемый клубок багов.
Fb (Meta) создал React и архитектуру Flux как противовес этому хаосу. Они ввели однонаправленный поток данных (Unidirectional data flow): данные текут только сверху вниз, а наверх летят только экшены (события). Иммутабельность стала гарантией того, что данные никто не сможет случайно «тихо» мутировать по дороге.
Прямое сравнение: React vs Vue 3
Сравнение React и Vue 3 — это идеальный способ увидеть противостояние двух фундаментальных философий фронтенда: иммутабельности против реактивности на Proxy (мутабельности).
| Критерий | React | Vue 3 |
|---|---|---|
| Подход к данным | Иммутабельный. Данные только для чтения. Для изменений создаются новые копии (ссылки). | Мутабельный. Данные оборачиваются в Proxy. Можно менять свойства напрямую. |
| Как отслеживаются изменения | Явно. Вы сами вызываете setState. React сравнивает ссылки (old !== new). | Автоматически. Proxy перехватывает момент записи (set) в свойство объекта. |
| Область рендеринга | Сверху вниз. Перерисовывается компонент, вызвавший обновление, и все его дети (по умолчанию). | Точечно (Fine-grained). Перерисовывается только тот компонент, который использует изменившуюся переменную. |
| Оптимизация «из коробки» | Требует ручного контроля (React.memo, useMemo, useCallback). | Автоматическая. Фреймворк сам знает точные зависимости компонента. |
Зачем нужна иммутабельность?
Простой путь которым пошел React: Такие библиотеки, как React, используют иммутабельность для быстрого определения необходимости перерисовки (Рендера) компонентов.
У концепции иммутабельности есть несколько весомых минусов, из-за которых её не используют слепо во всех проектах.
Основные недостатки
- Расход памяти: Каждый раз при изменении данных создается новая копия объекта. Если объект огромный (например, массив на 100 000 элементов), постоянное копирование быстро забивает оперативную память.
- Падение производительности: Копирование данных тратит такты процессора. Частая сборка мусора (Garbage Collection) для удаления старых, больше ненужных копий объектов может вызывать микрофризы в работе приложения.
- Сложнее синтаксис для вложенных структур: Если у вас глубокая вложенность объектов, код их обновления становится громоздким. Приходится «разворачивать» каждый уровень через spread-оператор:
{ ...obj, profile: { ...obj.profile, settings: { ... } } } - Порог входа и риск ошибок: Разработчик должен постоянно помнить, какие методы массивов мутируют оригинал (push, sort, splice), а какие нет. Случайное использование не того метода ломает всю логику иммутабельности.
- Избыточность для простых задач: В небольших скриптах, утилитах или локальных циклах мутация переменных экономит время написания кода и работает быстрее.
🛠️ Как эти минусы компенсируют на практике?
- Структурный общий доступ (Structural Sharing): Специальные библиотеки (например, Immer или Immutable.js) не копируют объект целиком. Они создают новую ссылку только для изменившейся ветки дерева данных, а неизменившиеся части переиспользуют из старого объекта.
- Библиотека Immer: Она позволяет писать привычный «мутирующий» код внутри специальной функции-обертки, но автоматически превращает его в иммутабельный под капотом.
2. Циклические ссылки
Какие потенциальные проблемы сопутствуют возникновеню циклических ссылок?
Циклические ссылки в 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) запрещено мутировать состояние напрямую. Наличие циклов делает невозможным поверхностное копирование через спред (...), ломает сравнение ссылок для перерендера и требует сложного кэширования при глубоком обновлении.
3. 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).
4. Расскажи про 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.
- 🔍 Сложность отладки: Поиск того, какая именно реализация интерфейса сейчас внедрилась в код, может занять время в больших проектах.
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).
- Фокус на системных решениях: Вы не просто сидели и ждали деплоя, жалуясь на админов. Вы создали инструмент отладки до того, как всё упало. Это главный признак зрелого инженера.
- Спокойный тон: Описывая задержку в полгода, вы не критикуете компанию, а преподносите это как «инфраструктурные ограничения заказчика». Это показывает вашу корпоративную дипломатию.
Этот кейс идеально зайдет как ответ на вопросы:
- «Расскажите о проекте со сложной архитектурой или процессами».
- «Как вы организуете работу, если смежные команды сдвигают сроки?»
- «Расскажите о вашей технической гордости на проекте».
