Топ вопросов для 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 мы создаем двигатель прямо внутри конструктора машины:
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.
Решение: Код с Dependency Injection
Мы переписываем класс так, чтобы он принимал готовый двигатель как аргумент (через конструктор):
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 просто сошлется на старые участки памяти для неизмененных данных.