Топ вопросов для JS-собеса

Навигация по странице

1. Циклические ссылки

Какие потенциальные проблемы сопутствуют возникновеню циклических ссылок?

Циклические ссылки в JavaScript возникают, когда два или более объекта ссылаются друг на друга напрямую или через цепочку свойств.

  1. 🔋 Утечки памяти (Memory Leaks)
  • Проблема: Старые браузеры (с алгоритмом подсчета ссылок) не могли удалить такие объекты.
  • Итог: Память засоряется, приложение начинает тормозить и в итоге аварийно завершает работу.
  • Нюанс: Современный алгоритм Mark-and-Sweep справляется с этим, если объекты недостижимы от корня (window/global). Но если хоть один объект в цикле «жив», вся цепочка останется в памяти навсегда.

Алгоритм Mark-and-Sweep (Отметь и сотри) — это...

  1. 💥 Ошибки сериализации в JSON
  • Проблема: Метод JSON.stringify() не умеет работать с циклическими структурами.
  • Итог: При попытке конвертировать такой объект в строку код упадет с критической ошибкой: TypeError: Converting circular structure to JSON.
  1. 🌀 Бесконечная рекурсия при обходе
  • Проблема: При попытке глубокого копирования (например, своей функцией), валидации или выводе объекта в лог алгоритм будет бесконечно ходить по кругу.
  • Итог: Переполнение стека вызовов (RangeError: Maximum call stack size exceeded) и падение приложения.
  1. 🕸️ Сложность поддержки и отладки
  • Проблема: Структура данных теряет прозрачность. При чтении логов в консоли трудно понять, где истинный конец данных.
  • Итог: Разработчикам сложно предсказать поведение системы, возрастает риск случайно изменить связанные данные в неожиданном месте.
  1. 🔒 Проблемы с паттернами иммутабельности
  • Проблема: В современных фреймворках (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-атаку для кражи токена.

Как защититься? (Чек-лист для собеседования)

Если вас спросят, как минимизировать риски XSS, назовите эти 4 уровня защиты:

  1. Никогда не хранить JWT в localStorage (использовать HttpOnly куки).
  2. Экранирование и санитизация (Sanitization): Всегда очищать данные от пользователей на бэкенде и фронтенде (использовать библиотеки вроде DOMPurify). Никогда не вставлять сырой HTML через dangerouslySetInnerHTML в React или v-html в Vue без строгой проверки.
  3. CSP (Content Security Policy): Настроить HTTP-заголовки безопасности, которые строго запрещают браузеру загружать скрипты со сторонних неизвестных доменов и выполнять встроенный (inline) JS-код.
  4. Аудит зависимостей: Регулярно запускать npm audit или использовать инструменты вроде Snyk для проверки уязвимостей в сторонних пакетах.

Почему куки защищают от XSS, но создают угрозу CSRF-атаки, и как с ней бороться?

Куки с флагом HttpOnly полностью закрывают доступ для JavaScript, что делает XSS-атаки для кражи токена бесполезными. Однако они открывают дверь для другой классической угрозы — CSRF (Cross-Site Request Forgery / Межсайтная подделка запроса).

Суть проблемы в автоматизме: браузер прикрепляет куки к каждому запросу на сервер, который их выпустил, независимо от того, кто этот запрос инициировал.

Сценарий CSRF-атаки: как это работает

Главный инструмент защиты: Флаг SameSite

Чтобы защититься от автоматической отправки кук со сторонних ресурсов, в современных браузерах был разработан атрибут SameSite. Он настраивается на сервере при выдаче куки и может принимать три значения:

  1. SameSite=Strict (Максимальная защита)
  • Как работает: Браузер никогда не отправит куку, если запрос идет со стороннего сайта.
  • Проблема: Если друг пришлет вам ссылку на ваш личный кабинет в мессенджере, и вы кликните по ней, вы перейдете на сайт разлогиненным. Браузер заблокирует куку, так как переход совершен со стороннего сайта (из мессенджера). Придется обновлять страницу.
  1. SameSite=Lax (Баланс безопасности и удобства — стандарт по умолчанию)
  • Как работает: Браузер блокирует куки для скрытых запросов (через fetch, axios, теги <img> или <iframe>). Но разрешает отправку кук при безопасной ручной навигации пользователя — когда он кликает по обычной ссылке <a href="...">.
  • Итог: Защищает от скрытых POST/PUT запросов хакера, но сохраняет пользователя залогиненным при переходе из поисковиков или соцсетей.
  1. 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 основных типа внедрения зависимостей

В зависимости от того, как именно объект получает данные, выделяют три способа:

  1. Constructor Injection (Через конструктор): Самый частый и правильный способ (пример выше). Зависимости передаются в момент создания объекта. Без них объект просто не создастся.
  2. Setter Injection (Через сеттер / метод): Зависимость передается через специальный метод после создания объекта. Полезно, если зависимость необязательна или может меняться на лету.
const car = new Car();
car.setEngine(v8Engine); // Внедрение через сеттер
  1. Interface / Property Injection (Через свойства): Прямое присвоение в поле объекта. Используется редко, так как нарушает инкапсуляцию.

Что такое DI-контейнер (Inversion of Control Container)?

Вручную передавать объекты через конструкторы (new Car(new Engine(new Fuel(new Oil())))) на больших проектах превращается в «ад конструкторов». Чтобы автоматизировать этот процесс, используют DI-контейнеры.

DI-контейнер — это специальный инструмент (библиотека), который берет на себя всю рутину:

  1. Вы регистрируете в нем классы: «Вот класс Engine, а вот класс Car».
  2. Когда вам нужна машина, вы просите её у контейнера: const car = container.get(Car).
  3. Контейнер сам видит, что Car нужен Engine, автоматически создает new Engine(), затем создает new Car(engine) и возвращает вам готовый собранный объект.

Плюсы и минусы DI для ответа на собеседовании

Главные преимущества (Плюсы):

  • 🧪 Легкое тестирование: В тестах вместо реального сервиса платежей или БД можно за одну секунду подкинуть «заглушку» (Mock).
  • 🧱 Слабая связанность (Loose Coupling): Классы не зависят от конкретных реализаций друг друга, они зависят только от интерфейсов (контрактов).
  • 🔄 Переиспользование кода: Один и тот же сервис (например, Logger) можно легко внедрить в десятки разных классов.

Обратная сторона (Минусы):

  • 📈 Повышение порога входа: Новичкам бывает трудно сходу понять, откуда берутся объекты, если в коде нет явных вызовов new.
  • 🔍 Сложность отладки: Поиск того, какая именно реализация интерфейса сейчас внедрилась в код, может занять время в больших проектах.

4. Концепция иммутабельности

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

В JavaScript по умолчанию большинство объектов (массивы, функции, объекты) являются мутабельными (изменяемыми).

🧱 Зачем это нужно?

  1. Предсказуемость: Код становится легче читать и тестировать. Функция не изменит внешние данные случайно.
  2. Отслеживание изменений: Легко понять, что данные изменились. Достаточно сравнить ссылки (oldObj !== newObj), а не проверять каждое свойство внутри.
  3. Безопасность в UI: Такие библиотеки, как React, используют иммутабельность для быстрого определения необходимости перерисовки (Рендера) компонентов.
  4. История состояний: Позволяет легко реализовать функции отмены и повтора действий (Undo/Redo).

У концепции иммутабельности есть несколько весомых минусов, из-за которых её не используют слепо во всех проектах.

📉 Основные недостатки

  1. Расход памяти: Каждый раз при изменении данных создается новая копия объекта. Если объект огромный (например, массив на 100 000 элементов), постоянное копирование быстро забивает оперативную память.
  2. Падение производительности: Копирование данных тратит такты процессора. Частая сборка мусора (Garbage Collection) для удаления старых, больше ненужных копий объектов может вызывать микрофризы в работе приложения.
  3. Сложнее синтаксис для вложенных структур: Если у вас глубокая вложенность объектов, код их обновления становится громоздким. Приходится «разворачивать» каждый уровень через spread-оператор ({ ...obj, profile: { ...obj.profile, settings: { ... } } }).
  4. Порог входа и риск ошибок: Разработчик должен постоянно помнить, какие методы массивов мутируют оригинал (push, sort, splice), а какие нет. Случайное использование не того метода ломает всю логику иммутабельности.
  5. Избыточность для простых задач: В небольших скриптах, утилитах или локальных циклах мутация переменных экономит время написания кода и работает быстрее.

🛠️ Как эти минусы компенсируют на практике?

  1. Структурный общий доступ (Structural Sharing): Специальные библиотеки (например, Immer или Immutable.js) не копируют объект целиком. Они создают новую ссылку только для изменившейся ветки дерева данных, а неизменившиеся части переиспользуют из старого объекта.
  2. Библиотека 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'  (Новый объект создан)

💎 Почему это круто?

  1. Никаких мутаций: Исходный объект user гарантированно защищен.
  2. Знакомый синтаксис: Вы можете использовать любые привычные методы, включая мутирующие методы массивов (push, pop, splice, shift).
  3. Умная память (Structural Sharing): Если в объекте user были другие ветки (например, draft.orders), которые вы не трогали, Immer не будет их копировать. Новый объект updatedUser просто сошлется на старые участки памяти для неизмененных данных.