Перейти к основному содержимому

ОЧЕНЬ ЖЕСТКОЕ РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ НА MIDDLE/SENIOR FRONTEND РАЗРАБОТЧИКА С ЗП 320К!

· 186 мин. чтения

Сегодня мы разберём собеседование на позицию Middle Plus / Senior Frontend-разработчика в крупную компанию с зарплатной вилкой 280–320 тыс. рублей на руки. Интервью охватило широкий спектр тем — от процессов релиза, обеспечения качества кода, рефакторинга и использования AI-инструментов до алгоритмов, структур данных, ООП, SOLID, архитектурных паттернов (MVC, MVVM, CQRS), протоколов HTTP, безопасности, микрофронтендов и задач на понимание асинхронности в JavaScript. Ккандидат продемонстрировал уверенное владение практическими аспектами фронтенд-разработки, а также способность рассуждать о сложных системных вопросах, хотя некоторые теоретические темы вызвали затруднения.

Вопрос 1. Из каких этапов состоит процесс релиза фронтенда?

Таймкод: 00:01:00

Ответ собеседника: Неполный. Описан процесс: создание релизной ветки, вливание задач из спринта, тестирование на девстенде и препродстенде, регрессионные тесты, деплой на прод с повторным тестированием. Однако ответ был прерван и не полностью раскрыт — далее перешли к деталям CI/CD пайплайна.

Правильный ответ:

Полный процесс релиза фронтенда включает следующие этапы:

1. Планирование релиза и формирование содержимого

На этом этапе команда определяет, какие задачи (features, bugfixes, hotfixes) войдут в релиз. Обычно это происходит в рамках спринта или релизного цикла. Формируется релизная ветка (release branch) от main/master или develop ветки в зависимости от выбранной стратегии ветвления (Git Flow, GitHub Flow, Trunk-Based Development).

2. Создание релизной ветки и мердж задач

Разработчики мержат свои feature-ветки в релизную ветку. Важно следить за тем, чтобы все задачи были протестированы на уровне unit-тестов и code review пройден. На этом этапе также может происходить разрешение конфликтов слияния.

3. Сборка и статический анализ (CI)

При пуше в релизную ветку запускается CI-пайплайн, который включает:

  • Установку зависимостей (npm install, yarn install)
  • Лinting (ESLint, Stylelint) для проверки качества кода
  • Компиляцию и сборку (Webpack, Vite, esbuild)
  • Запуск unit-тестов (Jest, Vitest)
  • Проверку типов (TypeScript)
  • Генерацию артефактов сборки (build artifacts)

4. Деплой на staging/dev-окружение

Собранные артефакты деплоятся на staging (или dev-стенд), где проводится:

  • Smoke-тестирование — базовая проверка работоспособности
  • Интеграционное тестирование — проверка взаимодействия с бэкендом, API
  • Ручное тестирование QA-инженерами
  • Проверка кроссбраузерности и адаптивности

5. Деплой на pre-production (препрод) окружение

Препрод — это максимально приближённое к продакшену окружение. Здесь выполняется:

  • Регрессионное тестирование — проверка, что новые изменения не сломали существующий функционал
  • Нагрузочное тестирование (при необходимости)
  • Тестирование безопасности
  • Приёмочное тестирование (UAT — User Acceptance Testing) от заказчика или продуктовой команды

6. Канареечный или поэтапный релиз (Canary/Progressive Rollout)

Вместо одновременного деплоя на всех пользователей часто применяют:

  • Canary release — выкатка на небольшой процент трафика (1-5%) с мониторингом метрик
  • Feature flags — постепенное включение новой функциональности для определённых групп пользователей
  • Blue-green deployment — переключение между двумя идентичными окружениями

7. Деплой на production (продакшен)

После успешного прохождения всех предыдущих этапов выполняется выкатка на продакшен. На этом важно:

  • Иметь план отката (rollback plan) на случай проблем
  • Использовать стратегии деплоя без downtime (rolling update, blue-green)
  • Убедиться, что CDN кеш инвалидирован

8. Мониторинг и пост-релизная проверка

После деплоя на продакшен:

  • Мониторинг ошибок (Sentry, Datadog, Grafana)
  • Отслеживание ключевых метрик (LCP, FID, CLS — Core Web Vitals)
  • Проверка корректности аналитики и трекинга
  • Мониторинг бизнес-метрик (конверсии, пользовательский опыт)
  • Пост-релизное тестирование (smoke-тесты на проде)

9. Коммуникация и документация

  • Уведомление команды и стейкхолдеров об успешном релизе
  • Обновление changelog
  • Документирование известных проблем
  • Проведение ретроспективы релиза (при необходимости)

10. Откат (Rollback)

В случае обнаружения критических проблем:

  • Быстрый откат на предыдущую версию
  • Анализ причин проблемы
  • Исправление и повторный релиз

Ключевой принцип — каждый этап должен быть автоматизирован настолько, насколько это возможно, чтобы минимизировать человеческий фактор и ускорить доставку ценности пользователям.

Вопрос 2. Какие этапы проходит код в CI/CD пайплайне при создании релиза?

Таймкод: 00:02:21

Ответ собеседника: Неполный. Упомянул пайплайн, выбор стенда для релиза, установку зависимостей через npm ci, проверку качества кода (линтеры, тесты), сборку через Vite/Webpack, упаковку в Docker-контейнер и деплой на стенд. Порядок этапов был немного путаным (сначала сказал сборку, потом поправился).

Правильный ответ:

Типичный CI/CD пайплайн для фронтенда при создании релиза проходит следующие этапы в строгом порядке:

1. Триггер и подготовка (Trigger & Checkout)

При нажатии кнопки «Create Release» или создании git tag происходит:

  • Клонирование репозитория на CI-раннер
  • Checkout нужной ветки или тега
  • Определение переменных окружения (target environment, version)

2. Установка зависимостей (Install)

# Используется npm ci вместо npm install для воспроизводимости
npm ci

# Или для yarn
yarn install --frozen-lockfile

npm ci предпочтительнее в CI, потому что:

  • Удаляет node_modules перед установкой
  • Строго следует package-lock.json
  • Быстрее для чистой установки
  • Падает при несоответствии lock-файла и package.json

3. Проверка качества кода (Quality Gates)

# Лinting
npm run lint

# Проверка типов TypeScript
npx tsc --noEmit

# Форматирование (опционально)
npm run format:check

Этот этап выполняется параллельно там, где возможно, для ускорения.

4. Запуск тестов (Test)

# Unit-тесты
npm run test:unit -- --coverage

# Интеграционные тесты (опционально)
npm run test:integration

# E2E-тесты (опционально, обычно на следующем этапе)
npm run test:e2e

Порог покрытия кода (code coverage) часто задаётся как quality gate — при падении ниже порога пайплайн останавливается.

5. Сборка (Build)

# Vite
npm run build
# Результат: dist/ директория

# Webpack
npx webpack --mode production
# Результат: build/ директория

На этом этапе происходит:

  • Транспиляция TypeScript в JavaScript
  • Минификация и оптимизация кода (tree-shaking)
  • Генерация source maps
  • Хеширование имён файлов для кеширования (app.a1b2c3.js)
  • Оптимизация изображений и ассетов

6. Анализ бандла (Bundle Analysis)

# Проверка размера бандла
npx bundlesize

# Или интегрированный анализ
npm run build -- --analyze

Предотвращает неконтролируемый рост размера бандла.

7. Упаковка в контейнер (Docker Build)

# Multi-stage build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# Сборка и пуш образа
docker build -t registry.company.com/frontend:${VERSION} .
docker push registry.company.com/frontend:${VERSION}

8. Деплой на staging (Deploy to Staging)

# Kubernetes
kubectl set image deployment/frontend \
frontend=registry.company.com/frontend:${VERSION} \
--namespace=staging

# Или через Helm
helm upgrade --install frontend ./chart \
--set image.tag=${VERSION} \
--namespace=staging

9. Smoke-тесты на staging (Post-Deploy Tests)

# Проверка доступности
curl -f https://staging.company.com/health || exit 1

# Автоматизированные smoke-тесты
npm run test:smoke -- --env=staging

10. Деплой на production (Deploy to Production)

После ручного подтверждения (manual approval) или автоматически при прохождении всех проверок:

# Canary deployment
kubectl set image deployment/frontend-canary \
frontend=registry.company.com/frontend:${VERSION} \
--namespace=production

# После мониторинг — полный rollout
kubectl set image deployment/frontend \
frontend=registry.company.com/frontend:${VERSION} \
--namespace=production

11. Инвалидация CDN и пост-релизные проверки

# CloudFront
aws cloudfront create-invalidation \
--distribution-id ${DISTRIBUTION_ID} \
--paths "/*"

# Проверка деплоя
npm run test:smoke -- --env=production

12. Уведомления (Notifications)

  • Отправка уведомления в Slack/Teams об успешном релизе
  • Обновление статуса в Jira/Linear
  • Тегирование коммита как released

Пример конфигурации GitLab CI:

stages:
- install
- quality
- test
- build
- package
- deploy-staging
- verify-staging
- deploy-production
- verify-production

install:
stage: install
script:
- npm ci
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/

lint:
stage: quality
script:
- npm run lint
- npx tsc --noEmit

unit-tests:
stage: test
script:
- npm run test:unit -- --coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'

build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour

docker-build:
stage: package
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG

deploy-staging:
stage: deploy-staging
script:
- helm upgrade --install frontend ./chart --set image.tag=$CI_COMMIT_TAG --namespace=staging
environment:
name: staging
only:
- tags

deploy-production:
stage: deploy-production
script:
- helm upgrade --install frontend ./chart --set image.tag=$CI_COMMIT_TAG --namespace=production
environment:
name: production
when: manual
only:
- tags

Ключевые принципы: fail-fast (остановка при первой ошибке), воспроизводимость (deterministic builds), артефакты между этапами, ручное подтверждение для прода.

Вопрос 3. Что управляет контейнерами на стендах и опыт работы с Kubernetes/Docker?

Таймкод: 00:03:36

Ответ собеседника: Неполный. Предположил Kubernetes как оркестратор контейнеров. С Kubernetes не работал, с Docker работал — настраивал конфиг и запускал контейнеры, но доступа к инфраструктуре у фронтенда не было.

Правильный ответ:

Оркестрация контейнеров в production

Для управления контейнерами на стендах используются оркестраторы:

Kubernetes (K8s) — самый распространённый оркестратор, который обеспечивает:

  • Автоматическое развёртывание и масштабирование контейнеров
  • Self-healing — перезапуск упавших контейнеров
  • Балансировку нагрузки
  • Rolling updates и rollback
  • Управление конфигурацией через ConfigMap и Secrets
  • Service Discovery

Альтернативы:

  • Docker Swarm — проще в настройке, но менее функционален
  • Nomad от HashiCorp — поддерживает не только контейнеры
  • AWS ECS/Fargate — облачные решения без управления серверами

Основные ресурсы Kubernetes:

# Deployment — управляет набором Pod'ов
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: registry.company.com/frontend:v1.2.3
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 3
---
# Service — обеспечивает доступ к Pod'ам
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 80
type: ClusterIP
---
# Ingress — внешний доступ
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
tls:
- hosts:
- app.company.com
secretName: frontend-tls
rules:
- host: app.company.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80

Работа с Docker — практический опыт

Типичные задачи с Docker для фронтенда:

# Оптимизированный Dockerfile для React/Vue приложения
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine AS production
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
CMD ["nginx", "-g", "daemon off;"]
# Основные команды Docker
docker build -t frontend:latest .
docker run -p 8080:80 frontend:latest
docker ps
docker logs -f container_id
docker exec -it container_id sh

Как фронтенд-разработчик может взаимодействовать с K8s:

Даже без прямого доступа к кластеру полезно понимать:

  • Как читать логи: kubectl logs -f deployment/frontend -n production
  • Как проверить статус: kubectl get pods -n production
  • Как описать проблему: kubectl describe pod frontend-xxx -n production
  • Как раскатить новую версию: kubectl set image deployment/frontend frontend=registry.company.com/frontend:v1.2.4 -n production
  • Как откатить: kubectl rollout undo deployment/frontend -n production

Helm Charts для управления релизами:

# values.yaml
image:
repository: registry.company.com/frontend
tag: "latest"
pullPolicy: IfNotPresent

replicaCount: 3

ingress:
enabled: true
host: app.company.com

resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
# Деплой через Helm
helm upgrade --install frontend ./helm-chart \
--set image.tag=v1.2.3 \
--namespace production \
--create-namespace

Хотя фронтенд-разботчики редко администрируют Kubernetes напрямую, понимание этих концепций помогает эффективнее взаимодействовать с DevOps командой и быстрее решать проблемы с деплоем.

Вопрос 4. Какие существуют способы обеспечения качества кода помимо линтеров?

Таймкод: 00:04:26

Ответ собеседника: Правильный. Назвал: написание тестов (юнит, интеграционные, e2e на Cypress/Playwright), стилгайды команды для единого стиля кода, SonarQube для автоматической проверки качества.

Правильный ответ:

Ответ собеседателя корректный и покрывает основные аспекты. Дополним для полноты картины.

1. Тестирование (Testing)

Многоуровневая стратегия тестирования:

  • Unit-тесты — проверка отдельных функций и компонентов
  • Интеграционные тесты — проверка взаимодействия компонентов
  • E2E-тесты — проверка пользовательских сценариев (Cypress, Playwright)
  • Visual regression тесты — проверка визуального соответствия (Percy, Chromatic)
  • Contract тесты — проверка API-контрактов (Pact)

2. Code Review

Систематический review кода перед мержем:

  • Проверка логики и архитектурных решений
  • Выявление потенциальных багов и edge cases
  • Знание команды с кодом (knowledge sharing)
  • Проверка соответствия стандартам проекта

3. Статический анализ кода

  • SonarQube/SonarCloud — комплексный анализ: баги, уязвимости, code smells, дублирование
  • Code Climate — оценка качества кода и сложности
  • Snyk Code — поиск уязвимостей в коде

4. Метрики покрытия кода (Code Coverage)

# Настройка порогов в jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};

5. Стандарты кодирования и стилгайды

  • ESLint + Prettier для автоматического форматирования
  • EditorConfig для единообразия настроек IDE
  • Conventional Commits для читаемой истории git
  • Документирование архитектурных решений (ADR — Architecture Decision Records)

6. Типизация

TypeScript как способ предотвращения ошибок:

// Строгая типизация в tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true
}
}

7. Автоматизированные проверки в CI/CD

  • Pre-commit и pre-push хуки (Husky + lint-staged)
  • Автоматические проверки в Pull Request
  • Блокировка мержа при падении проверок
// package.json — настройка lint-staged
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}

8. Мониторинг в production

  • Error tracking (Sentry, Bugsnag)
  • Профилирование производительности
  • Логирование и трейсинг
  • Метрики Core Web Vitals

Комбинация этих подходов создаёт многоуровневую систему обеспечения качества, которая ловит проблемы на разных этапах — от написания кода до работы в production.

Вопрос 5. Как поступать с техническим долгом после быстрого релиза?

Таймкод: 00:05:23

Ответ собеседника: Правильный. Необходимо создать задачу на рефакторинг/доработку MVP-фичи и положить её в бэклог, чтобы когда у команды появится время, кто-то взял её в работу.

Правильный ответ:

Ответ собеседника верный — фиксация технического долга в бэклоге является обязательным шагом. Рассмотрим подход более детально.

1. Фиксация технического долга

Сразу после релиза необходимо:

  • Создать задачу в трекере (Jira, Linear, YouTrack) с чётким описанием что нужно доработать
  • Указать приоритет и оценку сложности
  • Привязать к оригинальной фиче для трассируемости
  • Добавить тег tech-debt или refactoring

2. Приоритизация технического долга

Не весь технический долг равноценен:

КриITICAL (исправлять в ближайших спринтах):

  • Утечки памяти
  • Проблемы безопасности
  • Блокирующие баги в смежных фичах

HIGH (планировать в текущем квартале):

  • Код, который часто меняется и требует доработки
  • Отсутствие тестов на критичный функционал
  • Дублирование кода

MEDIUM (когда будет время):

  • Оптимизация производительности
  • Обновление зависимостей
  • Улучшение читаемости кода

LOW (nice to have):

  • Косметические улучшения
  • Рефакторинг стабильного кода

3. Стратегии работы с техническим долгом

Правило «мокрого пола» (Boy Scout Rule): > «Оставь код лучше, чем ты его нашёл»

При каждом касании кода — улучшай то, что рядом.

Выделение ёмкости (Capacity allocation):

  • 10-20% ёмкости спринта на технический долг
  • Фиксируется в процессе планирования спринта

Специальные спринты:

  • Раз в квартал «tech debt sprint»
  • Или «hackathon» для внутренних улучшений

4. Предотвращение накопления долга

// Пример: вместо быстрого хака
// ❌ Быстро, но неподдерживаемо
function processData(data: any) {
// TODO: добавить типизацию
// TODO: добавить обработку ошибок
// TODO: добавить тесты
return data.map((item: any) => item.value * 2);
}

// ✅ Правильный подход с первого раза
interface DataItem {
value: number;
}

function processData(data: DataItem[]): number[] {
if (!Array.isArray(data)) {
throw new Error('Data must be an array');
}
return data.map(item => item.value * 2);
}

5. Метрики технического долга

Отслеживайте:

  • Количество задач с тегом tech-debt в бэклоге
  • Соотношение tech-debt задач к feature задачам
  • Время, затраченное на исправление старых проблем
  • Code coverage по проекту
  • Количество багов, связанных с известным техническим долгом

6. Коммуникация с бизнесом

Важно уметь объяснить бизнесу ценность работы с техническим долгом:

  • «Без рефакторинга разработка новых фич замедлится на 30%»
  • «Этот долг блокирует реализацию запрошенной фичи»
  • «Исправление сейчас сэкономит X часов в будущем»

Технический долг — это нормальная часть разработки, но он должен быть осознанным, задокументированным и планомерно погашаемым.

Вопрос 6. По каким признакам можно определить некачественный код при код-ревью?

Таймкод: 00:06:41

Ответ собеседника: Правильный. Назвал: непонятные названия переменных, дублирование кода, большие компоненты на 500+ строк, вынос логики в хуки, излишняя мемоизация ухудшающая производительность.

Правильный ответ:

Ответ собеседника покрывает ключевые аспекты. Рассмотрим полный спектр code smells для фронтенда.

1. Проблемы с именованием

// ❌ Плохие имена
const d = new Date();
const data = fetchData();
const flag = true;
const arr = [];

// ✅ Хорошие имена
const currentDate = new Date();
const userProfile = fetchUserProfile();
const isUserAuthenticated = true;
const activeOrders = [];

2. Дублирование кода (DRY violation)

// ❌ Дублирование
function validateEmail(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function validateUserEmail(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

// ✅ Единая утилита
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

function isValidEmail(email: string): boolean {
return EMAIL_REGEX.test(email);
}

3. Большие компоненты и функции (God Component)

Признаки необходимости декомпозиции:

  • Компонент более 200-300 строк
  • Несколько ответственностей в одном компоненте
  • Множество useState/useEffect
  • Сложная вложенная логика в JSX
// ❌ Монолитный компонент
function UserDashboard() {
// 100+ строк состояния
// 50+ строк эффектов
// 200+ строк JSX с вложенными условиями
}

// ✅ Декомпозиция
function UserDashboard() {
return (
<DashboardLayout>
<UserProfile />
<OrderHistory />
<NotificationPanel />
</DashboardLayout>
);
}

4. Неправильное использование мемоизации

// ❌ Избыточная мемоизация
const value = useMemo(() => a + b, [a, b]); // Примитивная операция

const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // Не зависит от пропсов или состояния

// ✅ Оправданная мемоизация
const expensiveValue = useMemo(() => {
return heavyComputation(data);
}, [data]);

const sortedList = useMemo(() => {
return [...items].sort(complexComparator);
}, [items]);

5. Игнорирование ошибок

// ❌ Пустой catch
try {
await fetchData();
} catch (e) {
// ничего не делаем
}

// ✅ Обработка ошибок
try {
await fetchData();
} catch (error) {
logger.error('Failed to fetch data', { error });
showNotification('Не удалось загрузить данные');
}

6. Магические числа и строки

// ❌ Магические числа
if (status === 3) { ... }
setTimeout(callback, 300000);

// ✅ Именованные константы
const ORDER_STATUS_COMPLETED = 3;
const CACHE_TTL_MS = 5 * 60 * 1000;

if (status === ORDER_STATUS_COMPLETED) { ... }
setTimeout(callback, CACHE_TTL_MS);

7. Отсутствие типизации (any в TypeScript)

// ❌ Использование any
function processData(data: any) {
return data.map((item: any) => item.value);
}

// ✅ Строгая типизация
interface DataItem {
value: number;
}

function processData(data: DataItem[]): number[] {
return data.map(item => item.value);
}

8. Прямая мутация состояния

// ❌ Мутация
state.users.push(newUser);
state.user.name = 'New Name';

// ✅ Иммутабельность
setState(prev => ({
...prev,
users: [...prev.users, newUser]
}));

setState(prev => ({
...prev,
user: { ...prev.user, name: 'New Name' }
}));

9. Сложная вложенная логика в JSX

// ❌ Сложная вложенность
return (
<div>
{isLoading ? (
<Spinner />
) : error ? (
<Error message={error} />
) : data ? (
data.length > 0 ? (
data.map(item => (
item.isActive ? (
<Item key={item.id} data={item} />
) : null
))
) : (
<Empty />
)
) : null}
</div>
);

// ✅ Ранние возвраты и компоненты
if (isLoading) return <Spinner />;
if (error) return <Error message={error} />;
if (!data?.length) return <Empty />;

return (
<div>
{data.filter(item => item.isActive).map(item => (
<Item key={item.id} data={item} />
))}
</div>
);

10. Отсутствие обработки edge cases

  • Пустые массивы и null/undefined
  • Ошибки сети
  • Таймауты
  • Большие объёмы данных

11. Проблемы с производительностью

  • Отсутствие виртуализации длинных списков
  • Частые ре-рендеры из-за новых ссылок на объекты/массивы в пропсах
  • Отсутствие lazy loading для тяжёлых компонентов
  • Запросы в цикле без batching

12. Непоследовательность стиля

  • Смешение подходов (классовые и функциональные компоненты без причины)
  • Разный стиль именования (camelCase vs snake_case)
  • Непоследовательная структура файлов

Хорошее код-ревью — это баланс между качеством кода и скоростью разработки. Не стоит быть догматичным, но важные проблемы должны быть обязательно отмечены и исправлены.

Вопрос 7. Когда можно начинать рефакторинг и что значит «код должен быть подготовлен к рефакторингу»?

Таймкод: 00:07:59

Ответ собеседника: Неполный. Начинать рефакторинг можно при отсутствии более приоритетных бизнес-задач. На вопрос о подготовке к рефакторингу не смог внятно ответить, предположил что-то про гайды и декомпозицию задач. С подсказкой пришёл к мысли, что нужны тесты до рефакторинга, чтобы убедиться, что поведение не изменилось.

Правильный ответ:

Когда можно начинать рефакторинг

Рефакторинг можно начинать в следующих ситуациях:

Плановый рефакторинг:

  • При наличии свободной ёмкости в спринте (10-20% времени)
  • В рамках выделенных «tech debt» спринтов
  • При подготовке к масштабированию функциональности

Вынужденный рефакторинг:

  • Перед добавлением новой фичи, которая потребует изменения «грязного» кода
  • При исправлении багов, которые сложно чинить из-за плохой архитектуры
  • Когда скорость разработки заметно снизилась из-за технического долга

По требованию бизнеса:

  • При изменении требований, которые делают текущую реализацию неактуальной

Что значит «код подготовлен к рефакторингу»

Это критически важное понятие. Код считается подготовленным, когда:

1. Есть покрытие тестами (Safety Net)

// Тесты должны описывать текущее поведение
describe('UserRegistration', () => {
it('should validate email format', () => {
const result = validateEmail('invalid-email');
expect(result).toBe(false);
});

it('should register user with valid data', async () => {
const userData = { email: 'test@example.com', password: '123456' };
const result = await registerUser(userData);
expect(result.success).toBe(true);
});

it('should handle network errors gracefully', async () => {
mockNetworkError();
const result = await registerUser(validData);
expect(result.error).toBeDefined();
});
});

Без тестов рефакторинг — это не улучшение, а переписывание вслепую.

2. Понятно текущее поведение

  • Документация или комментарии описывают что код делает
  • Известны edge cases и как они обрабатываются
  • Понятны контракты с другими частями системы

3. Определены границы рефакторинга

  • Чётко понятно что именно будет изменено
  • Определено что останется нетронутым
  • Установлены критерии успешного завершения

4. Код изолирован от зависимостей

// ❌ Сложно рефакторить — сильная связанность
class OrderService {
constructor() {
this.db = new Database();
this.api = new ExternalAPI();
this.logger = new Logger();
}

async processOrder(order) {
// 100 строк, перемешивающих логику, запросы и валидацию
}
}

// ✅ Легко рефакторить — разделение ответственностей
class OrderValidator {
validate(order: Order): ValidationResult { ... }
}

class OrderRepository {
async save(order: Order): Promise<void> { ... }
async findById(id: string): Promise<Order> { ... }
}

class OrderProcessor {
constructor(
private validator: OrderValidator,
private repository: OrderRepository,
private notifier: OrderNotifier
) {}

async process(order: Order): Promise<Result> {
const validation = this.validator.validate(order);
if (!validation.isValid) return Result.failure(validation.errors);

await this.repository.save(order);
await this.notifier.notifyOrderCreated(order);
return Result.success();
}
}

Процесс безопасного рефакторинга:

Шаг 1: Убедиться в наличии тестов

npm run test -- --coverage
# Покрытие должно быть достаточным для изменяемого кода

Шаг 2: Запустить тесты — все должны проходить

npm test
# Зелёный статус перед началом

Шаг 3: Внести небольшие изменения

// Извлечение метода
// Было
function processOrder(order) {
// 20 строк валидации
// 30 строки сохранения
// 15 строк уведомления
}

// Стало
function processOrder(order) {
validateOrder(order);
saveOrder(order);
notifyOrderCreated(order);
}

Шаг 4: Запустить тесты после каждого изменения

npm test
# Убедиться что ничего не сломалось

Шаг 5: Зафиксировать изменения

git add .
git commit -m "refactor: extract validation logic from processOrder"

Чек-лист готовности к рефакторингу:

  • Тесты покрывают изменяемый функционал
  • Все тесты проходят до начала рефакторинга
  • Понятно текущее поведение кода
  • Ограничен scope рефакторинга
  • Есть план изменений
  • Определены критерии успеха
  • Код можно откатить если что-то пойдёт не так

Рефакторинг без тестов — это не рефакторинг, а переписывание. Тесты — это страховочная сетка, которая позволяет уверенно улучшать код.

Вопрос 8. Что такое рефакторинг и должно ли при рефакторинге меняться поведение для пользователя?

Таймкод: 00:09:12

Ответ собеседника: Правильный. Рефакторинг — это улучшение качества кода или переписывание легаци на новый стек для повышения поддерживаемости. Поведение для пользователя при рефакторинге меняться не должно.

Правильный ответ:

Ответ собеседника полностью корректен. Дадим развёрнутое определение с примерами.

Определение рефакторинга

Рефакторинг — это процесс изменения внутренней структуры кода без изменения его внешнего поведения. Цель — улучшить читаемость, поддерживаемость, производительность или архитектуру, сохранив функциональность нетронутой.

Ключевой принцип из книги Мартина Фаулера «Refactoring»: > «Рефакторинг — это контролируемый процесс улучшения кода, который не меняет его поведение.»

Что НЕ является рефакторингом:

  • Переписывание (Rewrite) — создание нового кода с нуля
  • Добавление функциональности — новые фичи
  • Исправление багов — изменение поведения для корректной работы
  • Оптимизация производительности — может менять поведение (кэширование, lazy loading)

Типичные примеры рефакторинга:

1. Извлечение метода (Extract Method)

// До рефакторинга
function printInvoice(invoice: Invoice) {
console.log(`Invoice #${invoice.id}`);
console.log(`Customer: ${invoice.customer.name}`);
console.log(`Date: ${invoice.date.toLocaleDateString()}`);

let total = 0;
for (const item of invoice.items) {
console.log(`${item.name}: $${item.price} x ${item.quantity}`);
total += item.price * item.quantity;
}

const tax = total * 0.2;
console.log(`Subtotal: $${total}`);
console.log(`Tax (20%): $${tax}`);
console.log(`Total: $${total + tax}`);
}

// После рефакторинга
function printInvoice(invoice: Invoice) {
printHeader(invoice);
const total = printItems(invoice.items);
printTotals(total);
}

function printHeader(invoice: Invoice) {
console.log(`Invoice #${invoice.id}`);
console.log(`Customer: ${invoice.customer.name}`);
console.log(`Date: ${invoice.date.toLocaleDateString()}`);
}

function printItems(items: LineItem[]): number {
let total = 0;
for (const item of items) {
console.log(`${item.name}: $${item.price} x ${item.quantity}`);
total += item.price * item.quantity;
}
return total;
}

function printTotals(subtotal: number) {
const tax = subtotal * 0.2;
console.log(`Subtotal: $${subtotal}`);
console.log(`Tax (20%): $${tax}`);
console.log(`Total: $${subtotal + tax}`);
}

2. Переименование переменных и функций

// До
function calc(d: number, r: number): number {
return d * r;
}

// После
function calculateDiscount(originalPrice: number, discountRate: number): number {
return originalPrice * discountRate;
}

3. Устранение дублирования

// До
function getUserDisplayName(user: User) {
return user.firstName + ' ' + user.lastName;
}

function getAdminDisplayName(admin: User) {
return admin.firstName + ' ' + admin.lastName;
}

// После
function getDisplayName(user: User) {
return `${user.firstName} ${user.lastName}`;
}

4. Упрощение условий

// До
function canAccess(user: User, resource: Resource): boolean {
if (user.role === 'admin') {
return true;
} else if (user.role === 'editor' && resource.type === 'draft') {
return true;
} else if (user.id === resource.ownerId) {
return true;
} else {
return false;
}
}

// После
function canAccess(user: User, resource: Resource): boolean {
if (user.role === 'admin') return true;
if (user.role === 'editor' && resource.type === 'draft') return true;
if (user.id === resource.ownerId) return true;
return false;
}

5. Замена магических чисел константами

// До
if (user.loginAttempts > 3) {
lockAccount(user);
}

// После
const MAX_LOGIN_ATTEMPTS = 3;

if (user.loginAttempts > MAX_LOGIN_ATTEMPTS) {
lockAccount(user);
}

Когда поведение МОЖЕТ меняться:

Иногда граница между рефакторингом и исправлением размыта. Поведение может меняться если:

  • Исправляется баг, который считался «фичей»
  • Уточняются требования — оказывается, текущее поведение некорректно
  • Изменяются внешние API или контракты

Но в этих случаях это уже не чистый рефакторинг, а рефакторинг + исправление.

Как убедиться что поведение не изменилось:

  1. Запустить тесты до рефакторинга — все должны проходить
  2. Вносить маленькие изменения
  3. Запускать тесты после каждого изменения
  4. Если тест упал — либо откатить изменение, либо обновить тест если поведение действительно должно измениться

Рефакторинг — это навык, который требует практики. Хороший рефакторинг делает код понятнее для человека, не меняя то, что он делает для компьютера.

Вопрос 9. Какие AI-инструменты используете в работе и в каком режиме?

Таймкод: 00:10:33

Ответ собеседника: Неполный. Использует ChatGPT и DeepSeek для делегирования рутинных задач (написание тестов, утилит). Работает в режиме чата, создаёт папки для проектов. Агентский режим (Cursor) не использовал активно, пробовал, но для текущего проекта он не подошёл из-за специфики.

Правильный ответ:

Ответ собеседника показывает базовое знакомство с AI-инструментами. Рассмотрим полный спектр современных AI-инструментов для разработки.

Категории AI-инструментов:

1. AI-ассистенты для кода (Chat-based)

  • ChatGPT (GPT-4) — универсальный помощник для объяснения кода, генерации примеров, обсуждения архитектурных решений
  • Claude — хорош для работы с большими контекстами, анализа кода
  • DeepSeek — бесплатная альтернатива для базовых задач

Типичные сценарии использования:

  • Генерация boilerplate кода
  • Написание unit-тестов
  • Объяснение незнакомого кода
  • Рефакторинг отдельных функций
  • Написание документации

2. AI-ассистенты в IDE (Inline)

  • GitHub Copilot — автодополнение кода прямо в редакторе
  • Amazon CodeWhisperer — аналог Copilot от AWS
  • Tabnine — локальные модели для приватности
// Пример: Copilot предлагает реализацию по сигнатуре
function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>;

return (...args: Parameters<T>) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}

3. AI-агенты (Agent-based)

  • Cursor — IDE с встроенным AI-агентом, который может:

    • Читать и изменять несколько файлов
    • Выполнять команды в терминале
    • Искать по кодовой базе
    • Понимать контекст проекта
  • GitHub Copilot Workspace — агент для работы с PR и задачами

  • Devin, SWE-agent — автономные агенты для сложных задач

  • Windsurf — альтернатива Cursor

4. AI для специфических задач

  • v0.dev (Vercel) — генерация UI-компонентов по описанию
  • Bolt.new — создание полноценных приложений по промпту
  • Perplexity — поиск по документации и решение проблем

Эффективные паттерны использования:

Паттерн 1: Делегирование рутины

Промпт: "Напиши unit-тесты для функции validateEmail с покрытием
edge cases: пустая строка, без @, без домена, спецсимволы"

Паттерн 2: Пара «программист + AI»

Разработчик пишет скелет → AI заполняет детали →
Разработчик проверяет и корректирует

Паттерн 3: Обучение через AI

"Объясни мне паттерн Observer на TypeScript с примером
из реального фронтенд-проекта"

Паттерн 4: Ревью с AI

"Проверь этот код на потенциальные баги, проблемы
производительности и нарушения принципов SOLID"

Ограничения и подводные камни:

  • AI может генерировать неоптимальный или устаревший код
  • Необходимо проверять безопасность сгенерированного кода
  • Зависимость от AI может мешать развитию навыков
  • Конфиденциальный код не должен отправляться в публичные AI-сервисы

Рекомендации по использованию:

  • Используйте AI для ускорения, а не замены мышления
  • Всегда проверяйте и тестируйте сгенерированный код
  • Учитесь формулировать точные промпты
  • Настройте правила использования AI в команде
  • Используйте корпоративные решения для приватности кода

AI-инструменты — это мощный усилитель продуктивности, но они не заменяют понимание кода и инженерное мышление.

Вопрос 10. Как выглядит процесс использования AI для написания тестов?

Таймкод: 00:12:28

Ответ собеседника: Правильный. Скидывает примеры из других проектов/файлов и просит написать юнит-тесты по аналогии. Признаёт, что импорты и часть тест-кейсов будут неправильными, но это даёт идею, что именно нужно протестировать. Дальше дорабатывает логику самостоятельно.

Правильный ответ:

Ответ собеседника демонстрирует зрелый подход к использованию AI. Рассмотрим процесс более детально.

Эффективный процесс написания тестов с AI:

Шаг 1: Подготовка контекста

Предоставьте AI максимум релевантной информации:

Контекст:
- Функция: validateUserRegistration
- Файл: src/utils/validation.ts
- Зависимости: zod для валидации
- Примеры тестов из проекта: [вставить пример]

Функция для тестирования:
interface RegistrationData {
email: string;
password: string;
confirmPassword: string;
age: number;
}

interface ValidationResult {
isValid: boolean;
errors: string[];
}

function validateUserRegistration(data: RegistrationData): ValidationResult {
const errors: string[] = [];

if (!data.email || !isValidEmail(data.email)) {
errors.push('Invalid email format');
}

if (!data.password || data.password.length < 8) {
errors.push('Password must be at least 8 characters');
}

if (data.password !== data.confirmPassword) {
errors.push('Passwords do not match');
}

if (data.age < 18) {
errors.push('Must be at least 18 years old');
}

return {
isValid: errors.length === 0,
errors
};
}

Шаг 2: Формулировка запроса

Напиши unit-тесты для функции validateUserRegistration.
Используй Jest и @testing-library.

Включи тест-кейсы для:
- Валидных данных
- Невалидного email
- Короткого пароля
- Несовпадающих паролей
- Возраста меньше 18
- Пустых полей
- Граничных значений (ровно 18 лет, ровно 8 символов в пароле)

Шаг 3: Анализ результата AI

AI может сгенерировать примерно такой код:

import { validateUserRegistration } from './validation';

describe('validateUserRegistration', () => {
const validData = {
email: 'test@example.com',
password: 'password123',
confirmPassword: 'password123',
age: 25
};

it('should return valid for correct data', () => {
const result = validateUserRegistration(validData);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});

it('should return invalid for incorrect email', () => {
const result = validateUserRegistration({
...validData,
email: 'invalid-email'
});
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Invalid email format');
});

it('should return invalid for short password', () => {
const result = validateUserRegistration({
...validData,
password: 'short',
confirmPassword: 'short'
});
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must be at least 8 characters');
});

it('should return invalid for mismatched passwords', () => {
const result = validateUserRegistration({
...validData,
confirmPassword: 'different123'
});
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Passwords do not match');
});

it('should return invalid for age under 18', () => {
const result = validateUserRegistration({
...validData,
age: 17
});
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Must be at least 18 years old');
});

it('should handle empty fields', () => {
const result = validateUserRegistration({
email: '',
password: '',
confirmPassword: '',
age: 0
});
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});

it('should accept minimum valid age (boundary)', () => {
const result = validateUserRegistration({
...validData,
age: 18
});
expect(result.isValid).toBe(true);
});

it('should accept minimum password length (boundary)', () => {
const result = validateUserRegistration({
...validData,
password: '12345678',
confirmPassword: '12345678'
});
expect(result.isValid).toBe(true);
});
});

Шаг 4: Корректировка и дополнение

Проверьте и исправьте:

  • Импорты и пути
  • Моки зависимостей
  • Пропущенные edge cases
  • Названия тестов (должны описывать поведение)

Продвинутые техники:

Паттерн 1: Итеративное улучшение

"Добавь тесты для случая когда email содержит спецсимволы"
"Добавь тест на максимальную длину пароля"

Паттерн 2: Тестирование компонентов

"Напиши тесты для React-компонента UserForm.
Используй @testing-library/react.
Проверь: рендеринг, валидацию, отправку формы, отображение ошибок."

Паттерн 3: Генерация тестовых данных

"Сгенерируй фабрику тестовых данных для User объекта
с использованием faker.js"
import { faker } from '@faker-js/faker';

function createTestUser(overrides: Partial<User> = {}): User {
return {
id: faker.string.uuid(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
age: faker.number.int({ min: 18, max: 99 }),
createdAt: faker.date.past(),
...overrides
};
}

// Использование
const testUser = createTestUser({ email: 'specific@test.com' });

Чего ожидать от AI и что проверять:

AI хорошо справляется со стандартными тест-кейсами, но может пропустить:

  • Специфичные для проекта правила валидации
  • Интеграционные сценарии
  • Проблемы с асинхронным кодом
  • Мокирование сложных зависимостей

Используйте AI как отправную точку, а не финальный результат. Ваша экспертиза в понимании бизнес-логики и edge cases остаётся незаменимой.

Вопрос 11. Что такое бинарное дерево и зачем оно нужно?

Таймкод: 00:13:36

Ответ собеседника: Неправильный. Не смог вспомнить определение бинарного дерева. Знал только, что это структура для поиска и что «би» означает два. Бинарное дерево на практике не использовал. Вспомнил только алгоритм бинарного поиска на уровне теории.

Правильный ответ:

Определение бинарного дерева

Бинарное дерево — это иерархическая структура данных, в которой каждый узел имеет не более двух потомков (детей). Потомки называются левым и правым.

10 ← корень (root)
/ \
5 15 ← внутренние узлы
/ \ / \
3 7 12 20 ← листья (leaves)

Основные термины:

  • Корень (Root) — верхний узел дерева, не имеет родителя
  • Лист (Leaf) — узел без потомков
  • Родитель (Parent) — узел, имеющий потомков
  • Потомок (Child) — узел, связанный с родителем
  • Глубина (Depth) — расстояние от корня до узла
  • Высота (Height) — максимальная глубина в дереве

Реализация на TypeScript:

class TreeNode<T> {
value: T;
left: TreeNode<T> | null = null;
right: TreeNode<T> | null = null;

constructor(value: T) {
this.value = value;
}
}

class BinaryTree<T> {
root: TreeNode<T> | null = null;

// Вставка (простая, без балансировки)
insert(value: T): void {
const newNode = new TreeNode(value);

if (!this.root) {
this.root = newNode;
return;
}

const queue: TreeNode<T>[] = [this.root];

while (queue.length > 0) {
const current = queue.shift()!;

if (!current.left) {
current.left = newNode;
return;
} else {
queue.push(current.left);
}

if (!current.right) {
current.right = newNode;
return;
} else {
queue.push(current.right);
}
}
}
}

Виды бинарных деревьев:

1. Бинарное дерево поиска (BST)

Для каждого узла:

  • Все значения в левом поддереве меньше значения узла
  • Все значения в правом поддереве больше значения узла
class BinarySearchTree<T> {
root: TreeNode<T> | null = null;

insert(value: T): void {
this.root = this.insertNode(this.root, value);
}

private insertNode(node: TreeNode<T> | null, value: T): TreeNode<T> {
if (!node) {
return new TreeNode(value);
}

if (value < node.value) {
node.left = this.insertNode(node.left, value);
} else if (value > node.value) {
right = this.insertNode(node.right, value);
}

return node;
}

search(value: T): boolean {
return this.searchNode(this.root, value);
}

private searchNode(node: TreeNode<T> | null, value: T): boolean {
if (!node) return false;

if (value === node.value) return true;

if (value < node.value) {
return this.searchNode(node.left, value);
}
return this.searchNode(node.right, value);
}
}

Сложность операций в BST:

  • Лучший/средний случай: O(log n)
  • Худший случай (вырожденное дерево): O(n)

2. Сбалансированные деревья

Для гарантии O(log n) используются самобалансирующиеся деревья:

  • AVL-дерево — разница высот поддеревьев не более 1
  • Красно-чёрное дерево — используется в TreeMap, TreeSet
  • B-дерево — для баз данных и файловых систем

Обходы дерева (Tree Traversals):

class BinaryTree<T> {
// Прямой обход (Preorder): Корень → Левый → Правый
preorder(node: TreeNode<T> | null = this.root, result: T[] = []): T[] {
if (!node) return result;

result.push(node.value); // Посещаем корень
this.preorder(node.left, result); // Левое поддерево
this.preorder(node.right, result); // Правое поддерево

return result;
}

// Симметричный обход (Inorder): Левый → Корень → Правый
// Для BST даёт отсортированную последовательность!
inorder(node: TreeNode<T> | null = this.root, result: T[] = []): T[] {
if (!node) return result;

this.inorder(node.left, result); // Левое поддерево
result.push(node.value); // Посещаем корень
this.inorder(node.right, result); // Правое поддерево

return result;
}

// Обратный обход (Postorder): Левый → Правый → Корень
postorder(node: TreeNode<T> | null = this.root, result: T[] = []): T[] {
if (!node) return result;

this.postorder(node.left, result); // Левое поддерево
this.postorder(node.right, result); // Правое поддерево
result.push(node.value); // Посещаем корень

return result;
}

// Обход в ширину (BFS): по уровням
levelOrder(): T[] {
if (!this.root) return [];

const result: T[] = [];
const queue: TreeNode<T>[] = [this.root];

while (queue.length > 0) {
const node = queue.shift()!;
result.push(node.value);

if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}

return result;
}
}

Практическое применение:

1. Индексы в базах данных

-- B-tree индекс для быстрого поиска
CREATE INDEX idx_users_email ON users(email);

2. Дерево компонентов в React/Virtual DOM

// React использует дерево для представления UI
// Diff algorithm сравнивает деревья для эффективного обновления

3. Парсеры и компиляторы

Выражение: (2 + 3) * 4

*
/ \
+ 4
/ \
2 3

4. Сжатие данных (Код Хаффмана)

Частоты: a:45, b:13, c:12, d:16, e:9, f:5

100
/ \
a:45 55
/ \
25 30
/ \ / \
b:13 c:12 d:16
/ \
f:5 e:9

5. Файловые системы

/
├── home/
│ ├── user/
│ │ ├── documents/
│ │ └── downloads/
│ └── admin/
├── etc/
│ ├── nginx/
│ └── systemd/
└── var/
└── log/

6. Автодополнение (Trie)

class TrieNode {
children: Map<string, TrieNode> = new Map();
isEndOfWord: boolean = false;
}

class Trie {
root = new TrieNode();

insert(word: string): void {
let current = this.root;
for (const char of word) {
if (!current.children.has(char)) {
current.children.set(char, new TrieNode());
}
current = current.children.get(char)!;
}
current.isEndOfWord = true;
}

search(prefix: string): string[] {
let current = this.root;
for (const char of prefix) {
if (!current.children.has(char)) return [];
current = current.children.get(char)!;
}
return this.collectWords(current, prefix);
}

private collectWords(node: TrieNode, prefix: string): string[] {
const words: string[] = [];
if (node.isEndOfWord) words.push(prefix);

for (const [char, child] of node.children) {
words.push(...this.collectWords(child, prefix + char));
}

return words;
}
}

Когда использовать бинарные деревья:

  • Нужен быстрый поиск (O(log n) в сбалансированном дереве)
  • Данные имеют иерархическую структуру
  • Нужен отсортированный обход данных
  • Реализация приоритетных очередей (heap)

Когда НЕ использовать:

  • Данные не имеют естественного порядка — лучше хеш-таблица
  • Нужен произвольный доступ по индексу — лучше массив
  • Дерево может стать несбалансированным без возможности ребалансировки

Бинарные деревья — фундаментальная структура данных, понимание которой важно для алгоритмического мышления и проектирования эффективных систем.

Вопрос 12. Как работает алгоритм бинарного поиска и какое требование к данным?

Таймкод: 00:14:30

Ответ собеседника: Неполный. Разбивает массив на части, ищет сначала в одной половине, потом в другой. На вопрос об упорядоченности массива сначала ответил «неважно», затем поправился, что массив должен быть отсортирован. Механизм работы описал поверхностно.

Правильный ответ:

Требование к данным

Массив обязательно должен быть отсортирован. Это критическое требование — без него алгоритм не работает. Бинарный поиск использует тот факт, что в отсортированном массиве можно определить, в какой половине находится искомый элемент, сравнив его со средним элементом.

Принцип работы

Алгоритм использует стратегию «разделяй и властвуй» (divide and conquer):

  1. Определяем границы поиска: left и right
  2. Находим средний элемент: mid = (left + right) / 2
  3. Сравниваем искомое значение со средним элементом
  4. Если равны — нашли элемент
  5. Если искомое меньше — ищем в левой половине
  6. Если искомое больше — ищем в правой половине
  7. Повторяем пока не найдём или границы не сомкнутся

Визуализация:

Ищем 23 в массиве [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]

Шаг 1: left=0, right=9, mid=4
[2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
^ ^ ^
left mid right
16 < 23 → ищем в правой половине

Шаг 2: left=5, right=9, mid=7
[2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
^ ^ ^
left mid right
56 > 23 → ищем в левой половине

Шаг 3: left=5, right=6, mid=5
[2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
^ ^
left/right
mid
23 == 23 → нашли! Индекс = 5

Реализация на TypeScript:

// Итеративная реализация (предпочтительна)
function binarySearch<T>(arr: T[], target: T): number {
let left = 0;
let right = arr.length - 1;

while (left <= right) {
// Используем такую формулу для избежания переполнения
const mid = left + Math.floor((right - left) / 2);
const midValue = arr[mid];

if (midValue === target) {
return mid; // Нашли элемент
} else if (midValue < target) {
left = mid + 1; // Ищем в правой половине
} else {
right = mid - 1; // Ищем в левой половине
}
}

return -1; // Элемент не найден
}

// Рекурсивная реализация
function binarySearchRecursive<T>(
arr: T[],
target: T,
left = 0,
right = arr.length - 1
): number {
if (left > right) {
return -1; // Базовый случай: элемент не найден
}

const mid = left + Math.floor((right - left) / 2);
const midValue = arr[mid];

if (midValue === target) {
return mid;
} else if (midValue < target) {
return binarySearchRecursive(arr, target, mid + 1, right);
} else {
return binarySearchRecursive(arr, target, left, mid - 1);
}
}

Варианты бинарного поиска:

Поиск первого вхождения (для дубликатов):

function findFirstOccurrence<T>(arr: T[], target: T): number {
let left = 0;
let right = arr.length - 1;
let result = -1;

while (left <= right) {
const mid = left + Math.floor((right - left) / 2);

if (arr[mid] === target) {
result = mid; // Запоминаем позицию
right = mid - 1; // Но продолжаем искать левее
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}

return result;
}

// Пример
findFirstOccurrence([1, 2, 2, 2, 3, 4, 5], 2); // Возвращает 1

Поиск последнего вхождения:

function findLastOccurrence<T>(arr: T[], target: T): number {
let left = 0;
let right = arr.length - 1;
let result = -1;

while (left <= right) {
const mid = left + Math.floor((right - left) / 2);

if (arr[mid] === target) {
result = mid; // Запоминаем позицию
left = mid + 1; // Но продолжаем искать правее
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}

return result;
}

// Пример
findLastOccurrence([1, 2, 2, 2, 3, 4, 5], 2); // Возвращает 3

Поиск позиции для вставки (lower bound):

function lowerBound<T>(arr: T[], target: T): number {
let left = 0;
let right = arr.length; // Внимание: не length - 1

while (left < right) {
const mid = left + Math.floor((right - left) / 2);

if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}

return left; // Позиция, куда можно вставить target
}

// Примеры
lowerBound([1, 3, 5, 7], 4); // Возвращает 2 (вставить между 3 и 5)
lowerBound([1, 3, 5, 7], 5); // Возвращает 2 (уже есть элемент 5)
lowerBound([1, 3, 5, 7], 0); // Возвращает 0 (вставить в начало)

Сложность алгоритма:

МетрикаЗначение
Время (лучший случай)O(1) — элемент посередине
Время (средний случай)O(log n)
Время (худший случай)O(log n)
Память (итеративный)O(1)
Память (рекурсивный)O(log n) — стек вызовов

Почему O(log n)?

n = 1000
После 1 шага: n/2 = 500
После 2 шагов: n/4 = 250
После 3 шагов: n/8 = 125
...
После k шагов: n/2^k = 1

n/2^k = 1 → 2^k = n → k = log₂(n)

Для n = 1,000,000: всего ~20 шагов!

Сравнение с линейным поиском:

Размер массиваЛинейный поискБинарный поиск
1001007
10,00010,00014
1,000,0001,000,00020
1,000,000,0001,000,000,00030

Практическое применение:

// Встроенные методы в стандартных библиотеках

// JavaScript - нет встроенного binary search, но можно использовать:
const sorted = [1, 3, 5, 7, 9];
const index = sorted.findIndex(x => x >= 5); // Линейный поиск!

// Используйте библиотеки:
// npm install binary-search
import bs from 'binary-search';
bs(sorted, 5); // 2

// Python
// bisect.bisect_left(sorted_list, target)

// Java
// Arrays.binarySearch(arr, target)

// C++
// std::lower_bound(begin, end, value)

Типичные ошибки:

// ❌ Ошибка: переполнение при вычислении mid
const mid = (left + right) / 2; // Может переполниться для больших чисел

// ✅ Правильно:
const mid = left + Math.floor((right - left) / 2);

// ❌ Ошибка: неправильное условие цикла
while (left < right) // Пропустим последний элемент

// ✅ Правильно:
while (left <= right)

// ❌ Ошибка: неверное обновление границ
left = mid; // Может зациклиться
right = mid;

// ✅ Правильно:
left = mid + 1;
right = mid - 1;

Бинарный поиск — один из самых эффективных алгоритмов поиска и часто встречается на технических собеседованиях. Важно не только знать реализацию, но и понимать варианты и edge cases.

Вопрос 13. Что такое хэш-таблица, зачем она нужна и как применить её для идентификации объектов без ID?

Таймкод: 00:15:24

Ответ собеседника: Правильный. Хэш-таблица — структура данных для хранения значений с доступом за O(1), аналог объекта в JS. Позволяет быстро получать доступ по ключу. В задаче с объектами без ID — можно использовать значение как ключ хэш-таблицы, чтобы за O(1) получить нужный объект.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим тему более глубоко.

Определение хэш-таблицы

Хэш-таблица (Hash Map) — это структура данных, которая хранит пары ключ-значение и обеспечивает:

  • Вставку за O(1) в среднем
  • Поиск за O(1) в среднем
  • Удаление за O(1) в среднем

Как работает хэш-таблица внутри:

Ключ → Хэш-функция → Индекс в массиве → Значение

"email@example.com" → hash() → 42 → { name: "John", age: 25 }
  1. Ключ проходит через хэш-функцию
  2. Хэш-функция возвращает число (хэш)
  3. Хэш преобразуется в индекс массива: index = hash % array.length
  4. Значение хранится по этому индексу

Реализация простой хэш-таблицы:

class HashTable<K, V> {
private buckets: Array<Array<[K, V]>>;
private size: number;

constructor(initialCapacity = 16) {
this.size = initialCapacity;
this.buckets = new Array(initialCapacity).fill(null).map(() => []);
}

private hash(key: K): number {
const str = JSON.stringify(key);
let hash = 0;

for (let i = 0; i < str.length; i++) {
hash = (hash * 31 + str.charCodeAt(i)) >>> 0;
}

return hash % this.size;
}

set(key: K, value: V): void {
const index = this.hash(key);
const bucket = this.buckets[index];

// Проверяем, есть ли уже такой ключ
const existingIndex = bucket.findIndex(([k]) => k === key);

if (existingIndex !== -1) {
bucket[existingIndex] = [key, value]; // Обновляем
} else {
bucket.push([key, value]); // Добавляем новую пару
}
}

get(key: K): V | undefined {
const index = this.hash(key);
const bucket = this.buckets[index];

const pair = bucket.find(([k]) => k === key);
return pair ? pair[1] : undefined;
}

has(key: K): boolean {
return this.get(key) !== undefined;
}

delete(key: K): boolean {
const index = this.hash(key);
const bucket = this.buckets[index];

const itemIndex = bucket.findIndex(([k]) => k === key);
if (itemIndex !== -1) {
bucket.splice(itemIndex, 1);
return true;
}
return false;
}
}

Коллизии и способы их разрешения:

Коллизия — когда разные ключи дают одинаковый хэш.

Метод цепочек (Separate Chaining):

// Каждый бакет — это массив пар
// bucket[42] = [["key1", value1], ["key2", value2]]

Открытая адресация (Open Addressing):

// Если ячейка занята — ищем следующую свободную
// Linear probing: index, index+1, index+2, ...
// Quadratic probing: index, index+1, index+4, index+9, ...

Практическое применение для идентификации объектов без ID:

Задача: Пользователь выбирает элемент из списка, нужно найти его в массиве.

interface Product {
name: string;
price: number;
category: string;
}

// ❌ Неэффективный подход — O(n) на каждый поиск
function findProductById(products: Product[], selectedName: string): Product | undefined {
return products.find(p => p.name === selectedName);
}

// ✅ Эффективный подход — O(1) после построения индекса
function createProductIndex(products: Product[]): Map<string, Product> {
const index = new Map<string, Product>();

for (const product of products) {
index.set(product.name, product);
}

return index;
}

// Использование
const products: Product[] = [
{ name: "iPhone 15", price: 999, category: "Electronics" },
{ name: "MacBook Pro", price: 1999, category: "Electronics" },
{ name: "AirPods", price: 199, category: "Accessories" }
];

const productIndex = createProductIndex(products);

// Теперь поиск за O(1)
const selected = productIndex.get("MacBook Pro"); // { name: "MacBook Pro", ... }

Составные ключи:

// Когда одного поля недостаточно для уникальности
function createCompositeKey(product: Product): string {
return `${product.name}|${product.category}`;
}

// Или используем Map с объектом как ключ (через WeakMap или сериализацию)
const index = new Map<string, Product>();

products.forEach(product => {
const key = JSON.stringify({ name: product.name, category: product.category });
index.set(key, product);
});

// Поиск
const searchKey = JSON.stringify({ name: "iPhone 15", category: "Electronics" });
const found = index.get(searchKey);

Практические примеры использования хэш-таблиц:

1. Дедупликация данных:

function removeDuplicates<T>(items: T[], getKey: (item: T) => string): T[] {
const seen = new Set<string>();
const result: T[] = [];

for (const item of items) {
const key = getKey(item);
if (!seen.has(key)) {
seen.add(key);
result.push(item);
}
}

return result;
}

// Использование
const users = [
{ id: 1, email: "a@b.com" },
{ id: 2, email: "c@d.com" },
{ id: 3, email: "a@b.com" } // Дубликат по email
];

const unique = removeDuplicates(users, u => u.email);
// [{ id: 1, email: "a@b.com" }, { id: 2, email: "c@d.com" }]

2. Подсчёт частот:

function countFrequencies<T>(items: T[]): Map<T, number> {
const frequencies = new Map<T, number>();

for (const item of items) {
frequencies.set(item, (frequencies.get(item) || 0) + 1);
}

return frequencies;
}

// Использование
const votes = ["apple", "banana", "apple", "orange", "banana", "apple"];
const result = countFrequencies(votes);
// Map { "apple" => 3, "banana" => 2, "orange" => 1 }

3. Кэширование (Memoization):

function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();

return ((...args: any[]) => {
const key = JSON.stringify(args);

if (cache.has(key)) {
return cache.get(key)!;
}

const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}

// Использование
const expensiveCalculation = memoize((n: number): number => {
console.log(`Calculating for ${n}`);
// Тяжёлые вычисления
return n * n;
});

expensiveCalculation(5); // "Calculating for 5" → 25
expensiveCalculation(5); // 25 (из кэша, без вычисления)

4. Группировка данных:

function groupBy<T, K extends string | number>(
items: T[],
getKey: (item: T) => K
): Map<K, T[]> {
const groups = new Map<K, T[]>();

for (const item of items) {
const key = getKey(item);

if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key)!.push(item);
}

return groups;
}

// Использование
const orders = [
{ id: 1, status: "pending" },
{ id: 2, status: "completed" },
{ id: 3, status: "pending" },
{ id: 4, status: "cancelled" }
];

const byStatus = groupBy(orders, o => o.status);
// Map {
// "pending" => [{ id: 1, ... }, { id: 3, ... }],
// "completed" => [{ id: 2, ... }],
// "cancelled" => [{ id: 4, ... }]
// }

Сравнение структур данных:

ОперацияМассивХэш-таблицаБинарное дерево поиска
ПоискO(n)O(1) среднийO(log n)
ВставкаO(1) в конецO(1) среднийO(log n)
УдалениеO(n)O(1) среднийO(log n)
Доступ по индексуO(1)НетНет
Отсортированный обходO(n log n)O(n log n)O(n)

Когда использовать хэш-таблицу:

  • Нужен быстрый доступ по ключу
  • Нужно проверить наличие элемента
  • Нужно подсчитать частоты или группировать данные
  • Нужно кэшировать результаты

Когда НЕ использовать:

  • Нужен отсортированный порядок — используйте дерево
  • Нужен доступ по индексу — используйте массив
  • Очень мало данных — массив может быть быстрее из-за кэш-локальности

Хэш-таблица — одна из самых полезных структур данных на практике, и она используется практически в каждом приложении.

Вопрос 14. Что такое хэш и хэширование? Зачем нужна хэш-сумма при скачивании дистрибутива?

Таймкод: 00:17:00

Ответ собеседника: Неполный. Хэш назвал уникальным идентификатором. На вопрос о хэшировании и хэш-сумме затруднился ответить. С подсказкой узнал, что хэширование — преобразование данных в коротное ID в одну сторону, используется для хранения паролей и проверки целостности файлов.

Правильный ответ:

Что такое хэш и хэширование

Хэширование — это процесс преобразования данных произвольного размера в значение фиксированного размера (хэш) с помощью хэш-функции.

Хэш (хеш-значение, дайджест) — это результат хэширования, строка фиксированной длины.

Входные данные (любой размер)

[Хэш-функция]

Хэш (фиксированный размер)

"hello" → SHA-256 → "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
"Hello" → SHA-256 → "185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"
"hello world!!!" → SHA-256 → "a3b1c4d5e6f7..." (совершенно другой хэш)

Свойства хороших хэш-функций:

1. Детерминированность — одинаковый вход всегда даёт одинаковый хэш 2. Фиксированный размер — независимо от размера выхода 3. Быстрота вычисления — O(n) для размера входа 4. Лавинный эффект — малое изменение входа кардинально меняет хэш 5. Стойкость к коллизиям — сложно найти два разных входа с одинаковым хэшем

Популярные хэш-функции:

АлгоритмРазмер выходаПрименение
MD5128 битУстарел, не для безопасности
SHA-1160 битУстарел, не для безопасности
SHA-256256 битБезопасность, блокчейн, сертификаты
SHA-3可变Новый стандарт
bcrypt可变Хранение паролей
xxHash32/64 битБыстрое хэширование, не криптографическое

Простая реализация хэш-функции (для понимания):

// Простая хэш-функция для строк (djb2)
function simpleHash(str: string): number {
let hash = 5381;

for (let i = 0; i < str.length; i++) {
// hash * 33 + charCode
hash = ((hash << 5) + hash) + str.charCodeAt(i);
}

return hash >>> 0; // Преобразуем в беззнаковое 32-битное число
}

console.log(simpleHash("hello")); // 261238937
console.log(simpleHash("hello")); // 261238937 (тот же)
console.log(simpleHash("Hello")); // 177230477 (другой!)

Зачем нужна хэш-сумма при скачивании дистрибутива

Хэш-сумма файла — это «отпечаток пальца» файла, который позволяет проверить его целостность.

Проблема:

Вы скачали файл размером 500MB. Как убедиться, что:
1. Файл скачался полностью (не повредился при передаче)
2. Файл не был изменён злоумышленником
3. Вы скачали именно тот файл, который опубликовал разработчик

Решение с хэш-суммой:

Разработчик публикует:
- Файл: ubuntu-22.04.iso
- SHA-256: a4acfda10b18da50e2ec50ccaf860d7f20b389df8765611142305c0e95c9f10b

Вы скачали файл и вычислили хэш:
$ sha256sum ubuntu-22.04.iso
a4acfda10b18da50e2ec50ccaf860d7f20b389df8765611142305c0e95c9f10b

Хэши совпадают → файл цел и аутентичен

Практические примеры использования хэшей:

1. Проверка целостности файлов:

# Linux/macOS
sha256sum downloaded-file.zip
md5sum downloaded-file.zip

# Windows (PowerShell)
Get-FileHash -Algorithm SHA256 downloaded-file.zip

2. Хранение паролей:

import bcrypt from 'bcrypt';

// Регистрация — храним хэш, а не пароль
async function hashPassword(password: string): Promise<string> {
const saltRounds = 10;
return bcrypt.hash(password, saltRounds);
}

// Вход — сравниваем хэши
async function verifyPassword(
password: string,
hashedPassword: string
): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}

// Использование
const hashed = await hashPassword("mySecret123");
// "$2b$10$LJ3m4ys3Lg2VBe0E/R5XxOYqIGNPMGBO..."

const isValid = await verifyPassword("mySecret123", hashed); // true
const isInvalid = await verifyPassword("wrongPassword", hashed); // false

Почему нельзя хранить пароли в открытом виде:

База данных скомпрометирована:

❌ Открытый вид:
| user | password |
|------|--------------|
| john | mySecret123 | ← Злоумышленник сразу знает пароль

✅ Хэш:
| user | password_hash |
|------|--------------------------------------------------------------|
| john | $2b$10$LJ3m4ys3Lg2VBe0E/R5XxOYqIGNPMGBO... | ← Невозможно восстановить пароль

3. Кэширование с хэш-ключами:

import crypto from 'crypto';

function generateCacheKey(params: Record<string, any>): string {
const str = JSON.stringify(params);
return crypto.createHash('sha256').update(str).digest('hex');
}

// Использование
const cacheKey = generateCacheKey({
userId: 123,
page: 1,
limit: 20
});
// "a1b2c3d4e5f6..."

4. Content Hash для кэширования в веб-разработке:

// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
// main.a1b2c3d4.js
// При изменении содержимого — меняется хэш — браузер загрузит новый файл
}
};

5. Git использует SHA-1 для идентификации коммитов:

$ git log --oneline
a1b2c3d Fix bug in authentication
d4e5f6a Add new feature
# a1b2c3d — это сокращённый хэш коммита

Криптографические vs некриптографические хэш-функции:

Криптографические (SHA-256, bcrypt, Argon2):

  • Медленные специально (защита от brute-force)
  • Стойкие к коллизиям
  • Для безопасности: пароли, подписи, сертификаты

Некриптографические (xxHash, MurmurHash, FNV):

  • Очень быстрые
  • Могут иметь коллизии
  • Для хэш-таблиц, кэшей, проверки целостности (не безопасность)

Атака на коллизии:

// MD5 и SHA-1 считаются небезопасными, потому что найдены коллизии:
// Два разных файла с одинаковым MD5!

// Для безопасности используйте SHA-256 или выше
import crypto from 'crypto';

function sha256(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex');
}

console.log(sha256("sensitive data"));
// "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"

Итог:

Хэширование — фундаментальная концепция в информатике, которая используется повсюду: от проверки целостности файлов и хранения паролей до хэш-таблиц и систем контроля версий. Понимание хэшей важно для любого разработчика.

Вопрос 15. Какие основные принципы ООП существуют?

Таймкод: 00:20:09

Ответ собеседника: Правильный. Назвал все четыре принципа: абстракция, полиморфизм, наследование и инкапсуляция.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим каждый принцип подробно с примерами.

1. Инкапсуляция (Encapsulation)

Сокрытие внутренней реализации и предоставление контролируемого доступа через публичный интерфейс.

class BankAccount {
private balance: number = 0;
private readonly accountId: string;

constructor(initialBalance: number) {
this.accountId = crypto.randomUUID();
if (initialBalance < 0) {
throw new Error('Initial balance cannot be negative');
}
this.balance = initialBalance;
}

// Публичный метод для получения баланса (только чтение)
getBalance(): number {
return this.balance;
}

// Публичный метод для пополнения с валидацией
deposit(amount: number): void {
if (amount <= 0) {
throw new Error('Deposit amount must be positive');
}
this.balance += amount;
}

// Публичный метод для снятия с валидацией
withdraw(amount: number): void {
if (amount <= 0) {
throw new Error('Withdrawal amount must be positive');
}
if (amount > this.balance) {
throw new Error('Insufficient funds');
}
this.balance -= amount;
}
}

// Использование
const account = new BankAccount(1000);
account.deposit(500); // OK
account.withdraw(200); // OK
// account.balance = 999999; // ❌ Ошибка компиляции — приватное поле
console.log(account.getBalance()); // 1300

Преимущества инкапсуляции:

  • Защита данных от некорректного изменения
  • Возможность изменить реализацию без изменения интерфейса
  • Контроль над тем, как данные читаются и модифицируются

2. Наследование (Inheritance)

Создание новых классов на основе существующих с повторным использованием кода.

// Базовый класс
class Animal {
constructor(
protected name: string,
protected age: number
) {}

makeSound(): void {
console.log('Some generic sound');
}

getInfo(): string {
return `${this.name}, age: ${this.age}`;
}
}

// Производный класс
class Dog extends Animal {
constructor(
name: string,
age: number,
private breed: string
) {
super(name, age); // Вызов конструктора родителя
}

// Переопределение метода
makeSound(): void {
console.log('Woof! Woof!');
}

// Новый метод
fetch(item: string): void {
console.log(`${this.name} fetches the ${item}`);
}

// Расширение метода родителя
getInfo(): string {
return `${super.getInfo()}, breed: ${this.breed}`;
}
}

// Ещё один производный класс
class Cat extends Animal {
constructor(name: string, age: number) {
super(name, age);
}

makeSound(): void {
console.log('Meow!');
}
}

// Использование
const dog = new Dog('Rex', 3, 'Labrador');
dog.makeSound(); // "Woof! Woof!"
dog.fetch('ball'); // "Rex fetches the ball"
dog.getInfo(); // "Rex, age: 3, breed: Labrador"

const cat = new Cat('Whiskers', 2);
cat.makeSound(); // "Meow!"

3. Полиморфизм (Polimorphism)

Возможность объектов разных классов обрабатываться через единый интерфейс.

// Полиморфизм через наследование
function animalConcert(animals: Animal[]): void {
for (const animal of animals) {
animal.makeSound(); // Вызывается соответствующая реализация
}
}

const animals: Animal[] = [
new Dog('Rex', 3, 'Labrador'),
new Cat('Whiskers', 2),
new Dog('Buddy', 5, 'Beagle')
];

animalConcert(animals);
// "Woof! Woof!"
// "Meow!"
// "Woof! Woof!"

Полиморфизм через интерфейсы:

interface Shape {
area(): number;
perimeter(): number;
}

class Circle implements Shape {
constructor(private radius: number) {}

area(): number {
return Math.PI * this.radius ** 2;
}

perimeter(): number {
return 2 * Math.PI * this.radius;
}
}

class Rectangle implements Shape {
constructor(
private width: number,
private height: number
) {}

area(): number {
return this.width * this.height;
}

perimeter(): number {
return 2 * (this.width + this.height);
}
}

class Triangle implements Shape {
constructor(
private a: number,
private b: number,
private c: number
) {}

area(): number {
// Формула Герона
const s = this.perimeter() / 2;
return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
}

perimeter(): number {
return this.a + this.b + this.c;
}
}

// Полиморфная функция
function printShapeInfo(shape: Shape): void {
console.log(`Area: ${shape.area().toFixed(2)}`);
console.log(`Perimeter: ${shape.perimeter().toFixed(2)}`);
}

// Использование
const shapes: Shape[] = [
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 4, 5)
];

shapes.forEach(printShapeInfo);

4. Абстракция (Abstraction)

Выделение существенных характеристик объекта и игнорирование несущественных.

// Абстрактный класс — нельзя создать напрямую
abstract class PaymentProcessor {
// Абстрактные методы — должны быть реализованы в наследниках
abstract processPayment(amount: number): Promise<boolean>;
abstract refund(transactionId: string): Promise<boolean>;

// Общая логика для всех процессоров
protected logTransaction(type: string, amount: number): void {
console.log(`[${new Date().toISOString()}] ${type}: $${amount}`);
}

// Шаблонный метод
async executePayment(amount: number): Promise<string> {
this.logTransaction('PAYMENT', amount);

const success = await this.processPayment(amount);

if (!success) {
throw new Error('Payment failed');
}

return crypto.randomUUID();
}
}

// Конкретная реализация
class StripeProcessor extends PaymentProcessor {
constructor(private apiKey: string) {
super();
}

async processPayment(amount: number): Promise<boolean> {
// Логика работы со Stripe API
console.log(`Processing $${amount} via Stripe`);
return true;
}

async refund(transactionId: string): Promise<boolean> {
console.log(`Refunding transaction ${transactionId} via Stripe`);
return true;
}
}

class PayPalProcessor extends PaymentProcessor {
constructor(private clientId: string, private secret: string) {
super();
}

async processPayment(amount: number): Promise<boolean> {
console.log(`Processing $${amount} via PayPal`);
return true;
}

async refund(transactionId: string): Promise<boolean> {
console.log(`Refunding transaction ${transactionId} via PayPal`);
return true;
}
}

// Использование
async function checkout(processor: PaymentProcessor, amount: number) {
const transactionId = await processor.executePayment(amount);
console.log(`Transaction completed: ${transactionId}`);
}

await checkout(new StripeProcessor('sk_test_xxx'), 99.99);
await checkout(new PayPalProcessor('client_xxx', 'secret_xxx'), 149.99);

Принцип композиции вместо наследования:

// ❌ Проблема глубокого наследования
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
class GuideDog extends Dog { } // Слишком глубоко!

// ✅ Композиция — предпочтительный подход
interface CanBark {
bark(): void;
}

interface CanFetch {
fetch(item: string): void;
}

interface CanGuide {
guideOwner(): void;
}

class Dog implements CanBark, CanFetch {
bark() { console.log('Woof!'); }
fetch(item: string) { console.log(`Fetching ${item}`); }
}

class GuideDog implements CanBark, CanFetch, CanGuide {
bark() { console.log('Woof!'); }
fetch(item: string) { console.log(`Fetching ${item}`); }
guideOwner() { console.log('Guiding owner...'); }
}

Связь с SOLID принципами:

Принцип ООПСвязь с SOLID
ИнкапсуляцияSingle Responsibility, Open/Closed
НаследованиеLiskov Substitution
ПолиморфизмInterface Segregation, Dependency Inversion
АбстракцияВсе принципы SOLID

Четыре принципа ООП — это фундамент, на котором строится объектно-ориентированное проектирование. Понимание этих принципов позволяет создавать гибкие, расширяемые и поддерживаемые системы.

Вопрос 16. Что такое инкапсуляция?

Таймкод: 00:20:25

Ответ собеседника: Правильный. Инкапсуляция — сокрытие данных, чтобы нельзя было получить к ним доступ извне класса. В TypeScript для этого используются модификаторы private и protected.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим инкапсуляцию более детально.

Определение

Инкапсуляция — это механизм, который:

  1. Объединяет данные и методы работы с ними в единую сущность (класс)
  2. Скрывает внутреннюю реализацию от внешнего мира
  3. Предоставляет контролируемый доступ через публичный интерфейс

Модификаторы доступа в TypeScript:

class User {
// public — доступен отовсюду (по умолчанию)
public name: string;

// protected — доступен внутри класса и наследников
protected email: string;

// private — доступен только внутри класса
private passwordHash: string;

// readonly — можно только читать после инициализации
readonly id: string;

constructor(name: string, email: string, password: string) {
this.id = crypto.randomUUID();
this.name = name;
this.email = email;
this.passwordHash = this.hashPassword(password);
}

private hashPassword(password: string): string {
// Хэширование пароля
return `hashed_${password}`;
}

// Публичный метод для проверки пароля
verifyPassword(password: string): boolean {
return this.passwordHash === this.hashPassword(password);
}

// Геттер — контролируемое чтение
get userEmail(): string {
return this.email.replace(/(?<=.{3}).*?(?=@)/, '***');
}

// Сеттер — контролируемая запись
set userEmail(newEmail: string) {
if (!newEmail.includes('@')) {
throw new Error('Invalid email format');
}
this.email = newEmail;
}
}

// Использование
const user = new User('John', 'john@example.com', 'secret123');

console.log(user.name); // OK — public
console.log(user.userEmail); // OK — через геттер: "joh***@example.com"
// console.log(user.email); // ❌ Error — protected
// console.log(user.passwordHash); // ❌ Error — private

user.userEmail = 'new@example.com'; // OK — через сеттер с валидацией
// user.userEmail = 'invalid'; // ❌ Error — невалидный email

Паттерн «Свойство с закрытым сеттером»:

class Order {
private _status: 'pending' | 'processing' | 'shipped' | 'delivered' = 'pending';

// Можно читать отовсюду
get status(): string {
return this._status;
}

// Можно изменить только через метод с валидацией
ship(): void {
if (this._status !== 'processing') {
throw new Error('Order must be in processing status to ship');
}
this._status = 'shipped';
}

deliver(): void {
if (this._status !== 'shipped') {
throw new Error('Order must be shipped before delivery');
}
this._status = 'delivered';
}
}

Инкапсуляция в функциональном стиле:

// Замыкания как альтернатива классам
function createCounter(initialValue = 0) {
// Приватная переменная — недоступна извне
let count = initialValue;

return {
increment() {
count++;
},
decrement() {
count--;
},
getCount() {
return count;
},
reset() {
count = initialValue;
}
};
}

const counter = createCounter(10);
counter.increment();
counter.increment();
console.log(counter.getCount()); // 12
// console.log(counter.count); // undefined — нет доступа

Преимущества инкапсуляции:

  • Защита данных — невозможно установить некорректное состояние
  • Гибкость реализации — можно изменить внутреннюю логику без изменения API
  • Упрощение использования — пользователь видит только необходимое
  • Сложность отладки — состояние объекта контролируется

Антипаттерн — отсутствие инкапсуляции:

// ❌ Плохо — данные открыты, можно установить что угодно
class BadUser {
public name: string;
public age: number;
public email: string;
}

const badUser = new BadUser();
badUser.age = -5; // Некорректно, но возможно
badUser.email = 'invalid'; // Тоже возможно

// ✅ Хорошо — данные защищены
class GoodUser {
private _age: number = 0;
private _email: string = '';

set age(value: number) {
if (value < 0 || value > 150) {
throw new Error('Invalid age');
}
this._age = value;
}

set email(value: string) {
if (!value.includes('@')) {
throw new Error('Invalid email');
}
this._email = value;
}
}

Инкапсуляция — это не просто «сокрытие данных», а осознанное проектирование границ ответственности между объектами.

Вопрос 17. Что такое абстракция в контексте ООП?

Таймкод: 00:21:04

Ответ собеседника: Неправильный. Не смог дать определение абстракции. Попытался вспомнить через другие принципы ООП: полиморфизм и наследование, но абстракцию так и не раскрыл.

Правильный ответ:

Определение абстракции

Абстракция — это процесс выделения существенных характеристик объекта и игнорирования несущественных. В контексте ООП абстракция позволяет работать с объектами на уровне «что они делают», а не «как они это делают».

Простая аналогия:

Руль автомобиля — это абстракция:
- Вы знаете: поворот руля → машина поворачивает
- Вы НЕ знаете: как работает рулевая рейка, гидроусилитель и т.д.
- Вам НЕ НУЖНО это знать для управления

Абстракция через интерфейсы:

// Интерфейс — это контракт, который определяет ЧТО объект умеет делать
interface Storage {
save(key: string, value: unknown): Promise<void>;
load(key: string): Promise<unknown>;
delete(key: string): Promise<void>;
exists(key: string): Promise<boolean>;
}

// Разные реализации — КАК именно это делается
class LocalStorage implements Storage {
async save(key: string, value: unknown): Promise<void> {
localStorage.setItem(key, JSON.stringify(value));
}

async load(key: string): Promise<unknown> {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
}

async delete(key: string): Promise<void> {
localStorage.removeItem(key);
}

async exists(key: string): Promise<boolean> {
return localStorage.getItem(key) !== null;
}
}

class RedisStorage implements Storage {
constructor(private client: RedisClient) {}

async save(key: string, value: unknown): Promise<void> {
await this.client.set(key, JSON.stringify(value));
}

async load(key: string): Promise<unknown> {
const data = await this.client.get(key);
return data ? JSON.parse(data) : null;
}

async delete(key: string): Promise<void> {
await this.client.del(key);
}

async exists(key: string): Promise<boolean> {
return await this.client.exists(key) === 1;
}
}

// Код, использующий Storage, не знает и не должен знать о реализации
class UserService {
constructor(private storage: Storage) {}

async saveUser(user: User): Promise<void> {
await this.storage.save(`user:${user.id}`, user);
}

async getUser(id: string): Promise<User | null> {
return await this.storage.load(`user:${id}`) as User | null;
}
}

// Можно легко подменить реализацию
const localStorageService = new UserService(new LocalStorage());
const redisService = new UserService(new RedisStorage(redisClient));

Абстракция через абстрактные классы:

// Абстрактный класс определяет общий шаблон поведения
abstract class HttpClient {
// Абстрактные методы — должны быть реализованы
abstract get<T>(url: string): Promise<T>;
abstract post<T>(url: string, data: unknown): Promise<T>;

// Общая логика — используется всеми наследниками
protected async request<T>(
method: string,
url: string,
data?: unknown
): Promise<T> {
console.log(`[${method}] ${url}`);

// Общая логика: логирование, обработка ошибок, таймауты
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: data ? JSON.stringify(data) : undefined
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return response.json();
}
}

class JsonPlaceholderClient extends HttpClient {
private baseUrl = 'https://jsonplaceholder.typicode.com';

async get<T>(url: string): Promise<T> {
return this.request<T>('GET', `${this.baseUrl}${url}`);
}

async post<T>(url: string, data: unknown): Promise<T> {
return this.request<T>('POST', `${this.baseUrl}${url}`, data);
}
}

Уровни абстракции:

// Низкий уровень — много деталей
class LowLevelFileWriter {
openFile(path: string) { /* ... */ }
writeBytes(bytes: Buffer) { /* ... */ }
flush() { /* ... */ }
closeFile() { /* ... */ }
}

// Средний уровень — менее деталей
class MediumLevelFileWriter {
writeFile(path: string, content: string) {
this.openFile(path);
this.writeBytes(Buffer.from(content));
this.flush();
this.closeFile();
}
}

// Высокий уровень — только суть
class HighLevelFileWriter {
async saveReport(report: Report): Promise<void> {
// Пользователь не знает про файлы, байты и т.д.
// Он просто «сохраняет отчёт»
}
}

Абстракция в реальном фронтенде:

// Абстракция над уведомлениями
interface NotificationService {
show(message: string, type: 'success' | 'error' | 'info'): void;
}

// Реализация через toast-уведомления
class ToastNotificationService implements NotificationService {
show(message: string, type: 'success' | 'error' | 'info'): void {
// Использует библиотеку toast (react-toastify, sonner и т.д.)
toast[type](message);
}
}

// Реализация через модальные окна
class ModalNotificationService implements NotificationService {
show(message: string, type: 'success' | 'error' | 'info'): void {
// Показывает модальное окно с сообщением
showModal({ message, type });
}
}

// Компонент не знает о реализации
function SubmitButton({ onSuccess }: { onSuccess: () => void }) {
const notifications = useNotificationService(); // Получаем абстракцию

const handleSubmit = async () => {
try {
await submitForm();
notifications.show('Форма отправлена!', 'success');
onSuccess();
} catch (error) {
notifications.show('Ошибка при отправке', 'error');
}
};

return <button onClick={handleSubmit}>Отправить</button>;
}

Ключевые идеи абстракции:

  1. Сложность скрыта — пользователь видит только необходимый интерфейс
  2. Реализация может меняться — пока интерфейс не изменился, код работает
  3. Повторное использование — одна абстракция — много реализаций
  4. Тестирование — легко подменить реализацию моком

Отличие от других принципов:

  • Инкапсуляция — скрывает данные (КАК хранить)
  • Абстракция — скрывает реализацию (КАК делать)
  • Полиморфизм — позволяет разным объектам иметь одинаковый интерфейс (ЧТО делать)

Абстракция — это способ борьбы со сложностью: мы думаем о задачах на высоком уровне, не отвлекаясь на детали реализации.

Вопрос 18. Что такое абстрактный класс в TypeScript?

Таймкод: 00:22:18

Ответ собеседника: Правильный. Абстрактный класс описывает сигнатуры методов (типы, что принимают и возвращают), но не реализует логику. Нельзя создавать экземпляры абстрактных классов. Используется для описания контракта.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим абстрактные классы более подробно.

Определение

Абстрактный класс — это класс, который:

  • Нельзя инстанцировать напрямую (создать через new)
  • Может содержать абстрактные методы без реализации
  • Может содержать обычные методы с реализацией
  • Служит базовым классом для наследования

Базовый синтаксис:

abstract class Shape {
// Абстрактное свойство — должно быть реализовано
abstract color: string;

// Абстрактный метод — только сигнатура, без реализации
abstract area(): number;
abstract perimeter(): number;

// Обычный метод с реализацией — наследуется как есть
describe(): string {
return `${this.color} shape with area ${this.area().toFixed(2)}`;
}

// Обычный метод, использующий абстрактные методы
scale(factor: number): string {
return `Scaled area: ${(this.area() * factor * factor).toFixed(2)}`;
}
}

// ❌ Ошибка: нельзя создать экземпляр абстрактного класса
// const shape = new Shape(); // Error!

// ✅ Конкретная реализация
class Circle extends Shape {
color = 'red';

constructor(private radius: number) {
super();
}

area(): number {
return Math.PI * this.radius ** 2;
}

perimeter(): number {
return 2 * Math.PI * this.radius;
}
}

class Rectangle extends Shape {
color = 'blue';

constructor(
private width: number,
private height: number
) {
super();
}

area(): number {
return this.width * this.height;
}

perimeter(): number {
return 2 * (this.width + this.height);
}
}

// Использование
const circle = new Circle(5);
console.log(circle.describe()); // "red shape with area 78.54"
console.log(circle.scale(2)); // "Scaled area: 314.16"

const rect = new Rectangle(4, 6);
console.log(rect.describe()); // "blue shape with area 24.00"

Абстрактный класс vs Интерфейс:

// Интерфейс — только контракт, без реализации
interface IAnimal {
name: string;
makeSound(): void;
}

// Абстрактный класс — контракт + частичная реализация
abstract class Animal {
constructor(protected name: string) {}

abstract makeSound(): void;

// Общая логика для всех животных
sleep(): void {
console.log(`${this.name} is sleeping...`);
}

getInfo(): string {
return `Animal: ${this.name}`;
}
}

// Когда что использовать:

// Интерфейс — когда нужен только контракт
interface Serializable {
toJSON(): string;
fromJSON(json: string): void;
}

// Абстрактный класс — когда есть общая логика
abstract class Repository<T> {
protected items: T[] = [];

abstract findById(id: string): T | undefined;

// Общая логика
getAll(): T[] {
return [...this.items];
}

getCount(): number {
return this.items.length;
}
}

Практический пример — паттерн Template Method:

abstract class DataExporter {
// Шаблонный метод — определяет алгоритм
async export(data: unknown[], destination: string): Promise<void> {
// Шаг 1: Валидация (общая логика)
this.validate(data);

// Шаг 2: Трансформация (абстрактный — разная для каждого формата)
const transformed = this.transform(data);

// Шаг 3: Форматирование (абстрактный)
const formatted = this.format(transformed);

// Шаг 4: Сохранение (общая логика)
await this.save(formatted, destination);

// Шаг 5: Логирование (общая логика)
this.log(`Exported ${data.length} items to ${destination}`);
}

// Абстрактные методы — должны быть реализованы
abstract transform(data: unknown[]): unknown[];
abstract format(data: unknown[]): string;

// Методы по умолчанию — можно переопределить
validate(data: unknown[]): void {
if (!Array.isArray(data) || data.length === 0) {
throw new Error('Data must be a non-empty array');
}
}

async save(content: string, destination: string): Promise<void> {
await fs.writeFile(destination, content);
}

log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}

class CsvExporter extends DataExporter {
transform(data: unknown[]): unknown[] {
// Фильтрация и преобразование данных для CSV
return data.filter(item => item !== null);
}

format(data: unknown[]): string {
const headers = Object.keys(data[0] as object).join(',');
const rows = data.map(item =>
Object.values(item as object).join(',')
);
return [headers, ...rows].join('\n');
}
}

class JsonExporter extends DataExporter {
transform(data: unknown[]): unknown[] {
return data;
}

format(data: unknown[]): string {
return JSON.stringify(data, null, 2);
}

// Переопределяем валидацию
validate(data: unknown[]): void {
super.validate(data);
// Дополнительная валидация для JSON
try {
JSON.stringify(data);
} catch {
throw new Error('Data is not JSON-serializable');
}
}
}

// Использование
const data = [
{ id: 1, name: 'John', age: 30 },
{ id: 2, name: 'Jane', age: 25 }
];

const csvExporter = new CsvExporter();
await csvExporter.export(data, './output.csv');

const jsonExporter = new JsonExporter();
await jsonExporter.export(data, './output.json');

Абстрактный класс с конструктором:

abstract class Component {
protected children: Component[] = [];

constructor(
protected name: string,
protected parent?: Component
) {
parent?.addChild(this);
}

abstract render(): string;

addChild(child: Component): void {
this.children.push(child);
}

// Метод, использующий абстрактный метод
toString(indent = 0): string {
const padding = ' '.repeat(indent);
let result = `${padding}<${this.name}>\n`;

for (const child of this.children) {
result += child.toString(indent + 2);
}

result += `${padding}</${this.name}>\n`;
return result;
}
}

class Button extends Component {
constructor(
name: string,
parent: Component,
private label: string
) {
super(name, parent);
}

render(): string {
return `<button>${this.label}</button>`;
}
}

class Container extends Component {
render(): string {
return this.children.map(c => c.render()).join('\n');
}
}

// Использование
const form = new Container('form');
new Button('submit-btn', form, 'Submit');
new Button('cancel-btn', form, 'Cancel');

console.log(form.toString());

Когда использовать абстрактные классы:

  • Есть общая логика, которую нужно разделить
  • Нужно заставить наследников реализовать определённые методы
  • Нужен базовый класс с частичной реализацией

Когда использовать интерфейсы:

  • Нужен только контракт без реализации
  • Нужно множественное «наследование» поведения
  • Описание формы объекта (data shape)

Комбинация интерфейса и абстрактного класса:

// Интерфейс для максимальной гибкости
interface ILogger {
log(message: string): void;
error(message: string): void;
}

// Абстрактный класс для общей логики
abstract class BaseLogger implements ILogger {
abstract log(message: string): void;
abstract error(message: string): void;

protected formatMessage(level: string, message: string): string {
return `[${new Date().toISOString()}] [${level}] ${message}`;
}
}

// Конкретная реализация
class ConsoleLogger extends BaseLogger {
log(message: string): void {
console.log(this.formatMessage('INFO', message));
}

error(message: string): void {
console.error(this.formatMessage('ERROR', message));
}
}

Абстрактные классы — мощный инструмент для создания расширяемых архитектур с общей логикой и обязательным контрактом для наследников.

Вопрос 19. Что такое лямбда-функции (стрелочные функции) и в чём их особенности?

Таймкод: 00:22:56

Ответ собеседника: Правильный. Лямбда-функции — это стрелочные функции. Основные отличия: не всплывают (no hoisting), не создают собственный контекст this (берут вышестоящий). Используются для краткой записи, передачи в методы массивов.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим стрелочные функции более детально.

Синтаксис стрелочных функций:

// Function declaration
function add(a: number, b: number): number {
return a + b;
}

// Function expression
const add2 = function(a: number, b: number): number {
return a + b;
};

// Arrow function — полная форма
const add3 = (a: number, b: number): number => {
return a + b;
};

// Arrow function — сокращённая форма (одно выражение)
const add4 = (a: number, b: number): number => a + b;

// Один параметр — скобки необязательны
const double = (n: number): number => n * 2;

// Нет параметров
const getRandom = (): number => Math.random();

// Возврат объекта — оборачиваем в скобки
const createUser = (name: string) => ({ name, id: Date.now() });

// Многострочное тело
const processData = (data: number[]): number[] => {
const filtered = data.filter(n => n > 0);
const doubled = filtered.map(n => n * 2);
return doubled.sort((a, b) => a - b);
};

Ключевые особенности:

1. Лексический this

class Timer {
private seconds = 0;

// ❌ Обычная функция — this зависит от контекста вызова
startBroken(): void {
setInterval(function() {
this.seconds++; // this — window/undefined, не экземпляр Timer!
console.log(this.seconds);
}, 1000);
}

// ✅ Стрелочная функция — this берётся из окружающего контекста
startFixed(): void {
setInterval(() => {
this.seconds++; // this — экземпляр Timer
console.log(this.seconds);
}, 1000);
}
}

Практический пример с обработчиками событий:

class Button {
private clicks = 0;
private element: HTMLElement;

constructor(elementId: string) {
this.element = document.getElementById(elementId)!;

// ❌ Проблема с обычной функцией
// this.element.addEventListener('click', function() {
// this.handleClick(); // this — элемент DOM, не Button!
// });

// ✅ Решение со стрелочной функцией
this.element.addEventListener('click', () => {
this.handleClick(); // this — экземпляр Button
});
}

private handleClick(): void {
this.clicks++;
console.log(`Clicked ${this.clicks} times`);
}
}

2. Отсутствие hoisting

// ✅ Function declaration — можно вызвать до объявления
console.log(add(2, 3)); // 5
function add(a: number, b: number): number {
return a + b;
}

// ❌ Arrow function — нельзя вызвать до объявления
// console.log(multiply(2, 3)); // Error!
const multiply = (a: number, b: number): number => a * b;

3. Нет собственного arguments

// Обычная функция
function regular() {
console.log(arguments); // [1, 2, 3]
}
regular(1, 2, 3);

// Стрелочная функция — нет arguments
const arrow = () => {
console.log(arguments); // ReferenceError!
};
// arrow(1, 2, 3);

// Решение — rest параметры
const arrowFixed = (...args: number[]) => {
console.log(args); // [1, 2, 3]
};
arrowFixed(1, 2, 3);

4. Не могут быть конструкторами

class Person {
constructor(public name: string) {}
}

const Person2 = (name: string) => {
// return { name }; // Можно вернуть объект
};

const p1 = new Person('John'); // OK
// const p2 = new Person2('John'); // Error! Нельзя использовать new

5. Нет prototype

function regular() {}
console.log(regular.prototype); // { constructor: f }

const arrow = () => {};
console.log(arrow.prototype); // undefined

Использование с методами массивов:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Цепочка вызовов — стрелочные функции идеальны
const result = numbers
.filter(n => n % 2 === 0) // [2, 4, 6, 8, 10]
.map(n => n * n) // [4, 16, 36, 64, 100]
.filter(n => n > 20) // [36, 64, 100]
.reduce((sum, n) => sum + n, 0); // 200

// С объектами
interface User {
id: number;
name: string;
age: number;
active: boolean;
}

const users: User[] = [
{ id: 1, name: 'John', age: 25, active: true },
{ id: 2, name: 'Jane', age: 30, active: false },
{ id: 3, name: 'Bob', age: 35, active: true }
];

// Имена активных пользователей старше 25
const names = users
.filter(u => u.active && u.age > 25)
.map(u => u.name);
// ['Bob']

Стрелочные функции в React:

// Обработчики событий
function SearchBar() {
const [query, setQuery] = useState('');

// ✅ Стрелочная функция сохраняет this (если нужно)
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
};

// ✅ Inline стрелочная функция
const handleClear = () => {
setQuery('');
};

return (
<div>
<input value={query} onChange={handleChange} />
<button onClick={handleClear}>Clear</button>
</div>
);
}

// Передача в дочерние компоненты
function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
// ✅ Стрелочная функция для передачи параметра
onDelete={(id) => handleDelete(id)}
// ❌ Проблема: новая функция на каждый рендер
// onDelete={handleDelete.bind(this, todo.id)}
/>
))}
</ul>
);
}

Каррирование со стрелочными функциями:

// Каррирование — преобразование функции с несколькими аргументами
// в последовательность функций с одним аргументом

// Обычная функция
function add(a: number, b: number, c: number): number {
return a + b + c;
}

// Каррированная версия
const curriedAdd = (a: number) => (b: number) => (c: number) =>
a + b + c;

// Использование
const add5 = curriedAdd(5); // (b) => (c) => 5 + b + c
const add5and3 = add5(3); // (c) => 5 + 3 + c
const result = add5and3(2); // 10

// Или сразу
const result2 = curriedAdd(5)(3)(2); // 10

// Практическое пример — логирование
const log = (level: string) => (module: string) => (message: string) => {
console.log(`[${level}] [${module}] ${message}`);
};

const errorLog = log('ERROR');
const authError = errorLog('AUTH');

authError('User not found'); // [ERROR] [AUTH] User not found
authError('Invalid token'); // [ERROR] [AUTH] Invalid token

Стрелочные функции и типизация:

// Типизация стрелочной функций
type MathOperation = (a: number, b: number) => number;

const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const multiply: MathOperation = (a, b) => a * b;

// Стрелочная функция как колбэк
function fetchData<T>(
url: string,
transform: (data: unknown) => T
): Promise<T> {
return fetch(url)
.then(res => res.json())
.then(transform);
}

// Использование
const users = await fetchData<User[]>(
'/api/users',
(data) => data as User[]
);

Когда использовать стрелочные функции:

  • Колбэки и обработчики событий
  • Методы массивов (map, filter, reduce)
  • Функции, которым нужен лексический this
  • Короткие однострочные функции

Когда использовать function declaration:

  • Методы объектов, где нужен динамический this
  • Конструкторы
  • Функции, которые должны быть hoisted
  • Функции, использующие arguments
// Метод объекта — function предпочтительнее
const user = {
name: 'John',

// ✅ Правильно
greet() {
return `Hello, ${this.name}!`;
},

// ❌ Неправильно — this не будет указывать на объект
greetArrow: () => {
return `Hello, ${this.name}!`; // this — внешний контекст
}
};

Стрелочные функции — не просто «синтаксический сахар», а фундаментально другой способ работы с this, который решает многие проблемы с контекстом в JavaScript.

Вопрос 20. Что такое функции высшего порядка? map/filter/reduce — какими функциями они являются?

Таймкод: 00:23:48

Ответ собеседника: Правильный. map/filter/reduce — это функции высшего порядка, так как они принимают другие функции в качестве аргументов.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим тему более подробно.

Определение функций высшего порядка

Функция высшего порядка (Higher-Order Function, HOF) — это функция, которая:

  1. Принимает другую функцию как аргумент, или
  2. Возвращает функцию как результат, или
  3. И то, и другое

Это возможно благодаря концепции «функции как объекты первого класса» (first-class functions) — функции можно присваивать переменным, передавать как аргументы и возвращать из других функций.

map, filter, reduce — функции высшего порядка:

const numbers = [1, 2, 3, 4, 5];

// map — принимает функцию-трансформацию
const doubled = numbers.map((n) => n * 2);
// [2, 4, 6, 8, 10]

// filter — принимает функцию-предикат
const evens = numbers.filter((n) => n % 2 === 0);
// [2, 4]

// reduce — принимает функцию-аккумулятор
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15

Реализация map, filter, reduce для понимания:

// Реализация map
function myMap<T, U>(
array: T[],
transform: (item: T, index: number) => U
): U[] {
const result: U[] = [];

for (let i = 0; i < array.length; i++) {
result.push(transform(array[i], i));
}

return result;
}

// Реализация filter
function myFilter<T>(
array: T[],
predicate: (item: T, index: number) => boolean
): T[] {
const result: T[] = [];

for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i)) {
result.push(array[i]);
}
}

return result;
}

// Реализация reduce
function myReduce<T, U>(
array: T[],
reducer: (accumulator: U, item: T, index: number) => U,
initialValue: U
): U {
let accumulator = initialValue;

for (let i = 0; i < array.length; i++) {
accumulator = reducer(accumulator, array[i], i);
}

return accumulator;
}

// Использование
const numbers = [1, 2, 3, 4, 5];

myMap(numbers, n => n * 2); // [2, 4, 6, 8, 10]
myFilter(numbers, n => n % 2 === 0); // [2, 4]
myReduce(numbers, (acc, n) => acc + n, 0); // 15

Функции, возвращающие функции:

// Фабрика функций
function createMultiplier(factor: number): (n: number) => number {
return (n: number) => n * factor;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// Каррирование
function curry<T, U, V>(
fn: (a: T, b: U) => V
): (a: T) => (b: U) => V {
return (a: T) => (b: U) => fn(a, b);
}

const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);

const add5 = curriedAdd(5);
console.log(add5(3)); // 8
console.log(add5(10)); // 15

Практические примеры функций высшего порядка:

1. Мемоизация:

function memoize<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>();

return ((...args: any[]) => {
const key = JSON.stringify(args);

if (cache.has(key)) {
return cache.get(key)!;
}

const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
}

// Использование
const expensiveFibonacci = memoize((n: number): number => {
if (n <= 1) return n;
return expensiveFibonacci(n - 1) + expensiveFibonacci(n - 2);
});

console.log(expensiveFibonacci(50)); // Быстро благодаря кэшу

2. Дебаунс и троттлинг:

function debounce<T extends (...args: any[]) => void>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>;

return (...args: Parameters<T>) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}

function throttle<T extends (...args: any[]) => void>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let lastCall = 0;

return (...args: Parameters<T>) => {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn(...args);
}
};
}

// Использование
const searchAPI = (query: string) => {
console.log(`Searching for: ${query}`);
};

const debouncedSearch = debounce(searchAPI, 300);
const throttledSearch = throttle(searchAPI, 1000);

// В input onChange:
// onChange={(e) => debouncedSearch(e.target.value)}

3. Композиция функций:

// Композиция — объединение функций в цепочку
function compose<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg);
}

function pipe<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg);
}

// Примеры функций
const trim = (s: string) => s.trim();
const toLowerCase = (s: string) => s.toLowerCase();
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
const removeExtraSpaces = (s: string) => s.replace(/\s+/g, ' ');

// compose — справа налево
const normalizeString1 = compose(removeExtraSpaces, capitalize, toLowerCase, trim);

// pipe — слева направо (более читаемо)
const normalizeString2 = pipe(trim, toLowerCase, capitalize, removeExtraSpaces);

console.log(normalizeString1(" hello WORLD ")); // "Hello world"
console.log(normalizeString2(" hello WORLD ")); // "Hello world"

4. Middleware паттерн:

type Middleware<T> = (context: T, next: () => void) => void;

function createMiddlewarePipeline<T>() {
const middlewares: Middleware<T>[] = [];

return {
use(middleware: Middleware<T>) {
middlewares.push(middleware);
return this;
},

execute(context: T) {
let index = 0;

const next = () => {
if (index < middlewares.length) {
const middleware = middlewares[index++];
middleware(context, next);
}
};

next();
}
};
}

// Использование
interface RequestContext {
path: string;
method: string;
user?: { id: string; role: string };
startTime: number;
}

const pipeline = createMiddlewarePipeline<RequestContext>();

pipeline
.use((ctx, next) => {
console.log(`[${ctx.method}] ${ctx.path}`);
ctx.startTime = Date.now();
next();
})
.use((ctx, next) => {
// Аутентификация
ctx.user = { id: '123', role: 'admin' };
next();
})
.use((ctx, next) => {
// Логирование
console.log(`User: ${ctx.user?.id}, Role: ${ctx.user?.role}`);
next();
});

pipeline.execute({ path: '/api/users', method: 'GET', startTime: 0 });

5. Функция tap (для отладки):

// tap — выполняет действие и возвращает исходное значение
function tap<T>(fn: (value: T) => void): (value: T) => T {
return (value: T) => {
fn(value);
return value;
};
}

// Использование в цепочке
const result = [1, 2, 3, 4, 5]
.filter(n => n % 2 === 0)
.map(tap(n => console.log(`Processing: ${n}`)))
.map(n => n * 2)
.reduce((sum, n) => sum + n, 0);
// Processing: 2
// Processing: 4
// result = 12

Сравнение подходов:

// ❌ Императивный стиль
function getActiveUserNames(users: User[]): string[] {
const result: string[] = [];

for (let i = 0; i < users.length; i++) {
if (users[i].active) {
const name = users[i].name.toUpperCase();
result.push(name);
}
}

return result.sort();
}

// ✅ Функциональный стиль с HOF
function getActiveUserNames(users: User[]): string[] {
return users
.filter(user => user.active)
.map(user => user.name.toUpperCase())
.sort();
}

Преимущества функций высшего порядка:

  • Декларативность — описываем ЧТО сделать, а не КАК
  • Переиспользование — логика изолирована в функциях
  • Композируемость — можно комбинировать простые функции
  • Тестируемость — каждая функция тестируется отдельно

Когда использовать HOF:

  • Обработка коллекций (map, filter, reduce)
  • Абстракция повторяющегося кода
  • Создание утилит (debounce, throttle, memoize)
  • Middleware и pipeline паттерны

Когда НЕ использовать:

  • Простая логика, которая читается лучше в императивном стиле
  • Критичный по производительности код (HOF могут быть медленнее из-за вызовов)
  • Когда цепочка становится слишком длинной и нечитаемой

Функции высшего порядка — основа функционального программирования в JavaScript/TypeScript, позволяющая писать выразительный и переиспользуемый код.

Вопрос 21. Что такое каррирование?

Таймкод: 00:24:01

Ответ собеседника: Правильный. Каррирование — преобразование функции с N аргументами в набор вложенных функций, каждая из которых принимает по одному аргументу и возвращает следующую функцию.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим каррирование более подробно.

Определение

Каррирование (Currying) — это техника преобразования функции с несколькими аргументами в последовательность функций, каждая из которых принимает ровно один аргумент.

f(a, b, c) → f(a)(b)(c)

Пример без и с каррированием:

// Обычная функция
function add(a: number, b: number, c: number): number {
return a + b + c;
}

add(1, 2, 3); // 6

// Каррированная версия
function curriedAdd(a: number): (b: number) => (c: number) => number {
return function(b: number): (c: number) => number {
return function(c: number): number {
return a + b + c;
};
};
}

// Стрелочные функции — более компактно
const curriedAddArrow = (a: number) => (b: number) => (c: number) =>
a + b + c;

// Использование
curriedAdd(1)(2)(3); // 6
curriedAddArrow(1)(2)(3); // 6

// Частичное применение
const add1 = curriedAdd(1); // (b) => (c) => 1 + b + c
const add1and2 = add1(2); // (c) => 1 + 2 + c
const result = add1and2(3); // 6

// Или так
const add5 = curriedAddArrow(5);
const add5and10 = add5(10);
add5and10(15); // 30

Универсальная функция каррирования:

// Каррирование для функций с двумя аргументами
function curry2<T, U, V>(
fn: (a: T, b: U) => V
): (a: T) => (b: U) => V {
return (a: T) => (b: U) => fn(a, b);
}

// Каррирование для функций с тремя аргументами
function curry3<T, U, V, W>(
fn: (a: T, b: U, c: V) => W
): (a: T) => (b: U) => (c: V) => W {
return (a: T) => (b: U) => (c: V) => fn(a, b, c);
}

// Использование
const multiply = (a: number, b: number) => a * b;
const curriedMultiply = curry2(multiply);

const triple = curriedMultiply(3);
console.log(triple(5)); // 15
console.log(triple(10)); // 30

const volume = (l: number, w: number, h: number) => l * w * h;
const curriedVolume = curry3(volume);

const boxWithHeight10 = curriedVolume(5)(2)(10); // 100

Практические примеры использования:

1. Создание специализированных функций:

// Базовая функция логирования
const log = (level: string) => (module: string) => (message: string) => {
console.log(`[${level}] [${module}] ${message}`);
};

// Создаём специализированные логгеры
const errorLog = log('ERROR');
const infoLog = log('INFO');
const debugLog = log('DEBUG');

// Логгеры для конкретных модулей
const authError = errorLog('AUTH');
const dbError = errorLog('DATABASE');
const apiInfo = infoLog('API');

// Использование
authError('User not found'); // [ERROR] [AUTH] User not found
dbError('Connection failed'); // [ERROR] [DATABASE] Connection failed
apiInfo('Request processed'); // [INFO] [API] Request processed

2. Конфигурирование запросов:

// Универсальная функция HTTP-запроса
const request = (method: string) => (url: string) => (body: unknown) =>
fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});

// Создаём специализированные функции
const get = request('GET');
const post = request('POST');
const put = request('PUT');
const del = request('DELETE');

// API-специфичные функции
const getUsers = get('/api/users');
const createUser = post('/api/users');
const updateUser = (id: string) => put(`/api/users/${id}`);
const deleteUser = (id: string) => del(`/api/users/${id}`);

// Использование
const users = await getUsers(null);
const newUser = await createUser({ name: 'John', email: 'john@example.com' });
await updateUser('123')({ name: 'John Updated' });
await deleteUser('123')(null);

3. Валидация данных:

// Каррированная функция валидации
const validate = (rule: (value: any) => boolean) => (message: string) => (value: any) => {
return rule(value) ? null : message;
};

// Правила валидации
const required = validate((v) => v !== null && v !== undefined && v !== '');
const minLength = (min: number) => validate((v) => v.length >= min);
const maxLength = (max: number) => validate((v) => v.length <= max);
const pattern = (regex: RegExp) => validate((v) => regex.test(v));

// Комбинируем валидаторы
const isRequired = required('This field is required');
const isEmail = pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)(`Invalid email format`);
const isValidPassword = minLength(8)('Password must be at least 8 characters');

// Использование
console.log(isRequired('')); // "This field is required"
console.log(isRequired('hello')); // null (валидно)
console.log(isEmail('invalid')); // "Invalid email format"
console.log(isEmail('a@b.com')); // null (валидно)

4. Математические операции:

// Каррированные математические операции
const add = (a: number) => (b: number) => a + b;
const subtract = (a: number) => (b: number) => a - b;
const multiply = (a: number) => (b: number) => a * b;
const divide = (a: number) => (b: number) => a / b;
const power = (base: number) => (exponent: number) => Math.pow(base, exponent);

// Специализированные функции
const add10 = add(10);
const subtract5 = subtract(5);
const double = multiply(2);
const halve = divide(2);
const square = power(2);
const cube = power(3);

// Использование
console.log(add10(5)); // 15
console.log(double(7)); // 14
console.log(square(4)); // 16
console.log(cube(3)); // 27

// В композиции с методами массивов
const numbers = [1, 2, 3, 4, 5];

numbers.map(add(10)); // [11, 12, 13, 14, 15]
numbers.map(double); // [2, 4, 6, 8, 10]
numbers.map(square); // [1, 4, 9, 16, 25]

5. Стилизация в React (styled-components подход):

// Каррированная функция стилизации
const style = (property: string) => (value: string) => (element: HTMLElement) => {
element.style.setProperty(property, value);
return element;
};

// Специализированные стили
const setColor = style('color');
const setBackground = style('background-color');
const setFontSize = style('font-size');

// Конкретные стили
const makeRed = setColor('red');
const makeBlueBackground = setBackground('blue');
const makeLarge = setFontSize('24px');

// Использование
// const element = makeRed(makeBlueBackground(makeLarge(document.getElementById('title')!)));

Каррирование vs Частичное применение:

// Каррирование — всегда по одному аргументу
const curriedAdd = (a: number) => (b: number) => (c: number) => a + b + c;
curriedAdd(1)(2)(3); // 6

// Частичное применение — можно несколько аргументов
function partial<T extends any[], U extends any[], R>(
fn: (...args: [...T, ...U]) => R,
...fixedArgs: T
): (...args: U) => R {
return (...args: U) => fn(...fixedArgs, ...args);
}

function addThree(a: number, b: number, c: number): number {
return a + b + c;
}

const add1 = partial(addThree, 1); // (b, c) => 1 + b + c
const add1and2 = partial(addThree, 1, 2); // (c) => 1 + 2 + c

add1(2, 3); // 6
add1and2(3); // 6

Автоматическое каррирование (Ramda-стиль):

// Универсальная функция каррирования
function curry(fn: Function): Function {
return function curried(...args: any[]) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...nextArgs: any[]) => curried.apply(this, [...args, ...nextArgs]);
};
}

// Использование
function addThree(a: number, b: number, c: number): number {
return a + b + c;
}

const curriedAdd = curry(addThree);

// Все эти вызовы работают:
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
curriedAdd(1, 2, 3); // 6

Когда использовать каррирование:

  • Нужно создавать специализированные версии функций
  • Есть повторяющийся первый аргумент
  • Пишется функциональный код с композицией
  • Нужна гибкость в порядке передачи аргументов

Когда НЕ использовать:

  • Функция всегда вызывается со всеми аргументами
  • Код становится нечитаемым
  • Команда не знакома с функциональным программированием

Каррирование — мощная техника из функционального программирования, которая особенно полезна для создания переиспользуемых и композируемых функций.

Вопрос 22. Что такое SOLID? Расскажите про принцип инверсии зависимостей (DIP).

Таймкод: 00:24:24

Ответ собеседника: Правильный. Назвал все принципы SOLID: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. Про DIP: высокоуровневые модули не должны зависеть от низкоуровневых — оба уровня должны зависеть от абстракций.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим SOLID подробнее с акцентом на DIP.

SOLID — пять принципов объектно-ориентированного проектирования:

S — Single Responsibility Principle (Принцип единственной ответственности)

Каждый класс должен иметь только одну причину для изменения.

// ❌ Нарушение — класс имеет несколько ответственностей
class User {
name: string;
email: string;

saveToDatabase() { /* ... */ }
sendEmail() { /* ... */ }
generateReport() { /* ... */ }
}

// ✅ Правильно — разделение ответственностей
class User {
constructor(
public name: string,
public email: string
) {}
}

class UserRepository {
save(user: User): void { /* ... */ }
findById(id: string): User | undefined { /* ... */ }
}

class EmailService {
send(to: string, subject: string, body: string): void { /* ... */ }
}

class ReportGenerator {
generateUserReport(user: User): string { /* ... */ }
}

O — Open/Closed Principle (Принцип открытости/закрытости)

Классы должны быть открыты для расширения, но закрыты для модификации.

// ❌ Нарушение — при добавлении нового типа нужно менять класс
class BadPaymentProcessor {
process(type: string, amount: number): void {
if (type === 'credit_card') {
// обработка кредитной карты
} else if (type === 'paypal') {
// обработка PayPal
}
// Добавление нового типа = изменение этого класса
}
}

// ✅ Правильно — расширяем через новые классы
interface PaymentMethod {
process(amount: number): Promise<boolean>;
}

class CreditCardPayment implements PaymentMethod {
async process(amount: number): Promise<boolean> {
// обработка кредитной карты
return true;
}
}

class PayPalPayment implements PaymentMethod {
async process(amount: number): Promise<boolean> {
// обработка PayPal
return true;
}
}

class CryptoPayment implements PaymentMethod {
async process(amount: number): Promise<boolean> {
// обработка криптовалюты
return true;
}
}

class PaymentProcessor {
constructor(private methods: Map<string, PaymentMethod>) {}

async process(type: string, amount: number): Promise<boolean> {
const method = this.methods.get(type);
if (!method) throw new Error(`Unknown payment type: ${type}`);
return method.process(amount);
}
}

L — Liskov Substitution Principle (Принцип подстановки Лисков)

Объекты подкласса должны быть заменяемы объектами суперкласса без нарушения корректности программы.

// ❌ Нарушение — квадрат не является корректной заменой прямоугольника
class Rectangle {
constructor(
protected width: number,
protected height: number
) {}

setWidth(width: number): void {
this.width = width;
}

setHeight(height: number): void {
this.height = height;
}

getArea(): number {
return this.width * this.height;
}
}

class Square extends Rectangle {
setWidth(width: number): void {
this.width = width;
this.height = width; // Нарушение контракта!
}

setHeight(height: number): void {
this.width = height; // Нарушение контракта!
this.height = height;
}
}

// Тест провалится для Square
function testRectangle(rect: Rectangle): void {
rect.setWidth(5);
rect.setHeight(4);
console.assert(rect.getArea() === 20); // Для Square будет 16!
}

// ✅ Правильно — не наследуем Square от Rectangle
interface Shape {
getArea(): number;
}

class Rectangle implements Shape {
constructor(
private width: number,
private height: number
) {}

getArea(): number {
return this.width * this.height;
}
}

class Square implements Shape {
constructor(private side: number) {}

getArea(): number {
return this.side * this.side;
}
}

I — Interface Segregation Principle (Принцип разделения интерфейса)

Клиенты не должны зависеть от интерфейсов, которые они не используют.

// ❌ Нарушение — один большой интерфейс
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}

class HumanWorker implements Worker {
work() { /* ... */ }
eat() { /* ... */ }
sleep() { /* ... */ }
}

class RobotWorker implements Worker {
work() { /* ... */ }
eat() { /* ... */ } // Робот не ест!
sleep() { /* ... */ } // Робот не спит!
}

// ✅ Правильно — разделённые интерфейсы
interface Workable {
work(): void;
}

interface Eatable {
eat(): void;
}

interface Sleepable {
sleep(): void;
}

class HumanWorker implements Workable, Eatable, Sleepable {
work() { /* ... */ }
eat() { /* ... */ }
sleep() { /* ... */ }
}

class RobotWorker implements Workable {
work() { /* ... */ }
// Не нужно реализовывать eat() и sleep()
}

D — Dependency Inversion Principle (Принцип инверсии зависимостей)

Высокоуровневые модули не должны зависеть от низкоуровневых. Оба типа модулей должны зависеть от абстракций.

Подробный пример DIP:

// ❌ Нарушение DIP — высокоуровневый модуль зависит от низкоуровневого
class MySQLDatabase {
connect(): void {
console.log('Connecting to MySQL...');
}

query(sql: string): any[] {
console.log(`Executing: ${sql}`);
return [];
}
}

class UserService {
private database = new MySQLDatabase(); // Прямая зависимость!

getUser(id: string): User | null {
this.database.connect();
const results = this.database.query(`SELECT * FROM users WHERE id = ${id}`);
return results[0] || null;
}
}

// Проблемы:
// 1. Нельзя легко заменить MySQL на PostgreSQL
// 2. Нельзя протестировать UserService без реальной базы
// 3. Изменения в MySQLDatabase могут сломать UserService
// ✅ Правильно — зависимость от абстракции

// 1. Определяем абстракцию (интерфейс)
interface Database {
connect(): Promise<void>;
query<T>(sql: string, params?: any[]): Promise<T[]>;
disconnect(): Promise<void>;
}

// 2. Низкоуровневые модули реализуют абстракцию
class MySQLDatabase implements Database {
async connect(): Promise<void> {
console.log('Connecting to MySQL...');
}

async query<T>(sql: string, params?: any[]): Promise<T[]> {
console.log(`MySQL executing: ${sql}`);
return [];
}

async disconnect(): Promise<void> {
console.log('Disconnecting from MySQL...');
}
}

class PostgreSQLDatabase implements Database {
async connect(): Promise<void> {
console.log('Connecting to PostgreSQL...');
}

async query<T>(sql: string, params?: any[]): Promise<T[]> {
console.log(`PostgreSQL executing: ${sql}`);
return [];
}

async disconnect(): Promise<void> {
console.log('Disconnecting from PostgreSQL...');
}
}

// 3. Высокоуровневый модуль зависит от абстракции
class UserService {
constructor(private database: Database) {} // Зависимость от интерфейса!

async getUser(id: string): Promise<User | null> {
await this.database.connect();
const results = await this.database.query<User>(
'SELECT * FROM users WHERE id = $1',
[id]
);
return results[0] || null;
}
}

// 4. Внедрение зависимостей (Dependency Injection)
const mysqlDb = new MySQLDatabase();
const userService = new UserService(mysqlDb);

// Легко подменить реализацию
const postgresDb = new PostgreSQLDatabase();
const userService2 = new UserService(postgresDb);

// Для тестов — мок
class MockDatabase implements Database {
async connect(): Promise<void> {}
async query<T>(sql: string): Promise<T[]> {
return [{ id: '1', name: 'Test User' } as unknown as T];
}
async disconnect(): Promise<void> {}
}

const testService = new UserService(new MockDatabase());

DIP в React с контекстом:

// Абстракция
interface AuthService {
login(email: string, password: string): Promise<User>;
logout(): Promise<void>;
getCurrentUser(): User | null;
}

// Реализации
class ApiAuthService implements AuthService {
async login(email: string, password: string): Promise<User> {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
return response.json();
}

async logout(): Promise<void> {
await fetch('/api/logout', { method: 'POST' });
}

getCurrentUser(): User | null {
const data = localStorage.getItem('user');
return data ? JSON.parse(data) : null;
}
}

// Контекст предоставляет абстракцию
const AuthContext = createContext<AuthService | null>(null);

function AuthProvider({ children }: { children: ReactNode }) {
const authService = useMemo(() => new ApiAuthService(), []);

return (
<AuthContext.Provider value={authService}>
{children}
</AuthContext.Provider>
);
}

// Компоненты зависят от абстракции
function LoginButton() {
const auth = useContext(AuthContext)!; // Зависимость от интерфейса

const handleLogin = async () => {
await auth.login('user@example.com', 'password');
};

return <button onClick={handleLogin}>Login</button>;
}

Преимущества DIP:

  • Тестируемость — легко подменить зависимости моками
  • Гибкость — можно менять реализации без изменения бизнес-логики
  • Слабая связанность — модули независимы друг от друга
  • Переиспользование — один и тот же код работает с разными реализациями

SOLID — это не догма, а руководство. Принципы следует применять там, где они приносят пользу, а не ради следования принципам.

Вопрос 23. Что означает акроним YAGNI?

Таймкод: 00:25:41

Ответ собеседника: Правильный. You Aren't Gonna Need It — не нужно заранее вносить улучшения, делать преждевременные оптимизации или усложнять код без необходимости.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим YAGNI более подробно.

Определение YAGNI

YAGNI (You Aren't Gonna Need It) — принцип экстремального программирования (XP), который гласит:

> «Не реализуй функциональность, пока она не понадобится»

Это означает, что не следует добавлять код «на всякий случай» или «для будущего использования».

Примеры нарушения YAGNI:

// ❌ Нарушение YAGNI — абстракции «на будущее»
abstract class BaseRepository<T> {
abstract findById(id: string): T | undefined;
abstract findAll(): T[];
abstract save(entity: T): void;
abstract delete(id: string): void;
// Методы, которые пока никто не использует:
abstract findWithRelations(id: string, relations: string[]): T | undefined;
abstract bulkInsert(entities: T[]): void;
abstract findByCriteria(criteria: Record<string, any>): T[];
abstract count(): number;
abstract exists(id: string): boolean;
abstract paginate(page: number, limit: number): T[];
}

class UserRepository extends BaseRepository<User> {
// Приходится реализовывать ВСЕ методы, даже если нужны только 2-3
findById(id: string): User | undefined { /* ... */ }
findAll(): User[] { /* ... */ }
save(entity: User): void { /* ... */ }
delete(id: string): void { /* ... */ }
findWithRelations(): User | undefined { throw new Error('Not implemented'); }
bulkInsert(): void { throw new Error('Not implemented'); }
findByCriteria(): User[] { throw new Error('Not implemented'); }
count(): number { throw new Error('Not implemented'); }
exists(): boolean { throw new Error('Not implemented'); }
paginate(): User[] { throw new Error('Not implemented'); }
}
// ✅ Правильно — только то, что нужно сейчас
class UserRepository {
findById(id: string): User | undefined {
// Реализация
}

save(user: User): void {
// Реализация
}
}

// Когда понадобится findAll — добавим тогда
// Когда понадобится paginate — добавим тогда

Ещё примеры:

// ❌ Нарушение — система плагинов для приложения с одним плагином
interface Plugin {
name: string;
version: string;
initialize(): void;
execute(): void;
destroy(): void;
}

class PluginManager {
private plugins: Plugin[] = [];

register(plugin: Plugin): void {
this.plugins.push(plugin);
plugin.initialize();
}

executeAll(): void {
for (const plugin of this.plugins) {
plugin.execute();
}
}

destroyAll(): void {
for (const plugin of this.plugins) {
plugin.destroy();
}
}
}

// Приложение использует только один плагин и никогда не будет больше

// ✅ Правильно — простая реализация
function processData(data: Data): Result {
return transform(data);
}
// ❌ Нарушение — кэширование, которое не нужно
class UserService {
private cache = new Map<string, User>();
private cacheTTL = 3600000; // 1 час
private cacheTimestamps = new Map<string, number>();

async getUser(id: string): Promise<User> {
// Проверяем кэш
const cached = this.cache.get(id);
const timestamp = this.cacheTimestamps.get(id);

if (cached && timestamp && Date.now() - timestamp < this.cacheTTL) {
return cached;
}

// Запрос к API
const user = await this.api.fetchUser(id);

// Сохраняем в кэш
this.cache.set(id, user);
this.cacheTimestamps.set(id, Date.now());

return user;
}

clearCache(): void {
this.cache.clear();
this.cacheTimestamps.clear();
}

getCacheSize(): number {
return this.cache.size;
}
}

// Приложение: внутренний админ-панель, 5 пользователей, данные меняются редко

// ✅ Правильно — без кэширования
class UserService {
async getUser(id: string): Promise<User> {
return this.api.fetchUser(id);
}
}

Когда YAGNI не применяется:

Есть случаи, когда планирование наперёд оправдано:

// ✅ Обосновано — публичный API библиотеки
// Пользователи библиотеки ожидают определённый интерфейс
export interface HttpClient {
get<T>(url: string): Promise<T>;
post<T>(url: string, data: unknown): Promise<T>;
put<T>(url: string, data: unknown): Promise<T>;
delete<T>(url: string): Promise<T>;
}

// ✅ Обосновано — критичные для безопасности части
// Лучше предусмотреть логирование заранее
class PaymentService {
async processPayment(order: Order): Promise<Result> {
this.auditLogger.log('PAYMENT_START', order);
try {
const result = await this.gateway.charge(order);
this.auditLogger.log('PAYMENT_SUCCESS', { order, result });
return result;
} catch (error) {
this.auditLogger.log('PAYMENT_ERROR', { order, error });
throw error;
}
}
}

// ✅ Обосновано — известные требования в ближайшем будущем
// Если точно знаем, что завтра добавим поддержку — можно предусмотреть
interface NotificationSender {
send(notification: Notification): Promise<void>;
}

// Знаем, что через спринт добавим SMS
class EmailNotificationSender implements NotificationSender {
async send(notification: Notification): Promise<void> {
// Отправка через email
}
}

Связь с другими принципами:

ПринципСвязь с YAGNI
KISS (Keep It Simple)Оба призывают к простоте
DRY (Don't Repeat Yourself)YAGNI может перевешивать DRY — лучше дублировать, чем создавать ненужную абстракцию
SOLIDYAGNI ограничивает чрезмерное применение SOLID

Правило трёх:

1 раз — просто напиши код
2 раза — подумай о рефакторинге
3 раза — выдели абстракцию

Антипаттерн «Февральский код» (Speculative Generality):

// Код, который никогда не используется
class ConfigurationManager {
private configs: Map<string, any> = new Map();
private validators: Map<string, (value: any) => boolean> = new Map();
private transformers: Map<string, (value: any) => any> = new Map();
private subscribers: Map<string, Array<(value: any) => void>> = new Map();

// ... 500 строк кода для управления конфигурацией

// В реальности используется только:
get(key: string): any {
return this.configs.get(key);
}
}

// Вместо этого достаточно:
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};

Преимущества следования YAGNI:

  • Меньше кода — меньше багов
  • Быстрее разработка
  • Проще поддержка
  • Код понятнее — нет «мёртвого» кода
  • Меньше тестов писать

Когда нарушать YAGNI:

  • Публичные API библиотек
  • Критичные системы (безопасность, финансы)
  • Когда стоимость изменения в будущем очень высока
  • Когда требования известны и точно будут

YAGNI — это не призыв писать плохой код, а приписка не тратить время на то, что может никогда не понадобиться.

Вопрос 24. Что такое иммутабельность и откуда она пришла?

Таймкод: 00:26:12

Ответ собеседника: Правильный. Иммутабельность — принцип, при котором ссылочные структуры данных не мутируются напрямую, а создаётся копия с изменениями. Минус — дополнительное потребление памяти. Пришёл из функционального программирования.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим иммутабельность более подробно.

Определение

Иммутабельность (Immutability) — свойство данных, при котором после создания объект нельзя изменить. Любая «модификация» создаёт новый объект, а оригинал остаётся неизменным.

Историческая справка

Иммутабельность пришла из функционального программирования (ФП), где является одним из фундаментальных принципов:

  • Lisp (1958) — первый функциональный язык
  • Haskell (1990) — чистый функциональный язык с полной иммутабельностью
  • Clojure (2007) — язык с персистентными структурами данных

В последние годы иммутабельность стала популярна в мультипарадигменных языках благодаря React, Redux и другим библиотекам.

Проблема мутаций:

// ❌ Мутации — источник багов
const user = { name: 'John', age: 25 };
const users = [user];

function updateAge(person: User) {
person.age = 26; // Мутация!
return person;
}

const updated = updateAge(user);

console.log(user.age); // 26 — изменился!
console.log(updated.age); // 26
console.log(users[0].age); // 26 — тоже изменился!
console.log(user === updated); // true — это один и тот же объект!

// Кто-то ещё работал с оригинальным user и ожидал age = 25
// Баг!

Иммутабельный подход:

// ✅ Иммутабельность — предсказуемое поведение
const user = { name: 'John', age: 25 };
const users = [user];

function updateAge(person: User): User {
return { ...person, age: 26 }; // Новый объект!
}

const updated = updateAge(user);

console.log(user.age); // 25 — не изменился!
console.log(updated.age); // 26
console.log(users[0].age); // 25 — не изменился!
console.log(user === updated); // false — разные объекты

Операции с объектами:

const state = {
user: { name: 'John', email: 'john@example.com' },
settings: { theme: 'dark', notifications: true },
todos: [
{ id: 1, text: 'Learn TypeScript', done: false },
{ id: 2, text: 'Build project', done: false }
]
};

// ✅ Добавление элемента в массив
const newState1 = {
...state,
todos: [...state.todos, { id: 3, text: 'Deploy', done: false }]
};

// ✅ Обновление элемента в массиве
const newState2 = {
...state,
todos: state.todos.map(todo =>
todo.id === 1 ? { ...todo, done: true } : todo
)
};

// ✅ Удаление элемента из массива
const newState3 = {
...state,
todos: state.todos.filter(todo => todo.id !== 2)
};

// ✅ Обложное вложенного объекта
const newState4 = {
...state,
user: {
...state.user,
name: 'Jane'
}
};

// ✅ Комбинация изменений
const newState5 = {
...state,
user: { ...state.user, name: 'Jane' },
settings: { ...state.settings, theme: 'light' },
todos: state.todos.map(todo =>
todo.id === 1 ? { ...todo, done: true } : todo
)
};

Вспомогательные функции:

// Установка значения по пути
function setPath<T extends Record<string, any>>(
obj: T,
path: string[],
value: any
): T {
if (path.length === 0) return value;

const [head, ...rest] = path;

return {
...obj,
[head]: rest.length === 0
? value
: setPath(obj[head] ?? {}, rest, value)
} as T;
}

// Использование
const state = {
app: {
user: { name: 'John', settings: { theme: 'dark' } }
}
};

const newState = setPath(state, ['app', 'user', 'settings', 'theme'], 'light');
console.log(state.app.user.settings.theme); // 'dark' — не изменился
console.log(newState.app.user.settings.theme); // 'light'

Библиотеки для иммутабельности:

Immer — иммутабельность с удобным API:

import { produce } from 'immer';

const state = {
users: [
{ id: 1, name: 'John', posts: [{ id: 1, title: 'Hello' }] },
{ id: 2, name: 'Jane', posts: [] }
]
};

// С Immer можно писать «мутабельный» код
const newState = produce(state, draft => {
// Выглядит как мутация, но это иммутабельно!
draft.users[0].posts.push({ id: 2, title: 'World' });
draft.users[1].name = 'Jane Doe';
});

console.log(state.users[0].posts.length); // 1 — не изменился
console.log(newState.users[0].posts.length); // 2

Immutable.js — персистентные структуры данных:

import { Map, List, fromJS } from 'Immutable';

const state = fromJS({
users: [
{ id: 1, name: 'John', tags: ['admin', 'user'] }
]
});

// Все операции возвращают новые объекты
const newState = state
.setIn(['users', 0, 'name'], 'Jane')
.updateIn(['users', 0, 'tags'], tags => tags.push('moderator'));

console.log(state.getIn(['users', 0, 'name'])); // 'John'
console.log(newState.getIn(['users', 0, 'name'])); // 'Jane'

Иммутабельность в React:

// React полагается на иммутабельность для определения изменений
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);

// ❌ Неправильно — React не увидит изменение
const addTodoBroken = (text: string) => {
todos.push({ id: Date.now(), text, done: false }); // Мутация!
setTodos(todos); // Та же ссылка — ре-рендера не будет
};

// ✅ Правильно — новый массив
const addTodo = (text: string) => {
setTodos([...todos, { id: Date.now(), text, done: false }]);
};

// ✅ Переключение статуса
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
};

// ✅ Удаление
const removeTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
}

Redux и иммутабельность:

// Redux reducer должен быть чистой функцией
interface State {
counter: number;
user: { name: string; loggedIn: boolean };
}

function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return { ...state, counter: state.counter + 1 };

case 'SET_USER':
return {
...state,
user: { ...state.user, name: action.payload }
};

case 'LOGOUT':
return {
...state,
user: { ...state.user, loggedIn: false }
};

default:
return state;
}
}

// Redux Toolkit использует Immer внутри
import { createSlice } from '@reduxjs/toolkit';

const slice = createSlice({
name: 'user',
initialState: { name: '', loggedIn: false },
reducers: {
// Можно писать «мутации» — Imber сделает иммутабельно
setUser(state, action) {
state.name = action.payload; // Immer создаст новый объект
}
}
});

Преимущества иммутабельности:

  • Предсказуемость — данные не изменяются неожиданно
  • Отладка — можно сохранять снимки состояния
  • Time-travel debugging — откат к предыдущим состояниям
  • Потокобезопасность — нет гонки данных (актуально для Web Workers)
  • Оптимизация — быстрое сравнение по ссылке (===)

Недостатки:

  • Память — создаются новые объекты
  • Производительность — глубокое копирование может быть дорогим
  • Сложность — глубоко вложенные структуры требуют аккуратности

Структурное разделение (Structural Sharing):

Для оптимизации памяти используются персистентные структуры данных:

Original: Modified:
A A'
/ \ / \
B C → B' C (C переиспользуется!)
/ \ / \
D E D' E' (D, E переиспользуются если не изменены)

Библиотеки как Immutable.js и Immer используют этот подход для минимизации потребления памяти.

Итог:

Иммутабельность — ключевой принцип современной фронтенд-разработки, особенно в экосистеме React/Redux. Она делает код более предсказуемым и упрощает отладку, хотя требует осознанного подхода к производительности.

Вопрос 25. Что такое архитектура в контексте разработки?

Таймкод: 00:27:16

Ответ собеседника: Правильный. Архитектура — это фундамент, на котором строится приложение, и паттерны, на которых основывается разработка кода.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим архитектуру более подробно.

Определение

Архитектура программного обеспечения — это:

  • Высокоуровневое структурирование системы
  • Организация компонентов и их взаимодействия
  • Принципы и решения, которые сложно изменить впоследствии
  • Ответ на вопрос «Как система организована в целом?»

Уровни архитектуры:

1. Системная архитектура — как части системы взаимодействуют между собой

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Frontend │ ←→ │ Backend │ ←→ │ Database │
│ (React) │ │ (Node.js) │ │ (PostgreSQL)│
└─────────────┘ └─────────────┘ └─────────────┘
↕ ↕
┌─────────────┐ ┌─────────────┐
│ CDN │ │ Redis │
└─────────────┘ └─────────────┘

2. Архитектура приложения — как организован код внутри одного сервиса

src/
├── features/ # Функциональные модули
│ ├── auth/
│ ├── users/
│ └── products/
├── shared/ # Переиспользуемый код
│ ├── ui/
│ ├── api/
│ └── utils/
├── app/ # Инициализация приложения
│ ├── providers/
│ └── routing/
└── pages/ # Страницы

3. Архитектура кода — паттерны и принципы на уровне модулей и компонентов

Популярные архитектурные паттерны:

MVC (Model-View-Controller):

Model (данные) ←→ Controller (логика) ↔ View (отображение)

Пример в фронтенде:
- Model: состояние приложения (Redux store)
- Controller: action creators, thunks
- View: React-компоненты

MVVM (Model-View-ViewModel):

Model ←→ ViewModel (состояние + команды) ↔ View (привязки данных)

Пример:
- Model: API клиент
- ViewModel: React хуки с состоянием
- View: JSX компоненты

Flux / Redux (однонаправленный поток данных):

Action → Dispatcher → Store → View
↑ |
└──────────────────────────────────┘
// Redux реализация Flux
interface State {
users: User[];
loading: boolean;
error: string | null;
}

// Action
const fetchUsers = () => async (dispatch: Dispatch) => {
dispatch({ type: 'FETCH_USERS_START' });
try {
const users = await api.getUsers();
dispatch({ type: 'FETCH_USERS_SUCCESS', payload: users });
} catch (error) {
dispatch({ type: 'FETCH_USERS_ERROR', payload: error.message });
}
};

// Reducer (чистая функция)
function usersReducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_USERS_START':
return { ...state, loading: true, error: null };
case 'FETCH_USERS_SUCCESS':
return { ...state, loading: false, users: action.payload };
case 'FETCH_USERS_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}

Feature-Sliced Design (FSD) — современный подход:

src/
├── app/ # Инициализация приложения
│ ├── styles/
│ ├── providers/
│ └── index.tsx
├── pages/ # Страницы (роутинг)
│ ├── home/
│ ├── profile/
│ └── products/
├── widgets/ # Самостоятельные блоки UI
│ ├── header/
│ ├── sidebar/
│ └── product-card/
├── features/ # Пользовательские сценарии
│ ├── auth/
│ │ ├── ui/
│ │ ├── model/
│ │ └── api/
│ └── add-to-cart/
├── entities/ # Бизнес-сущности
│ ├── user/
│ ├── product/
│ └── order/
└── shared/ # Переиспользуемый код
├── ui/
├── api/
├── lib/
└── config/

Принципы хорошей архитектуры:

1. Разделение ответственности (Separation of Concerns):

// ❌ Всё в одном месте
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);

useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);

if (!user) return <Spinner />;

return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={() => {
fetch(`/api/users/${userId}`, { method: 'DELETE' });
}}>Delete</button>
</div>
);
}

// ✅ Разделение на слои
// api/user.ts
export const userApi = {
getById: (id: string) => fetch(`/api/users/${id}`).then(r => r.json()),
delete: (id: string) => fetch(`/api/users/${id}`, { method: 'DELETE' })
};

// hooks/useUser.ts
export function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
userApi.getById(userId).then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);

return { user, loading };
}

// components/UserProfile.tsx
function UserProfile({ userId }: { userId: string }) {
const { user, loading } = useUser(userId);

if (loading) return <Spinner />;
if (!user) return null;

return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<DeleteButton userId={userId} />
</div>
);
}

2. Инверсия зависимостей:

// Компоненты зависят от абстракций, а не от конкретных реализаций
interface Theme {
colors: {
primary: string;
secondary: string;
background: string;
};
}

interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

// Компонент работает с любой реализацией Theme
function ThemedButton() {
const { theme } = useContext(ThemeContext)!;

return (
<button style={{ background: theme.colors.primary }}>
Click me
</button>
);
}

3. Принцип единой ответственности для модулей:

// Каждый модуль имеет одну причину для изменения

// ✅ Отдельные модули
// userService.ts — работа с данными пользователя
// userValidation.ts — валидация данных пользователя
// userPermissions.ts — проверка прав пользователя
// userUI.tsx — отображение пользователя

Монолит vs Микросервисы:

Монолит:
┌─────────────────────────────┐
│ Single Process │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Auth │ │Users│ │Order│ │
│ └─────┘ └─────┘ └─────┘ │
│ ┌─────────────────────┐ │
│ │ Shared Database │ │
│ └─────────────────────┘ │
└─────────────────────────────┘

Микросервисы:
┌──────┐ ┌──────┐ ┌──────┐
│ Auth │ │Users │ │Order │
│ Svc │ │ Svc │ │ Svc │
└──┬───┘ └──┬───┘ └──┬───┘
│ │ │
┌──┴───┐ ┌──┴───┐ ┌──┴───┐
│ DB │ │ DB │ │ DB │
└──────┘ └──────┘ └──────┘

Критерии хорошей архитектуры:

  • Масштабируемость — можно добавлять функциональность без переписывания
  • Тестируемость — компоненты можно тестировать изолированно
  • Поддерживаемость — код понятен и легко изменяется
  • Переиспользуемость — компоненты можно использовать в разных местах
  • Предсказуемость — поведение системы очевидно

Антипаттерны архитектуры:

  • Big Ball of Mud — отсутствие структуры, всё связано со всем
  • Lasagna Architecture — слишком много слоёв абстракции
  • Golden Hammer — использование одного паттерна для всех задач
  • Vendor Lock-in — жёсткая привязка к конкретному поставщику

Архитектура — это не просто «структура папок», а набор решений о том, как система организована, как её части взаимодействуют и как она будет развиваться. Хорошая архитектура делает систему гибкой и поддерживаемой.

Вопрос 26. Какие архитектурные решения нужны для быстрой работы приложения по всей стране и совместного редактирования документов?

Таймкод: 00:27:45

Ответ собеседника: Неполный. Для быстрой загрузки ресурсов — CDN со статикой. Для бэкенда — распределённые сервера в разных точках. Предположил, что база данных должна быть единой. Упомянул проблему пинга и ограничения скоростью света. Не смог предложить решение для конфликтов при одновременном редактировании.

Правильный ответ:

Это комплексная архитектурная задача. Рассмотрим все аспекты.

1. Быстрая работа по всей стране

CDN (Content Delivery Network):

Пользователь в Москве Пользователь в Владивостоке
↓ ↓
CDN Москва CDN Владивосток
(5-20ms) (5-20ms)
↓ ↓
┌─────────────────────────────────────────┐
│ Origin Server │
│ (Центральный дата-центр) │
└─────────────────────────────────────────┘

CDN кэширует статические ресурсы (JS, CSS, изображения) на ближайшем к пользователю сервере.

Географически распределённые серверы:

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Region: EU │ │ Region: RU │ │ Region: AP │
│ Frankfurt │ │ Moscow │ │ Singapore │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────┼────────────────┘

┌────────┴────────┐
│ Load Balancer │
│ (GeoDNS/Anycast)│
└─────────────────┘

2. Распределённые базы данных

Идея «единой базы данных» не масштабируется. Решения:

Master-Slave репликация:

┌─────────────┐
│ Master │ ← Запись
│ (Moscow) │
└──────┬──────┘
│ Репликация
┌───┴───┐
↓ ↓
┌──────┐ ┌──────┐
│Slave │ │Slave │ ← Чтение
│ EU │ │ AP │
└──────┘ └──────┘

Multi-Master репликация:

┌──────────┐ ┌──────────┐ ┌──────────┐
│ Master │ ←→ │ Master │ ←→ │ Master │
│ Moscow │ │ Frankfurt│ │ Singapore│
└──────────┘ └──────────┘ └──────────┘
↕ ↕ ↕
Запись + Запись + Запись +
Чтение Чтение Чтение

Шардирование (Sharding):

┌─────────────────────────────────────────┐
│ Application Layer │
└────────────────┬────────────────────────┘

┌────────────┼────────────┐
↓ ↓ ↓
┌────────┐ ┌────────┐ ┌────────┐
│Shard 1 │ │Shard 2 │ │Shard 3 │
│Users │ │Users │ │Users │
│A-H │ │I-P │ │Q-Z │
└────────┘ └────────┘ └────────┘

3. Решение конфликтов при совместном редактировании

Это ключевая проблема. Основные подходы:

Last-Write-Wins (LWW):

Пользователь A: "Привет" → timestamp: 100
Пользователь B: "Мир" → timestamp: 101

Результат: "Мир" (последняя запись победила)

Проблема: Данные A потеряны!

Operational Transformation (OT):

Используется в Google Docs. Алгоритм преобразует операции так, чтобы они давали одинаковый результат независимо от порядка применения.

Исходный текст: "ABC"

Пользователь A: insert('X', 0) → "XABC"
Пользователь B: delete(2) → "AB" (удаляет 'C')

После трансформации:
A затем B: insert('X', 0) → "XABC", затем delete(2) → "XAB"
B затем A: delete(2) → "AB", затем insert('X', 0) → "XAB"

Результат одинаковый: "XAB"

CRDT (Conflict-free Replicated Data Types):

Математически гарантирует конвергенцию без координации.

// Пример: G-Counter (Grow-only Counter)
class GCounter {
private counters: Map<string, number> = new Map();

increment(nodeId: string): void {
const current = this.counters.get(nodeId) || 0;
this.counters.set(nodeId, current + 1);
}

getValue(): number {
return Array.from(this.counters.values()).reduce((a, b) => a + b, 0);
}

merge(other: GCounter): void {
for (const [nodeId, value] of other.counters) {
const current = this.counters.get(nodeId) || 0;
this.counters.set(nodeId, Math.max(current, value));
}
}
}

// Использование
const counterA = new GCounter();
const counterB = new GCounter();

counterA.increment('A'); // A: 1
counterA.increment('A'); // A: 2
counterB.increment('B'); // B: 1

counterA.merge(counterB);
counterB.merge(counterA);

console.log(counterA.getValue()); // 3
console.log(counterB.getValue()); // 3 — одинаково!

CRDT для текста (Yjs, Automerge):

import * as Y from 'yjs';

// Создаём общий документ
const doc = new Y.Doc();

// Текстовый тип
const text = doc.getText('content');

// Пользователь A
text.insert(0, 'Привет');

// Пользователь B (одновременно)
text.insert(0, 'Мир');

// После синхронизации оба видят одинаковый результат
// Порядок зависит от внутренних правил CRDT

4. Архитектура совместного редактирования:

┌──────────┐ ┌──────────┐
│ Client A │ │ Client B │
│ (CRDT) │ │ (CRDT) │
└────┬─────┘ └────┬─────┘
│ │
↓ ↓
┌─────────────────────────────────┐
│ WebSocket Server Cluster │
│ (с учётом региона) │
└────────────────┬────────────────┘

┌────────┴────────┐
↓ ↓
┌──────────────┐ ┌──────────────┐
│ CRDT │ ←→│ CRDT │
│ Server 1 │ │ Server 2 │
│ (Moscow) │ │ (Frankfurt)│
└──────────────┘ └──────────────┘
↓ ↓
┌──────────────┐ ┌──────────────┐
│ Database │ ←→│ Database │
│ (Master) │ │ (Replica) │
└──────────────┘ └──────────────┘

5. Теорема CAP:

Распределённая система может гаранцировать только два из трёх свойств:

  • Consistency — все узлы видят одинаковые данные
  • Availability — каждый запрос получает ответ
  • Partition tolerance — система работает при потере связи между узлами
Consistency
/\
/ \
/ \
/ CP \
/________\
/ \
/ CA \
/______________\
/ \
/ AP \
/____________________\
Availability ---- Partition tolerance

Для совместного редактирования обычно выбирают AP
(доступность + устойчивость к разделению)
с eventual consistency (конечной согласованностью)

6. Практические решения:

СервисТехнология
Google DocsOperational Transformation
FigmaCRDT
NotionCRDT
ConfluenceOT + LWW
VS Code Live ShareCustom OT

7. Оптимизация задержек:

// Предсказание и оптимистичные обновления
function useOptimisticUpdate() {
const [state, setState] = useState(initialState);

const update = async (newData: Data) => {
// 1. Мгновенно обновляем UI (оптимистично)
setState(newData);

try {
// 2. Отправляем на сервер
await api.update(newData);
} catch (error) {
// 3. При ошибке — откатываем
setState(initialState);
showError('Не удалось сохранить');
}
};

return { state, update };
}

Итоговая архитектура:

Пользователь → CDN → Regional Load Balancer →
→ Regional API Servers → WebSocket Cluster →
→ CRDT Sync Layer → Distributed Database (Multi-Master)

Ключевые решения:

  • CDN для статики
  • Региональные серверы для API
  • CRDT для совместного редактирования
  • Eventual consistency для данных
  • Оптимистичные обновления для UX

Вопрос 27. Как должен вести себя фронтенд при одновременном редактировании данных и как синхронизировать распределённые БД?

Таймкод: 00:31:26

Ответ собеседния: Неполный. Предложил простейший вариант — лоадеры и обработку ошибок. На вопрос о синхронизации распределённых баз данных не смог предложить конкретного решения. Признал, что это задача бэкенда.

Правильный ответ:

Рассмотрим поведение фронтенда и синхронизацию БД подробно.

1. Поведение фронтенда при конфликтах

Перезагрузка страницы — плохой UX. Лучшие подходы:

Оптимистичные обновления с разрешением конфликтов:

interface Document {
id: string;
content: string;
version: number;
lastModified: string;
}

function useDocumentEditor(docId: string) {
const [document, setDocument] = useState<Document | null>(null);
const [conflict, setConflict] = useState<Conflict | null>(null);

const updateContent = async (newContent: string) => {
if (!document) return;

// 1. Оптимистично обновляем UI
const optimisticDoc = { ...document, content: newContent };
setDocument(optimisticDoc);

try {
// 2. Отправляем на сервер с версией для проверки конфликтов
const response = await api.updateDocument(docId, {
content: newContent,
baseVersion: document.version
});

// 3. Успех — обновляем версию
setDocument(response.document);
} catch (error) {
if (error.status === 409) {
// 4. Конфликт! Показываем UI для разрешения
setConflict({
local: optimisticDoc,
server: error.serverDocument
});
} else {
// 5. Другая ошибка — откатываем
setDocument(document);
showError('Не удалось сохранить');
}
}
};

const resolveConflict = (resolution: 'local' | 'server' | 'merged', mergedContent?: string) => {
if (resolution === 'local') {
// Принудительно отправляем локальную версию
forceUpdate(document!);
} else if (resolution === 'server') {
// Принимаем серверную версию
setDocument(conflict!.server);
} else if (resolution === 'merged' && mergedContent) {
// Отправляем объединённую версию
updateContent(mergedContent);
}
setConflict(null);
};

return { document, updateContent, conflict, resolveConflict };
}

UI для разрешения конфликтов:

function ConflictResolver({ conflict, onResolve }: ConflictProps) {
return (
<Modal title="Конфликт редактирования">
<p>Документ был изменён другим пользователем.</p>

<div className="diff-view">
<div className="version">
<h3>Ваша версия</h3>
<pre>{conflict.local.content}</pre>
</div>
<div className="version">
<h3>Серверная версия</h3>
<pre>{conflict.server.content}</pre>
</div>
</div>

<div className="actions">
<button onClick={() => onResolve('local')}>
Сохранить мою версию
</button>
<button onClick={() => onResolve('server')}>
Принять серверную версию
</button>
<button onClick={() => onResolve('merged')}>
Объединить вручную
</button>
</div>
</Modal>
);
}

2. Индикация совместного редактирования:

function CollaborativeEditor({ docId }: { docId: string }) {
const [collaborators, setCollaborators] = useState<Collaborator[]>([]);

useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/docs/${docId}`);

ws.onmessage = (event) => {
const message = JSON.parse(event.data);

switch (message.type) {
case 'CURSOR_POSITION':
updateCollaboratorCursor(message.userId, message.position);
break;
case 'USER_JOINED':
setCollaborators(prev => [...prev, message.user]);
break;
case 'USER_LEFT':
setCollaborators(prev => prev.filter(c => c.id !== message.userId));
break;
}
};

return () => ws.close();
}, [docId]);

return (
<div className="editor-container">
<Toolbar>
{collaborators.map(user => (
<UserAvatar
key={user.id}
user={user}
color={user.cursorColor}
/>
))}
</Toolbar>
<Editor>
{collaborators.map(user => (
<RemoteCursor
key={user.id}
position={user.cursorPosition}
color={user.cursorColor}
name={user.name}
/>
))}
</Editor>
</div>
);
}

3. Синхронизация распределённых баз данных

Синхронизация через лог операций:

┌─────────────┐ ┌─────────────┐
│ DB: EU │ │ DB: US │
└──────┬──────┘ └──────┬──────┘
│ │
↓ ↓
┌─────────────┐ ┌─────────────┐
│ Operation │ │ Operation │
│ Log │ │ Log │
│ │ │ │
│ 1. INSERT │ ─────────────────→ │ 1. INSERT │
│ 2. UPDATE │ ─────────────────→ │ 2. UPDATE │
│ 3. DELETE │ ←───────────────── │ 3. INSERT │
└─────────────┘ └─────────────┘

На практике — векторные часы:

class VectorClock {
private clock: Map<string, number> = new Map();

increment(nodeId: string): void {
const current = this.clock.get(nodeId) || 0;
this.clock.set(nodeId, current + 1);
}

merge(other: VectorClock): void {
for (const [nodeId, timestamp] of other.clock) {
const current = this.clock.get(nodeId) || 0;
this.clock.set(nodeId, Math.max(current, timestamp));
}
}

compare(other: VectorClock): 'before' | 'after' | 'concurrent' {
let hasGreater = false;
let hasLess = false;

const allNodes = new Set([...this.clock.keys(), ...other.clock.keys()]);

for (const nodeId of allNodes) {
const thisTime = this.clock.get(nodeId) || 0;
const otherTime = other.clock.get(nodeId) || 0;

if (thisTime > otherTime) hasGreater = true;
if (thisTime < otherTime) hasLess = true;
}

if (hasGreater && !hasLess) return 'after';
if (hasLess && !hasGreater) return 'before';
if (!hasGreater && !hasLess) return 'equal';
return 'concurrent'; // Конфликт!
}
}

// Использование
class DistributedRecord<T> {
constructor(
public data: T,
public vectorClock: VectorClock,
public nodeId: string
) {}

update(newData: T): DistributedRecord<T> {
const newClock = new VectorClock();
// Копируем текущие часы
for (const [id, time] of this.vectorClock.clock) {
newClock.clock.set(id, time);
}
newClock.increment(this.nodeId);

return new DistributedRecord(newData, newClock, this.nodeId);
}

merge(other: DistributedRecord<T>): DistributedRecord<T> {
const comparison = this.vectorClock.compare(other.vectorClock);

switch (comparison) {
case 'before':
return other; // other новее
case 'after':
return this; // this новее
case 'concurrent':
// Конфликт! Нужна стратегия разрешения
return this.resolveConflict(other);
}
}

private resolveConflict(other: DistributedRecord<T>): DistributedRecord<T> {
// Стратегия: Last-Write-Wins по таймстампу
// Или: объединение данных
// Или: запрос к пользователю
return this.timestamp > other.timestamp ? this : other;
}
}

4. Паттерны синхронизации:

Event Sourcing:

// Храним события, а не состояние
interface Event {
id: string;
type: 'USER_CREATED' | 'USER_UPDATED' | 'USER_DELETED';
payload: any;
timestamp: string;
nodeId: string;
}

class EventStore {
private events: Event[] = [];

append(event: Event): void {
this.events.push(event);
}

getEvents(aggregateId: string): Event[] {
return this.events.filter(e => e.payload.id === aggregateId);
}

// Состояние восстанавливается из событий
getState(aggregateId: string): any {
return this.getEvents(aggregateId).reduce((state, event) => {
return this.applyEvent(state, event);
}, {});
}

private applyEvent(state: any, event: Event): any {
switch (event.type) {
case 'USER_CREATED':
return { ...event.payload };
case 'USER_UPDATED':
return { ...state, ...event.payload };
case 'USER_DELETED':
return null;
default:
return state;
}
}
}

// Синхронизация между узлами
function syncEventStores(local: EventStore, remote: EventStore): void {
const localEvents = local.getAllEvents();
const remoteEvents = remote.getAllEvents();

// Находим события, которых нет на другом узле
const missingOnRemote = localEvents.filter(
e => !remoteEvents.find(re => re.id === e.id)
);
const missingOnLocal = remoteEvents.filter(
e => !localEvents.find(le => le.id === e.id)
);

// Обмениваемся
missingOnRemote.forEach(e => remote.append(e));
missingOnLocal.forEach(e => local.append(e));
}

5. Реализации в базах данных:

База данныхПодход к распределению
CockroachDBMulti-Region, Spanner-like
YugabyteDBRaft consensus, Geo-partitioning
CassandraTunable consistency, Hinted Handoff
MongoDBReplica Sets, Sharding
PostgreSQLLogical Replication, Citus

6. Стратегии разрешения конфликтов:

enum ConflictStrategy {
LAST_WRITE_WINS = 'lww',
FIRST_WRITE_WINS = 'fww',
MERGE = 'merge',
CUSTOM = 'custom'
}

class ConflictResolver<T> {
constructor(private strategy: ConflictStrategy) {}

resolve(local: T, remote: T, localTime: number, remoteTime: number): T {
switch (this.strategy) {
case ConflictStrategy.LAST_WRITE_WINS:
return remoteTime > localTime ? remote : local;

case ConflictStrategy.FIRST_WRITE_WINS:
return localTime < remoteTime ? local : remote;

case ConflictStrategy.MERGE:
return this.merge(local, remote);

case ConflictStrategy.CUSTOM:
throw new Error('Требуется ручное разрешение');
}
}

private merge(local: T, remote: T): T {
// Для объектов — deep merge
if (typeof local === 'object' && typeof remote === 'object') {
return { ...local, ...remote };
}
// Для массивов — объединение уникальных элементов
if (Array.isArray(local) && Array.isArray(remote)) {
return [...new Set([...local, ...remote])] as unknown as T;
}
// Для примитивов — выбираем большее
return local > remote ? local : remote;
}
}

7. Практические рекомендации для фронтенда:

// Состояние синхронизации
interface SyncState {
status: 'synced' | 'syncing' | 'offline' | 'conflict';
lastSyncedAt: Date | null;
pendingChanges: number;
}

function useSyncStatus() {
const [syncState, setSyncState] = useState<SyncState>({
status: 'synced',
lastSyncedAt: new Date(),
pendingChanges: 0
});

// Индикатор синхронизации в UI
return {
...syncState,
showIndicator: syncState.status !== 'synced',
indicatorColor: {
synced: 'green',
syncing: 'yellow',
offline: 'red',
conflict: 'orange'
}[syncState.status]
};
}

// Использование в компоненте
function SyncIndicator() {
const { status, showIndicator, indicatorColor, pendingChanges } = useSyncStatus();

if (!showIndicator) return null;

return (
<div className={`sync-indicator ${indicatorColor}`}>
{status === 'syncing' && `Синхронизация... (${pendingChanges})`}
{status === 'offline' && 'Нет соединения'}
{status === 'conflict' && 'Конфликт данных'}
</div>
);
}

Итог:

Фронтенд должен:

  • Использовать оптимистичные обновления
  • Показывать индикаторы синхронизации
  • Предоставлять UI для разрешения конфликтов
  • Работать offline с последующей синхронизацией

Синхронизация БД:

  • CRDT для автоматического разрешения
  • Event Sourcing для аудита и воспроизведения
  • Векторные часы для определения порядка событий
  • Настраиваемые стратегии разрешения конфликтов

Вопрос 28. Что такое MVC и MVVM? В чём их отличия?

Таймкод: 00:33:59

Ответ собеседника: Правильный. MVC — Model (бизнес-логика), View (интерфейс), Controller (связующее звено, обрабатывает действия пользователя). Также упомянул MVP и MVVM. На фронте применялись только на уровне теории, знает что Redux/MobX основаны на этих паттернах.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим паттерны подробнее с примерами.

MVC (Model-View-Controller)

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Model │ ←→ │ View │ ←── │ Controller │
│ (Данные) │ │ (Отображение)│ │ (Логика) │
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↑
└──────────── Уведомления ─────────────┘

Поток данных:

  1. Пользователь взаимодействует с View
  2. View передаёт действие Controller
  3. Controller обновляет Model
  4. Model уведомляет View об изменениях
  5. View обновляется

Реализация MVC:

// Model — данные и бизнес-логика
class UserModel {
private users: User[] = [];
private listeners: Array<() => void> = [];

addUser(user: User): void {
this.users.push(user);
this.notify();
}

getUsers(): User[] {
return [...this.users];
}

subscribe(listener: () => void): () => void {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}

private notify(): void {
this.listeners.forEach(listener => listener());
}
}

// View — отображение
class UserListView {
constructor(private container: HTMLElement) {}

render(users: User[]): void {
this.container.innerHTML = `
<ul>
${users.map(user => `
<li>
<span>${user.name}</span>
<button data-id="${user.id}" class="delete-btn">Delete</button>
</li>
`).join('')}
</ul>
`;

// Привязываем обработчики
this.container.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = (e.target as HTMLElement).dataset.id!;
this.onDelete(id);
});
});
}

onDelete: (id: string) => void = () => {};
}

// Controller — связующее звено
class UserController {
constructor(
private model: UserModel,
private view: UserListView
) {
// Подписываемся на изменения модели
this.model.subscribe(() => {
this.view.render(this.model.getUsers());
});

// Привязываем обработчик удаления
this.view.onDelete = (id: string) => {
this.deleteUser(id);
};
}

addUser(name: string): void {
const user: User = {
id: crypto.randomUUID(),
name,
email: `${name.toLowerCase()}@example.com`
};
this.model.addUser(user);
}

deleteUser(id: string): void {
// Логика удаления
this.model.removeUser(id);
}

init(): void {
this.view.render(this.model.getUsers());
}
}

// Использование
const model = new UserModel();
const view = new UserListView(document.getElementById('app')!);
const controller = new UserController(model, view);
controller.init();

MVVM (Model-View-ViewModel)

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Model │ ←→ │ ViewModel │ ←→ │ View │
│ (Данные) │ │ (Состояние)│ │ (Отображение)│
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↑
└──── Data Binding ──┘

Ключевое отличие — Data Binding:

// Vue.js — классический MVVM
const { createApp, ref, computed, watch } = Vue;

// Model
interface Todo {
id: string;
text: string;
completed: boolean;
}

class TodoModel {
async fetchTodos(): Promise<Todo[]> {
const response = await fetch('/api/todos');
return response.json();
}

async saveTodo(todo: Todo): Promise<void> {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(todo)
});
}
}

// ViewModel
function useTodos() {
const todos = ref<Todo[]>([]);
const loading = ref(false);
const filter = ref<'all' | 'active' | 'completed'>('all');

// Вычисляемое свойство
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed);
case 'completed':
return todos.value.filter(t => t.completed);
default:
return todos.value;
}
});

const stats = computed(() => ({
total: todos.value.length,
completed: todos.value.filter(t => t.completed).length,
active: todos.value.filter(t => !t.completed).length
}));

// Методы
async function loadTodos() {
loading.value = true;
todos.value = await new TodoModel().fetchTodos();
loading.value = false;
}

function addTodo(text: string) {
todos.value.push({
id: crypto.randomUUID(),
text,
completed: false
});
}

function toggleTodo(id: string) {
const todo = todos.value.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}

function removeTodo(id: string) {
todos.value = todos.value.filter(t => t.id !== id);
}

return {
todos,
filteredTodos,
loading,
filter,
stats,
loadTodos,
addTodo,
toggleTodo,
removeTodo
};
}

// View (Vue Template) — автоматически обновляется при изменении ViewModel
const template = `
<div>
<div v-if="loading">Loading...</div>

<div v-else>
<input
v-model="newTodoText"
@keyup.enter="addTodo(newTodoText); newTodoText = ''"
placeholder="Add todo..."
/>

<div>
<button
v-for="f in ['all', 'active', 'completed']"
:key="f"
:class="{ active: filter === f }"
@click="filter = f"
>
{{ f }} ({{ stats[f === 'all' ? 'total' : f] }})
</button>
</div>

<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
/>
<span :class="{ completed: todo.completed }">
{{ todo.text }}
</span>
<button @click="removeTodo(todo.id)">×</button>
</li>
</ul>
</div>
</div>
`;

MVP (Model-View-Presenter)

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Model │ ←── │ Presenter │ ←→ │ View │
│ (Данные) │ │ (Логика) │ │ (Пассивное)│
└─────────────┘ └─────────────┘ └─────────────┘

MVP — пассивный View:

// View интерфейс
interface IUserView {
showUsers(users: User[]): void;
showError(message: string): void;
showLoading(): void;
hideLoading(): void;
getUserName(): string;
}

// Presenter
class UserPresenter {
constructor(
private model: UserModel,
private view: IUserView
) {}

async loadUsers(): Promise<void> {
this.view.showLoading();
try {
const users = await this.model.fetchUsers();
this.view.showUsers(users);
} catch (error) {
this.view.showError('Failed to load users');
} finally {
this.view.hideLoading();
}
}

async addUser(): Promise<void> {
const name = this.view.getUserName();
if (!name) {
this.view.showError('Name is required');
return;
}

try {
await this.model.addUser({ name });
await this.loadUsers(); // Перезагружаем список
} catch (error) {
this.view.showError('Failed to add user');
}
}
}

// View реализация (React)
function UserView() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [name, setName] = useState('');

const presenter = useMemo(() => {
return new UserPresenter(new UserModel(), {
showUsers: setUsers,
showError: setError,
showLoading: () => setLoading(true),
hideLoading: () => setLoading(false),
getUserName: () => name
});
}, [name]);

useEffect(() => {
presenter.loadUsers();
}, []);

return (
<div>
{loading && <Spinner />}
{error && <Error message={error} />}

<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => presenter.addUser()}>Add</button>

<UserList users={users} />
</div>
);
}

Сравнение паттернов:

АспектMVCMVPMVVM
View активностьАктивныйПассивныйПассивный
Связь View-ModelПрямаяЧерез PresenterЧерез Data Binding
ТестируемостьСредняяВысокаяВысокая
СложностьНизкаяСредняяСредняя
ФреймворкиExpress, RailsAndroid (old)Vue, Angular, WPF

React и паттерны:

// React с Redux — ближе к Flux/MVC
// Model → Redux Store
// View → React Components
// Controller → Action Creators + Reducers

// React с hooks — ближе к MVVM
// Model → API + State
// View → JSX
// ViewModel → Custom Hooks

// Пример MVVM подхода в React
function useUserViewModel() {
// State (ViewModel)
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');

// Computed (ViewModel)
const filteredUsers = useMemo(() => {
return users.filter(u =>
u.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [users, searchQuery]);

// Actions (ViewModel)
const loadUsers = useCallback(async () => {
setLoading(true);
const data = await userApi.fetchAll();
setUsers(data);
setLoading(false);
}, []);

const addUser = useCallback(async (name: string) => {
const newUser = await userApi.create({ name });
setUsers(prev => [...prev, newUser]);
}, []);

return {
users: filteredUsers,
loading,
searchQuery,
setSearchQuery,
loadUsers,
addUser
};
}

// View — чистая функция от ViewModel
function UserPage() {
const vm = useUserViewModel();

return (
<div>
<input
value={vm.searchQuery}
onChange={e => vm.setSearchQuery(e.target.value)}
/>
<button onClick={vm.loadUsers}>Load</button>
{vm.loading ? <Spinner /> : <UserList users={vm.users} />}
</div>
);
}

Когда что использовать:

  • MVC — серверный рендеринг, простые приложения
  • MVP — Android, нужна высокая тестируемость
  • MVVM — Vue, Angular, WPF, богатый клиентский UI
  • Flux/Redux — сложные React-приложения с предсказуемым состоянием

В современной фронтенд-разработке чистые паттерны используются редко — чаще их гибриды и адаптации.

Вопрос 29. Что такое CQRS (Command Query Responsibility Segregation)?

Таймкод: 00:35:27

Ответ собеседника: Неправильный. Не слышал о CQRS, не смог ответить на вопрос.

Правильный ответ:

Определение CQRS

CQRS (Command Query Responsibility Segregation) — паттерн, который разделяет операции чтения и записи данные на отдельные модели.

Traditional Approach:
┌─────────────────────────────────────┐
│ Single Model │
│ ┌─────────────┬─────────────┐ │
│ │ Read │ Write │ │
│ └─────────────┴─────────────┘ │
└─────────────────────────────────────┘

CQRS Approach:
┌─────────────────┐ ┌─────────────────┐
│ Read Model │ │ Write Model │
│ (Queries) │ │ (Commands) │
└────────┬────────┘ └────────┬────────┘
│ │
↓ ↓
┌─────────────────┐ ┌─────────────────┐
│ Read DB │ │ Write DB │
│ (Optimized │ │ (Normalized) │
│ for reads) │ │ │
└─────────────────┘ └─────────────────┘

Принцип Command Query Separation (CQS):

CQRS является расширением принципа CQS Бертрада Мейера:

  • Command — изменяет состояние, не возвращает значение
  • Query — возвращает значение, не изменяет состояние
// ❌ Нарушение CQS — метод и изменяет, и возвращает
class OrderService {
// И создаёт заказ, И возвращает его
createOrder(data: OrderData): Order {
const order = this.repository.save(data);
this.eventBus.publish('order.created', order);
return order; // Нарушение!
}
}

// ✅ Правильно — разделение
class OrderService {
// Command — изменяет состояние, не возвращает данные
async createOrder(data: OrderData): Promise<void> {
const order = Order.create(data);
await this.orderRepository.save(order);
await this.eventBus.publish(new OrderCreatedEvent(order.id));
}

// Query — только читает, не изменяет
async getOrder(id: string): Promise<OrderDto | null> {
return this.orderReadRepository.findById(id);
}
}

Полная реализация CQRS:

// ============ Commands (Write Side) ============

// Command — намерение изменить состояние
interface Command {
type: string;
}

class CreateOrderCommand implements Command {
type = 'CREATE_ORDER';

constructor(
public readonly userId: string,
public readonly items: OrderItem[],
public readonly shippingAddress: Address
) {}
}

class CancelOrderCommand implements Command {
type = 'CANCEL_ORDER';

constructor(public readonly orderId: string) {}
}

// Command Handler — обработчик команды
interface CommandHandler<T extends Command> {
handle(command: T): Promise<void>;
}

class CreateOrderHandler implements CommandHandler<CreateOrderCommand> {
constructor(
private orderRepository: OrderWriteRepository,
private eventBus: EventBus
) {}

async handle(command: CreateOrderCommand): Promise<void> {
// Валидация
if (command.items.length === 0) {
throw new ValidationError('Order must have at least one item');
}

// Бизнес-логика
const order = Order.create({
userId: command.userId,
items: command.items,
shippingAddress: command.shippingAddress
});

// Сохранение
await this.orderRepository.save(order);

// Публикация событий
for (const event of order.domainEvents) {
await this.eventBus.publish(event);
}
}
}

// ============ Queries (Read Side) ============

// Query — запрос на получение данных
interface Query<T> {
type: string;
}

class GetOrderQuery implements Query<OrderDto> {
type = 'GET_ORDER';

constructor(public readonly orderId: string) {}
}

class GetUserOrdersQuery implements Query<PagedResult<OrderSummary>> {
type = 'GET_USER_ORDERS';

constructor(
public readonly userId: string,
public readonly page: number,
public readonly limit: number
) {}
}

// Query Handler
interface QueryHandler<T extends Query<R>, R> {
handle(query: T): Promise<R>;
}

class GetOrderHandler implements QueryHandler<GetOrderQuery, OrderDto | null> {
constructor(private readDb: ReadDatabase) {}

async handle(query: GetOrderQuery): Promise<OrderDto | null> {
// Чтение из оптимизированной для чтения базы
return this.readDb.orders.findOne({ id: query.orderId });
}
}

// ============ Read Model (оптимизирован для чтения) ============

// Write Model — нормализованная, для бизнес-логики
interface OrderWriteModel {
id: string;
userId: string;
status: OrderStatus;
items: OrderItem[];
shippingAddress: Address;
createdAt: Date;
updatedAt: Date;
}

// Read Model — денормализованная, для быстрого чтения
interface OrderReadModel {
id: string;
userId: string;
userName: string; // Денормализовано из User
status: OrderStatus;
statusText: string; // Вычислено
items: OrderItemReadModel[];
totalAmount: number; // Вычислено
itemCount: number; // Вычислено
shippingAddress: string; // Форматировано
createdAt: string; // Форматировано
}

interface OrderItemReadModel {
productId: string;
productName: string; // Денормализовано из Product
productImage: string; // Денормализовано из Product
quantity: number;
price: number;
subtotal: number; // Вычислено
}

Синхронизация Read и Write моделей:

// Event Handler — обновляет Read Model при изменениях
class OrderEventHandler {
constructor(private readDb: ReadDatabase) {}

async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
// Получаем данные из Write DB
const order = await this.writeDb.orders.findById(event.orderId);
const user = await this.writeDb.users.findById(order.userId);

// Денормализуем для Read DB
const readModel: OrderReadModel = {
id: order.id,
userId: order.userId,
userName: `${user.firstName} ${user.lastName}`,
status: order.status,
statusText: getStatusText(order.status),
items: order.items.map(item => ({
productId: item.productId,
productName: item.product.name,
productImage: item.product.imageUrl,
quantity: item.quantity,
price: item.price,
subtotal: item.quantity * item.price
})),
totalAmount: order.items.reduce(
(sum, item) => sum + item.quantity * item.price, 0
),
itemCount: order.items.length,
shippingAddress: formatAddress(order.shippingAddress),
createdAt: order.createdAt.toISOString()
};

await this.readDb.orders.upsert(readModel);
}

async handleOrderCancelled(event: OrderCancelledEvent): Promise<void> {
await this.readDb.orders.update(event.orderId, {
status: 'cancelled',
statusText: 'Cancelled'
});
}
}

CQRS с Event Sourcing:

┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Command │ → │ Aggregate │ → │ Event │
│ Handler │ │ (Domain) │ │ Store │
└─────────────┘ └─────────────┘ └──────┬──────┘


┌─────────────┐
│ Projector │
│ (Read Model)│
└──────┬──────┘


┌─────────────┐
│ Read DB │
│ (Query) │
└─────────────┘
// Event Store — хранит все события
class EventStore {
private events: Map<string, DomainEvent[]> = new Map();

async append(aggregateId: string, events: DomainEvent[]): Promise<void> {
const existing = this.events.get(aggregateId) || [];
this.events.set(aggregateId, [...existing, ...events]);
}

async getEvents(aggregateId: string): Promise<DomainEvent[]> {
return this.events.get(aggregateId) || [];
}
}

// Aggregate — восстанавливает состояние из событий
class Order {
private domainEvents: DomainEvent[] = [];

// Восстановление из собыдов
static rehydrate(events: DomainEvent[]): Order {
const order = new Order();
for (const event of events) {
order.apply(event);
}
return order;
}

// Применение команды
static create(data: CreateOrderData): Order {
const order = new Order();
order.apply(new OrderCreatedEvent({
orderId: generateId(),
userId: data.userId,
items: data.items
}));
return order;
}

cancel(): void {
if (this.status === 'shipped') {
throw new DomainError('Cannot cancel shipped order');
}
this.apply(new OrderCancelledEvent({ orderId: this.id }));
}

// Применение события к состоянию
private apply(event: DomainEvent): void {
switch (event.type) {
case 'ORDER_CREATED':
this.id = event.data.orderId;
this.userId = event.data.userId;
this.items = event.data.items;
this.status = 'pending';
break;
case 'ORDER_CANCELLED':
this.status = 'cancelled';
break;
}
this.domainEvents.push(event);
}
}

Преимущества CQRS:

  • Масштабируемость — можно масштабировать чтение и запись отдельно
  • Оптимизация — разные модели оптимизированы под разные задачи
  • Гибкость — можно менять модели независимо
  • Безопасность — чёткое разделение ответственности

Недостатки CQRS:

  • Сложность — больше кода, больше компонентов
  • Eventual Consistency — данные могут быть не синхронны
  • Избыточность — не нужен для простых приложений

Когда использовать CQRS:

  • Высоконагруженные системы
  • Сложная бизнес-логика
  • Разные паттерны чтения и записи
  • Event Sourcing

Когда НЕ использовать:

  • CRUD-приложения
  • Простые домены
  • Небольшие команды

CQRS — мощный паттерн для сложных систем, но он добавляет сложность и должен применяться обоснованно.

Вопрос 30. Какие архитектурные стили протоколов поверх HTTP вы знаете?

Таймкод: 00:36:17

Ответ собеседника: Правильный. Назвал REST, GraphQL, WebSocket, SSE (Server-Sent Events).

Правильный ответ:

Ответ собеседника корректен. Рассмотрим каждый стиль подробнее.

1. REST (Representational State Transfer)

Самый распространённый архитектурный стиль для API.

Принципы REST:
- Client-Server архитектура
- Stateless (нет состояния на сервере)
- Cacheable (ответы можно кэшировать)
- Uniform Interface (единый интерфейс)
- Layered System (слоистая архитектура)
// REST API пример
// GET /api/users — список пользователей
// GET /api/users/:id — один пользователь
// POST /api/users — создать пользователя
// PUT /api/users/:id — обновить полностью
// PATCH /api/users/:id — обновить частично
// DELETE /api/users/:id — удалить

// Express пример
const express = require('express');
const app = express();

// Ресурс: /api/users
app.get('/api/users', async (req, res) => {
const { page = 1, limit = 20, search } = req.query;

const users = await userService.findAll({
page: Number(page),
limit: Number(limit),
search: search as string
});

res.json({
data: users,
meta: { page, limit, total: await userService.count() }
});
});

app.get('/api/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ data: user });
});

app.post('/api/users', async (req, res) => {
const user = await userService.create(req.body);
res.status(201).json({ data: user });
});

app.patch('/api/users/:id', async (req, res) => {
const user = await userService.update(req.params.id, req.body);
res.json({ data: user });
});

app.delete('/api/users/:id', async (req, res) => {
await userService.delete(req.params.id);
res.status(204).send();
});

// Вложенные ресурсы
app.get('/api/users/:id/orders', async (req, res) => {
const orders = await orderService.findByUserId(req.params.id);
res.json({ data: orders });
});

Проблемы REST:

# Over-fetching — получаем лишние данные
GET /api/users/1
# Ответ: { id, name, email, phone, address, createdAt, updatedAt, ... }
# Нужно только: { name, email }

# Under-fetching — нужно делать много запросов
GET /api/users/1 # Получаем пользователя
GET /api/users/1/orders # Получаем заказы
GET /api/users/1/friends # Получаем друзей
# Три запроса вместо одного!

2. GraphQL

Разработан Facebook. Позволяет клиенту запрашивать только нужные данные.

┌─────────────┐ ┌─────────────┐
│ Client │ ←─────→ │ GraphQL │
│ │ Query │ Server │
└─────────────┘ └─────────────┘
// GraphQL Schema
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
friends: [User!]!
}

type Order {
id: ID!
total: Float!
items: [OrderItem!]!
createdAt: String!
}

type OrderItem {
product: Product!
quantity: Int!
price: Float!
}

type Product {
id: ID!
name: String!
price: Float!
}

type Query {
user(id: ID!): User
users(page: Int, limit: Int): [User!]!
}

type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}

input CreateUserInput {
name: String!
email: String!
}
`;

// Resolvers
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.findById(id);
},
users: async (_, { page = 1, limit = 20 }, { dataSources }) => {
return dataSources.userAPI.findAll({ page, limit });
}
},
User: {
orders: async (user, _, { dataSources }) => {
return dataSources.orderAPI.findByUserId(user.id);
},
friends: async (user, _, { dataSources }) => {
return dataSources.userAPI.findFriends(user.id);
}
}
};
// Клиентские запросы (Apollo Client)

// Запрос только нужных данных
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
`;

// Запрос с вложенными данными
const GET_USER_WITH_ORDERS = gql`
query GetUserWithOrders($id: ID!) {
user(id: $id) {
name
email
orders {
id
total
items {
product {
name
price
}
quantity
}
}
}
}
`;

// Использование в React
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useQuery(GET_USER_WITH_ORDERS, {
variables: { id: userId }
});

if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;

return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
<OrdersList orders={data.user.orders} />
</div>
);
}

Сравнение REST и GraphQL:

АспектRESTGraphQL
ЭндпоинтыМного (ресурсы)Один (/graphql)
ДанныеФиксированный форматКлиент выбирает
Over-fetchingДаНет
Under-fetchingДа (много запросов)Нет
КэшированиеПростое (HTTP)Сложное
ФильтрацияЧерез query paramsВстроено
Кривая обученияНизкаяВысокая

3. WebSocket

Дуплексный канал связи через одно соединение.

HTTP: Request → Response (закрыто)
WebSocket: ←→ ←→ ←→ ←→ (постоянное соединение)
// Сервер (ws library)
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

const clients = new Map<string, WebSocket>();

wss.on('connection', (ws, req) => {
const userId = authenticate(req);
clients.set(userId, ws);

ws.on('message', (data) => {
const message = JSON.parse(data.toString());
handleMessage(userId, message);
});

ws.on('close', () => {
clients.delete(userId);
});
});

// Отправка сообщения пользователю
function sendToUser(userId: string, message: any): void {
const client = clients.get(userId);
if (client?.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
}

// Отправка всем
function broadcast(message: any): void {
for (const client of clients.values()) {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
}
}
// Клиент
class ChatClient {
private ws: WebSocket;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;

constructor(private url: string) {
this.ws = this.connect();
}

private connect(): WebSocket {
const ws = new WebSocket(this.url);

ws.onopen = () => {
console.log('Connected');
this.reconnectAttempts = 0;
};

ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};

ws.onclose = () => {
this.reconnect();
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

return ws;
}

private reconnect(): void {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000);
setTimeout(() => {
this.ws = this.connect();
}, delay);
}
}

send(message: any): void {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}

private handleMessage(message: any): void {
switch (message.type) {
case 'NEW_MESSAGE':
console.log('New message:', message.data);
break;
case 'USER_TYPING':
console.log(`${message.user} is typing...`);
break;
}
}
}

4. SSE (Server-Sent Events)

Однонаправленный поток данных от сервера к клиенту.

Client ←──── Server
←──── (постоянно)
←────
// Сервер (Express)
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// Отправляем начальное событие
res.write(`event: connected\ndata: ${JSON.stringify({ time: Date.now() })}\n\n`);

// Периодическая отправка данных
const intervalId = setInterval(() => {
const data = {
time: Date.now(),
value: Math.random()
};
res.write(`event: update\ndata: ${JSON.stringify(data)}\n\n`);
}, 1000);

// Обработка подписок
eventBus.on('notification', (notification) => {
res.write(`event: notification\ndata: ${JSON.stringify(notification)}\n\n`);
});

// Очистка при отключении
req.on('close', () => {
clearInterval(intervalId);
});
});
// Клиент
class SSEClient {
private eventSource: EventSource;

constructor(private url: string) {
this.eventSource = new EventSource(url);
this.setupListeners();
}

private setupListeners(): void {
this.eventSource.addEventListener('connected', (event) => {
const data = JSON.parse(event.data);
console.log('Connected:', data);
});

this.eventSource.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
console.log('Update:', data);
});

this.eventSource.addEventListener('notification', (event) => {
const data = JSON.parse(event.data);
showNotification(data);
});

this.eventSource.onerror = () => {
console.error('SSE error, reconnecting...');
};
}

close(): void {
this.eventSource.close();
}
}

5. gRPC

Высокопроизводительный RPC фреймворк от Google.

┌─────────────┐ HTTP/2 + Protobuf ┌─────────────┐
│ Client │ ←─────────────────────→ │ Server │
│ │ Бинарный протокол │ │
└─────────────┘ └─────────────┘
// user.proto
syntax = "proto3";

service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser (CreateUserRequest) returns (User);
rpc WatchUsers (WatchUsersRequest) returns (stream User);
}

message GetUserRequest {
string id = 1;
}

message ListUsersRequest {
int32 page = 1;
int32 limit = 2;
}

message ListUsersResponse {
repeated User users = 1;
int32 total = 2;
}

message User {
string id = 1;
string name = 2;
string email = 3;
}

message CreateUserRequest {
string name = 1;
string email = 2;
}

message WatchUsersRequest {}

6. Webhooks

Обратные вызовы — сервер уведомляет клиента при событиях.

┌─────────────┐ ┌─────────────┐
│ Server │ ──POST /webhook────────→│ Client │
│ │ (при событии) │ Endpoint │
└─────────────┘ └─────────────┘
// Отправка webhook
async function sendWebhook(url: string, event: string, data: any): Promise<void> {
const payload = {
event,
data,
timestamp: new Date().toISOString()
};

const signature = createSignature(payload, WEBHOOK_SECRET);

await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Event': event
},
body: JSON.stringify(payload)
});
}

// Приём webhook
app.post('/webhooks/payment', async (req, res) => {
const signature = req.headers['x-webhook-signature'];

if (!verifySignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}

const { event, data } = req.body;

switch (event) {
case 'payment.success':
await handlePaymentSuccess(data);
break;
case 'payment.failed':
await handlePaymentFailed(data);
break;
}

res.status(200).json({ received: true });
});

Сравнение протоколов:

ПротоколНаправлениеСлучай использования
RESTRequest-ResponseCRUD API
GraphQLRequest-ResponseГибкие запросы
WebSocketDuplexЧат, игры, торги
SSEServer → ClientУведомления, ленты
gRPCDuplexМикросервисы
WebhooksServer → ClientИнтеграции, события

Выбор протокола зависит от задачи: REST для простых API, GraphQL для гибких запросов, WebSocket для real-time, SSE для однонаправленных потоков.

Вопрос 31. Чем HTTP код 201 отличается от 200?

Таймкод: 00:36:56

Ответ собеседника: Неправильный. Перепутал коды: вместо 201 (Created) начал описывать 202 (Accepted). Код 200 верно определил как успешный запрос.

Правильный ответ:

HTTP 200 OK vs 201 Created

200 OK — стандартный успешный ответ. Запрос выполнен успешно, тело ответа содержит запрошенные данные.

201 Created — ресурс успешно создан. Используется после POST-запросов. В заголовке Location указывается URL созданного ресурса.

// Express примеры

// 200 — успешное получение данных
app.get('/api/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// 200 подставляется автоматически
res.json({ data: user });
});

// 200 — успешное обновление
app.put('/api/users/:id', async (req, res) => {
const user = await userService.update(req.params.id, req.body);
res.json({ data: user }); // 200 OK
});

// 201 — ресурс создан
app.post('/api/users', async (req, res) => {
const user = await userService.create(req.body);

res.status(201)
.location(`/api/users/${user.id}`) // Заголовок Location
.json({ data: user });
});

// 201 — создание вложенного ресурса
app.post('/api/users/:id/orders', async (req, res) => {
const order = await orderService.create(req.params.id, req.body);

res.status(201)
.location(`/api/orders/${order.id}`)
.json({ data: order });
});

Другие важные 2xx коды:

// 202 Accepted — запрос принят в обработку, но ещё не выполнен
app.post('/api/reports/generate', async (req, res) => {
const jobId = await reportService.startGeneration(req.body);

res.status(202)
.location(`/api/jobs/${jobId}`)
.json({
message: 'Report generation started',
jobId,
statusUrl: `/api/jobs/${jobId}`
});
});

// 204 No Content — успешно, но тело ответа пустое
app.delete('/api/users/:id', async (req, res) => {
await userService.delete(req.params.id);
res.status(204).send(); // Нет тела ответа
});

// 204 — успешное обновление без возврата данных
app.patch('/api/users/:id/last-seen', async (req, res) => {
await userService.updateLastSeen(req.params.id);
res.status(204).send();
});

Сравнительная таблица:

КодЗначениеТело ответаLocation headerКогда использовать
200OKДаНетGET, PUT, PATCH, DELETE
201CreatedДаДаPOST (создание ресурса)
202AcceptedОпциональноОпциональноДолгие операции
204No ContentНетНетDELETE, PATCH без ответа

Практические примеры:

// Создание пользователя — 201
app.post('/api/users', async (req, res) => {
const { name, email } = req.body;

// Валидация
if (!name || !email) {
return res.status(400).json({ error: 'Name and email required' });
}

// Проверка уникальности
const existing = await userService.findByEmail(email);
if (existing) {
return res.status(409).json({ error: 'Email already exists' });
}

// Создание
const user = await userService.create({ name, email });

// 201 с Location
res.status(201)
.location(`/api/users/${user.id}`)
.json({
data: {
id: user.id,
name: user.name,
email: user.email
}
});
});

// Получение пользователя — 200
app.get('/api/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);

if (!user) {
return res.status(404).json({ error: 'User not found' });
}

res.json({ data: user }); // 200 OK
});

// Удаление — 204
app.delete('/api/users/:id', async (req, res) => {
const deleted = await userService.delete(req.params.id);

if (!deleted) {
return res.status(404).json({ error: 'User not found' });
}

res.status(204).send(); // Нет тела
});

Важные нюансы:

  • 201 всегда должен содержать заголовок Location с URL созданного ресурса
  • 200 — универсальный успешный ответ, подходит для большинства операций
  • 204 — когда клиенту не нужны данные в ответе (например, после удаления)
  • 202 — для асинхронных операций, когда результат будет позже

Правильное использование HTTP-кодов делает API предсказуемым и соответствует стандартам REST.

Вопрос 32. В чём разница между HTTP кодами 401 и 403?

Таймкод: 00:37:55

Ответ собеседника: Правильный. 401 — пользователь не авторизован. 403 — пользователь авторизован, но не имеет доступа к ресурсу.

Правильный ответ:

Ответ собеседника корректен. Рассмотрим подробнее.

401 Unauthorized — аутентификация не пройдена или отсутствует. Сервер не знает, кто вы.

403 Forbidden — аутентификация пройдена, но доступ запрещён. Сервер знает, кто вы, но вам сюда нельзя.

// Express middleware примеры

// Проверка аутентификации — 401
function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(' ')[1];

if (!token) {
return res.status(401).json({
error: 'Authentication required',
message: 'Please provide a valid token'
});
}

try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({
error: 'Invalid token',
message: 'Token is expired or invalid'
});
}
}

// Проверка авторизации — 403
function authorize(roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}

if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Forbidden',
message: `Required roles: ${roles.join(', ')}`
});
}

next();
};
}

// Использование
app.get('/api/profile', authenticate, (req, res) => {
res.json({ data: req.user });
});

app.get('/api/admin/users', authenticate, authorize(['admin']), (req, res) => {
res.json({ data: [] });
});

Сценарии использования:

401 — когда пользователь:
- Не предоставил токен
- Предоставил невалидный токен
- Токен истёк

403 — когда пользователь:
- Пытается получить доступ к чужому ресурсу
- Не имеет нужной роли
- Заблокирован
- Не имеет прав на операцию

Примеры с фронтенда:

// Обработка 401 — редирект на логин
async function fetchWithAuth(url: string) {
const response = await fetch(url, {
headers: { Authorization: `Bearer ${getToken()}` }
});

if (response.status === 401) {
// Токен невалидный или истёк
clearToken();
redirectToLogin();
return;
}

if (response.status === 403) {
// Доступ запрещён — показываем сообщение
showNotification('You do not have permission to access this resource');
return;
}

return response.json();
}

Вопрос 33. Какие есть способы получения результата долгого (асинхронного) запроса с фронтенда?

Таймкод: 00:38:17

Ответ собеседника: Правильный. Назвал long polling с ограничением браузера ~2 минуты, а также альтернативы: WebSocket, обычный polling и SSE.

Правильный ответ:

Ответ собеседника полный и корректный. Дополним деталями.

1. Polling (обычный опрос)

Клиент периодически опрашивает сервер.

// Простой polling
async function pollStatus(jobId: string): Promise<Result> {
const maxAttempts = 60;
const interval = 2000; // 2 секунды

for (let i = 0; i < maxAttempts; i++) {
const response = await fetch(`/api/jobs/${jobId}`);
const data = await response.json();

if (data.status === 'completed') {
return data.result;
}

if (data.status === 'failed') {
throw new Error(data.error);
}

await sleep(interval);
}

throw new Error('Polling timeout');
}

// С экспоненциальным backoff
async function pollWithBackoff(jobId: string): Promise<Result> {
let delay = 1000;
const maxDelay = 30000;

while (true) {
const data = await fetchJobStatus(jobId);

if (data.status === 'completed') return data.result;
if (data.status === 'failed') throw new Error(data.error);

await sleep(delay);
delay = Math.min(delay * 2, maxDelay);
}
}

2. Long Polling

Сервер держит соединение, пока не появятся данные или не истечет таймаут.

// Сервер (Express)
app.get('/api/events', (req, res) => {
const timeout = 30000; // 30 секунд

const handler = (event: any) => {
res.json({ data: event });
};

eventBus.once('update', handler);

// Таймаут — возвращаем пустой ответ
setTimeout(() => {
eventBus.off('update', handler);
if (!res.headersSent) {
res.status(204).send();
}
}, timeout);

// Клиент отключился
req.on('close', () => {
eventBus.off('update', handler);
});
});

// Клиент
async function longPoll(): Promise<void> {
try {
const response = await fetch('/api/events');

if (response.status === 200) {
const data = await response.json();
handleEvent(data);
}

// Сразу делаем новый запрос
longPoll();
} catch (error) {
// При ошибке ждём и повторяем
await sleep(5000);
longPoll();
}
}

3. Server-Sent Events (SSE)

Однонаправленный поток от сервера к клиенту.

// Сервер
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

const sendEvent = (data: any) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};

// Отправляем heartbeat каждые 30 секунд
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);

eventBus.on('update', sendEvent);

req.on('close', () => {
clearInterval(heartbeat);
eventBus.off('update', sendEvent);
});
});

// Клиент
const eventSource = new EventSource('/api/stream');

eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};

eventSource.onerror = () => {
console.log('Connection lost, reconnecting...');
};

4. WebSocket

Дуплексное соединение для обоих направлений.

// Клиент
class JobTracker {
private ws: WebSocket;

constructor(private jobId: string) {
this.ws = new WebSocket(`wss://api.example.com/ws`);
this.setupListeners();
}

private setupListeners(): void {
this.ws.onopen = () => {
this.ws.send(JSON.stringify({
type: 'subscribe',
jobId: this.jobId
}));
};

this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);

switch (message.type) {
case 'progress':
this.updateProgress(message.percent);
break;
case 'completed':
this.handleResult(message.result);
break;
case 'error':
this.handleError(message.error);
break;
}
};
}

close(): void {
this.ws.close();
}
}

Сравнение подходов:

МетодЗадержкаНагрузка на серверСложностьНаправление
PollingВысокая (интервал)Высокая (много запросов)НизкаяClient → Server
Long PollingНизкаяСредняяСредняяClient → Server
SSEНизкаяНизкаяСредняяServer → Client
WebSocketМинимальнаяНизкаяВысокаяDuplex

Рекомендации по выбору:

  • Polling — простые задачи, редкие обновления
  • Long Polling — нужна низкая задержка, но нельзя использовать WebSocket
  • SSE — сервер отправляет обновления (уведомления, ленты)
  • WebSocket — интерактивное взаимодействие (чаты, игры, торги)

Вопрос 33. Какие есть способы получения результата долгого (асинхронного) запроса с фронтенда?

Таймкод: 00:38:17

Ответ собеседника: Правильный. Назвал long polling с ограничением браузера ~2 минуты, а также альтернативы: WebSocket, обычный polling и SSE.

Правильный ответ:

Ответ собеседника полный и корректный. Дополним деталями.

1. Polling (обычный опрос)

Клиент периодически опрашивает сервер.

// Простой polling
async function pollStatus(jobId: string): Promise<Result> {
const maxAttempts = 60;
const interval = 2000; // 2 секунды

for (let i = 0; i < maxAttempts; i++) {
const response = await fetch(`/api/jobs/${jobId}`);
const data = await response.json();

if (data.status === 'completed') {
return data.result;
}

if (data.status === 'failed') {
throw new Error(data.error);
}

await sleep(interval);
}

throw new Error('Polling timeout');
}

// С экспоненциальным backoff
async function pollWithBackoff(jobId: string): Promise<Result> {
let delay = 1000;
const maxDelay = 30000;

while (true) {
const data = await fetchJobStatus(jobId);

if (data.status === 'completed') return data.result;
if (data.status === 'failed') throw new Error(data.error);

await sleep(delay);
delay = Math.min(delay * 2, maxDelay);
}
}

2. Long Polling

Сервер держит соединение, пока не появятся данные или не истечет таймаут.

// Сервер (Express)
app.get('/api/events', (req, res) => {
const timeout = 30000; // 30 секунд

const handler = (event: any) => {
res.json({ data: event });
};

eventBus.once('update', handler);

// Таймаут — возвращаем пустой ответ
setTimeout(() => {
eventBus.off('update', handler);
if (!res.headersSent) {
res.status(204).send();
}
}, timeout);

// Клиент отключился
req.on('close', () => {
eventBus.off('update', handler);
});
});

// Клиент
async function longPoll(): Promise<void> {
try {
const response = await fetch('/api/events');

if (response.status === 200) {
const data = await response.json();
handleEvent(data);
}

// Сразу делаем новый запрос
longPoll();
} catch (error) {
// При ошибке ждём и повторяем
await sleep(5000);
longPoll();
}
}

3. Server-Sent Events (SSE)

Однонаправленный поток от сервера к клиенту.

// Сервер
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

const sendEvent = (data: any) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};

// Отправляем heartbeat каждые 30 секунд
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);

eventBus.on('update', sendEvent);

req.on('close', () => {
clearInterval(heartbeat);
eventBus.off('update', sendEvent);
});
});

// Клиент
const eventSource = new EventSource('/api/stream');

eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};

eventSource.onerror = () => {
console.log('Connection lost, reconnecting...');
};

4. WebSocket

Дуплексное соединение для обоих направлений.

// Клиент
class JobTracker {
private ws: WebSocket;

constructor(private jobId: string) {
this.ws = new WebSocket(`wss://api.example.com/ws`);
this.setupListeners();
}

private setupListeners(): void {
this.ws.onopen = () => {
this.ws.send(JSON.stringify({
type: 'subscribe',
jobId: this.jobId
}));
};

this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);

switch (message.type) {
case 'progress':
this.updateProgress(message.percent);
break;
case 'completed':
this.handleResult(message.result);
break;
case 'error':
this.handleError(message.error);
break;
}
};
}

close(): void {
this.ws.close();
}
}

Сравнение подходов:

МетодЗадержкаНагрузка на серверСложностьНаправление
PollingВысокая (интервал)Высокая (много запросов)НизкаяClient → Server
Long PollingНизкаяСредняяСредняяClient → Server
SSEНизкаяНизкаяСредняяServer → Client
WebSocketМинимальнаяНизкаяВысокаяDuplex

Рекомендации по выбору:

  • Polling — простые задачи, редкие обновления
  • Long Polling — нужна низкая задержка, но нельзя использовать WebSocket
  • SSE — сервер отправляет обновления (уведомления, ленты)
  • WebSocket — интерактивное взаимодействие (чаты, игры, торги)

Вопрос 34. Что такое идемпотентные и неидемпотентные операции? Какие HTTP-методы являются идемпотентными?

Таймкод: 00:40:21

Ответ собеседника: Правильный. Назвал GET и DELETE как идемпотентные методы, уточнил про DELETE по ID.

Правильный ответ:

Ответ собеседника корректен, но неполный. Дополним.

Идемпотентность — свойство операции, при котором многократное выполнение даёт тот же результат, что и однократное.

f(f(x)) = f(x)

GET /users/1 → 200 (всегда один и тот же результат)
DELETE /users/1 → 200 (первый раз), 404 (повторно) — всё равно идемпотентен
PUT /users/1 → 200 (всегда перезаписывает полностью)

Идемпотентные HTTP-методы:

МетодИдемпотентенПричина
GETДаНе изменяет состояние
PUTДаПолная перезапись ресурса
DELETEДаПосле удаления ресурс уже не существует
HEADДаКак GET, но без тела
OPTIONSДаНе изменяет состояние

Неидемпотентные HTTP-методы:

МетодИдемпотентенПричина
POSTНетСоздаёт новый ресурс каждый раз
PATCHНет*Зависит от реализации
// POST — НЕ идемпотентен
// Каждый вызов создаёт нового пользователя
POST /api/users
{ "name": "John" }
// Ответ 1: 201 { "id": 1, "name": "John" }
// Ответ 2: 201 { "id": 2, "name": "John" } — другой ID!

// PUT — идемпотентен
// Всегда перезаписывает ресурс полностью
PUT /api/users/1
{ "name": "John Updated" }
// Ответ 1: 200 { "id": 1, "name": "John Updated" }
// Ответ 2: 200 { "id": 1, "name": "John Updated" } — тот же результат

// DELETE — идемпотентен
DELETE /api/users/1
// Ответ 1: 200 — удалено
// Ответ 2: 404 — уже удалено (состояние не изменилось)

PATCH — особый случай:

// PATCH НЕ идемпотентен (инкремент)
PATCH /api/users/1
{ "balance": { "increment": 100 } }
// Вызов 1: balance = 100
// Вызов 2: balance = 200 — результат изменился!

// PATCH идемпотентен (присвоение)
PATCH /api/users/1
{ "name": "John" }
// Вызов 1: name = "John"
// Вызов 2: name = "John" — тот же результат

Зачем нужна идемпотентность:

1. Повторные запросы при таймаутах
Клиент не получил ответ → повторяет запрос → безопасно

2. Очереди сообщений
Kafka, RabbitMQ — at-least-once delivery
Сообщение может доставиться дважды

3. Retry-логика
Автоматические повторы при ошибках сети

Как обеспечить идемпотентность для POST:

// Идемпотентный ключ (Idempotency-Key)
app.post('/api/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];

if (idempotencyKey) {
// Проверяем, был ли уже такой запрос
const existing = await redis.get(`idempotency:${idempotencyKey}`);
if (existing) {
return res.status(200).json(JSON.parse(existing));
}
}

// Создаём заказ
const order = await orderService.create(req.body);

// Сохраняем результат
if (idempotencyKey) {
await redis.setex(
`idempotency:${idempotencyKey}`,
86400, // 24 часа
JSON.stringify(order)
);
}

res.status(201).json(order);
});
// Клиент отправляет с ключом
const idempotencyKey = crypto.randomUUID();

fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({ items: [1, 2, 3] })
});

Вопрос 34. Что такое идемпотентные и неидемпотентные операции? Какие HTTP-методы являются идемпотентными?

Таймкод: 00:40:21

Ответ собеседника: Правильный. Назвал GET и DELETE как идемпотентные методы, уточнил про DELETE по ID.

Правильный ответ:

Ответ собеседника корректен, но неполный. Дополним.

Идемпотентность — свойство операции, при котором многократное выполнение даёт тот же результат, что и однократное.

f(f(x)) = f(x)

GET /users/1 → 200 (всегда один и тот же результат)
DELETE /users/1 → 200 (первый раз), 404 (повторно) — всё равно идемпотентен
PUT /users/1 → 200 (всегда перезаписывает полностью)

Идемпотентные HTTP-методы:

МетодИдемпотентенПричина
GETДаНе изменяет состояние
PUTДаПолная перезапись ресурса
DELETEДаПосле удаления ресурс уже не существует
HEADДаКак GET, но без тела
OPTIONSДаНе изменяет состояние

Неидемпотентные HTTP-методы:

МетодИдемпотентенПричина
POSTНетСоздаёт новый ресурс каждый раз
PATCHНет*Зависит от реализации
// POST — НЕ идемпотентен
// Каждый вызов создаёт нового пользователя
POST /api/users
{ "name": "John" }
// Ответ 1: 201 { "id": 1, "name": "John" }
// Ответ 2: 201 { "id": 2, "name": "John" } — другой ID!

// PUT — идемпотентен
// Всегда перезаписывает ресурс полностью
PUT /api/users/1
{ "name": "John Updated" }
// Ответ 1: 200 { "id": 1, "name": "John Updated" }
// Ответ 2: 200 { "id": 1, "name": "John Updated" } — тот же результат

// DELETE — идемпотентен
DELETE /api/users/1
// Ответ 1: 200 — удалено
// Ответ 2: 404 — уже удалено (состояние не изменилось)

PATCH — особый случай:

// PATCH НЕ идемпотентен (инкремент)
PATCH /api/users/1
{ "balance": { "increment": 100 } }
// Вызов 1: balance = 100
// Вызов 2: balance = 200 — результат изменился!

// PATCH идемпотентен (присвоение)
PATCH /api/users/1
{ "name": "John" }
// Вызов 1: name = "John"
// Вызов 2: name = "John" — тот же результат

Зачем нужна идемпотентность:

1. Повторные запросы при таймаутах
Клиент не получил ответ → повторяет запрос → безопасно

2. Очереди сообщений
Kafka, RabbitMQ — at-least-once delivery
Сообщение может доставиться дважды

3. Retry-логика
Автоматические повторы при ошибках сети

Как обеспечить идемпотентность для POST:

// Идемпотентный ключ (Idempotency-Key)
app.post('/api/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];

if (idempotencyKey) {
// Проверяем, был ли уже такой запрос
const existing = await redis.get(`idempotency:${idempotencyKey}`);
if (existing) {
return res.status(200).json(JSON.parse(existing));
}
}

// Создаём заказ
const order = await orderService.create(req.body);

// Сохраняем результат
if (idempotencyKey) {
await redis.setex(
`idempotency:${idempotencyKey}`,
86400, // 24 часа
JSON.stringify(order)
);
}

res.status(201).json(order);
});
// Клиент отправляет с ключом
const idempotencyKey = crypto.randomUUID();

fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({ items: [1, 2, 3] })
});

Вопрос 34. Что такое идемпотентные и неидемпотентные операции? Какие HTTP-методы являются идемпотентными?

Таймкод: 00:40:21

Ответ собеседника: Правильный. Назвал GET и DELETE как идемпотентные методы, уточнил про DELETE по ID.

Правильный ответ:

Ответ собеседника корректен, но неполный. Дополним.

Идемпотентность — свойство операции, при котором многократное выполнение даёт тот же результат, что и однократное.

f(f(x)) = f(x)

GET /users/1 → 200 (всегда один и тот же результат)
DELETE /users/1 → 200 (первый раз), 404 (повторно) — всё равно идемпотентен
PUT /users/1 → 200 (всегда перезаписывает полностью)

Идемпотентные HTTP-методы:

МетодИдемпотентенПричина
GETДаНе изменяет состояние
PUTДаПолная перезапись ресурса
DELETEДаПосле удаления ресурс уже не существует
HEADДаКак GET, но без тела
OPTIONSДаНе изменяет состояние

Неидемпотентные HTTP-методы:

МетодИдемпотентенПричина
POSTНетСоздаёт новый ресурс каждый раз
PATCHНет*Зависит от реализации
// POST — НЕ идемпотентен
// Каждый вызов создаёт нового пользователя
POST /api/users
{ "name": "John" }
// Ответ 1: 201 { "id": 1, "name": "John" }
// Ответ 2: 201 { "id": 2, "name": "John" } — другой ID!

// PUT — идемпотентен
// Всегда перезаписывает ресурс полностью
PUT /api/users/1
{ "name": "John Updated" }
// Ответ 1: 200 { "id": 1, "name": "John Updated" }
// Ответ 2: 200 { "id": 1, "name": "John Updated" } — тот же результат

// DELETE — идемпотентен
DELETE /api/users/1
// Ответ 1: 200 — удалено
// Ответ 2: 404 — уже удалено (состояние не изменилось)

PATCH — особый случай:

// PATCH НЕ идемпотентен (инкремент)
PATCH /api/users/1
{ "balance": { "increment": 100 } }
// Вызов 1: balance = 100
// Вызов 2: balance = 200 — результат изменился!

// PATCH идемпотентен (присвоение)
PATCH /api/users/1
{ "name": "John" }
// Вызов 1: name = "John"
// Вызов 2: name = "John" — тот же результат

Зачем нужна идемпотентность:

1. Повторные запросы при таймаутах
Клиент не получил ответ → повторяет запрос → безопасно

2. Очереди сообщений
Kafka, RabbitMQ — at-least-once delivery
Сообщение может доставиться дважды

3. Retry-логика
Автоматические повторы при ошибках сети

Как обеспечить идемпотентность для POST:

// Идемпотентный ключ (Idempotency-Key)
app.post('/api/orders', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];

if (idempotencyKey) {
// Проверяем, был ли уже такой запрос
const existing = await redis.get(`idempotency:${idempotencyKey}`);
if (existing) {
return res.status(200).json(JSON.parse(existing));
}
}

// Создаём заказ
const order = await orderService.create(req.body);

// Сохраняем результат
if (idempotencyKey) {
await redis.setex(
`idempotency:${idempotencyKey}`,
86400, // 24 часа
JSON.stringify(order)
);
}

res.status(201).json(order);
});
// Клиент отправляет с ключом
const idempotencyKey = crypto.randomUUID();

fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({ items: [1, 2, 3] })
});

Вопрос 35. В чём разница между Promise.all и Promise.allSettled?

Таймкод: 00:46:02

Ответ собеседника: Правильный. Promise.all падает при первом reject, Promise.allSettled дожидается всех.

Правильный ответ:

Ответ собеседника корректен. Дополним деталями и примерами.

Promise.all — fail-fast стратегия. Отклоняется при первом же reject.

Promise.allSettled — ждёт завершения всех промисов, возвращает результат каждого.

// Promise.all — падает при первой ошибке
async function fetchAllUsers(userIds: number[]) {
try {
const users = await Promise.all(
userIds.map(id => fetch(`/api/users/${id}`).then(r => r.json()))
);
return users; // Массив пользователей
} catch (error) {
// Если хотя бы один запрос упал — попадаем сюда
console.error('One of the requests failed:', error);
throw error;
}
}

// Promise.allSettled — всегда ждёт всех
async function fetchAllUsersSafe(userIds: number[]) {
const results = await Promise.allSettled(
userIds.map(id => fetch(`/api/users/${id}`).then(r => r.json()))
);

const successful: User[] = [];
const failed: { id: number; reason: string }[] = [];

results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successful.push(result.value);
} else {
failed.push({ id: userIds[index], reason: result.reason.message });
}
});

return { successful, failed };
}

Формат результата Promise.allSettled:

// Результат — массив объектов:
[
{ status: 'fulfilled', value: { id: 1, name: 'Alice' } },
{ status: 'rejected', reason: Error('Network error') },
{ status: 'fulfilled', value: { id: 3, name: 'Bob' } }
]

Практические примеры:

// Загрузка нескольких ресурсов с обработкой ошибок
async function loadDashboard() {
const [userResult, ordersResult, notificationsResult] =
await Promise.allSettled([
fetchUser(),
fetchOrders(),
fetchNotifications()
]);

return {
user: userResult.status === 'fulfilled' ? userResult.value : null,
orders: ordersResult.status === 'fulfilled' ? ordersResult.value : [],
notifications: notificationsResult.status === 'fulfilled' ? notificationsResult.value : [],
errors: [
...(userResult.status === 'rejected' ? [userResult.reason] : []),
...(ordersResult.status === 'rejected' ? [ordersResult.reason] : []),
...(notificationsResult.status === 'rejected' ? [notificationsResult.reason] : [])
]
};
}

// Параллельная загрузка изображений с прогрессом
async function uploadImages(files: File[]) {
let completed = 0;

const uploadPromises = files.map(file =>
uploadImage(file).then(result => {
completed++;
updateProgress(completed / files.length);
return result;
})
);

const results = await Promise.allSettled(uploadPromises);

const successful = results
.filter((r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled')
.map(r => r.value);

const failed = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map((r, i) => ({ file: files[i].name, error: r.reason }));

return { successful, failed };
}

Сравнение всех методов Promise:

const promises = [
Promise.resolve(1),
Promise.reject(new Error('fail')),
Promise.resolve(3)
];

// Promise.all — reject при первой ошибке
try {
await Promise.all(promises);
} catch (e) {
console.log('Promise.all:', e.message); // 'fail'
}

// Promise.allSettled — всегда ждёт всех
const allSettled = await Promise.allSettled(promises);
console.log(allSettled);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: Error('fail') },
// { status: 'fulfilled', value: 3 }
// ]

// Promise.race — первый завершённый (resolve или reject)
const first = await Promise.race(promises);
console.log('Promise.race:', first); // 1 (первый resolve)

// Promise.any — первый успешный
try {
const any = await Promise.any(promises);
console.log('Promise.any:', any); // 1
} catch (e) {
// AggregateError если все reject
}

Когда что использовать:

МетодКогда использовать
Promise.allВсе запросы критичны, ошибка одного = ошибка всей операции
Promise.allSettledНужны результаты всех запросов, ошибки не критичны
Promise.raceНужен первый ответ (например, таймаут)
Promise.anyНужен первый успешный результат (fallback)

Вопрос 36. В чём отличие WeakMap от обычной Map?

Таймкод: 00:46:58

Ответ собеседника: Правильный. Назвал ограничения на ключи, сборку мусора и невозможность итерации.

Правильный ответ:

Ответ собеседника корректен. Дополним деталями.

Map — хранит пары ключ-значение с любыми типами ключей.

WeakMap — хранит только объекты как ключи, не препятствует сборке мусора.

// Map — любые типы ключей
const map = new Map();
map.set('string', 1);
map.set(42, 2);
map.set(true, 3);
map.set({ id: 1 }, 4);
map.set(null, 5);

// WeakMap — только объекты как ключи
const weakMap = new WeakMap();
const key1 = { id: 1 };
const key2 = { id: 2 };

weakMap.set(key1, 'value1');
weakMap.set(key2, 'value2');

// Ошибка: нельзя использовать примитивы как ключи
// weakMap.set('string', 'value'); // TypeError

Сборка мусора в WeakMap:

let obj = { id: 1 };
const weakMap = new WeakMap();
weakMap.set(obj, 'some data');

console.log(weakMap.has(obj)); // true

// Убираем последнюю ссылку на объект
obj = null;

// После сборки мусора запись будет удалена
// weakMap автоматически очищается

Сравнение характеристик:

ХарактеристикаMapWeakMap
Типы ключейЛюбыеТолько объекты
ИтерацияДа (keys, values, entries)Нет
Размер.sizeНет .size
Очистка.clear()Автоматическая (GC)
Сборка мусораНе собираетсяКлючи могут быть собраны

Практическое применение WeakMap:

// 1. Приватные данные для объектов
const privateData = new WeakMap();

class User {
constructor(name: string, password: string) {
this.name = name;
privateData.set(this, { password });
}

getPassword(): string {
return privateData.get(this)?.password;
}
}

// 2. Кэширование результатов вычислений
const cache = new WeakMap();

function expensiveCalculation(obj: ComplexObject): number {
if (cache.has(obj)) {
return cache.get(obj)!;
}

const result = /* тяжёлое вычисление */ obj.value * 2;
cache.set(obj, result);
return result;
}

// 3. Хранение метаданных для DOM-элементов
const elementMetadata = new WeakMap();

function setMetadata(element: HTMLElement, data: any) {
elementMetadata.set(element, data);
}

function getMetadata(element: HTMLElement) {
return elementMetadata.get(element);
}

// Когда DOM-элемент удаляется — метаданные автоматически очищаются

WeakSet — аналогично WeakMap:

// WeakSet хранит только объекты, не препятствует GC
const processedObjects = new WeakSet();

function process(obj: any) {
if (processedObjects.has(obj)) {
return; // Уже обработан
}

// Обработка...
processedObjects.add(obj);
}

Когда использовать:

  • Map — когда нужна итерация, подсчёт размера, примитивные ключи
  • WeakMap — когда нужно привязать данные к объектам без утечек памяти
  • WeakSet — когда нужно отслеживать обработанные объекты

Вопрос 37. Как получить все ключи объекта в JavaScript?

Таймкод: 00:47:32

Ответ собеседника: Правильный. Назвал Object.keys().

Правильный ответ:

Ответ собеседника корректен, но неполный. Существует несколько способов получения ключей.

Object.keys() — возвращает массив перечисляемых собственных ключей (строковых).

const user = { name: 'John', age: 30, city: 'NYC' };
console.log(Object.keys(user)); // ['name', 'age', 'city']

Object.values() — возвращает массив значений.

console.log(Object.values(user)); // ['John', 30, 'NYC']

Object.entries() — возвращает массив пар [ключ, значение].

console.log(Object.entries(user));
// [['name', 'John'], ['age', 30], ['city', 'NYC']]

// Удобно для итерации
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}

Reflect.ownKeys() — возвращает ВСЕ собственные ключи, включая символы и неперечисляемые.

const id = Symbol('id');
const obj = {
name: 'John',
[id]: 123,
age: 30
};

Object.defineProperty(obj, 'hidden', {
value: 'secret',
enumerable: false
});

console.log(Object.keys(obj)); // ['name', 'age']
console.log(Reflect.ownKeys(obj)); // ['name', 'age', 'hidden', Symbol(id)]

for...in — итерация по ключам, включая унаследованные из прототипа.

const parent = { inherited: true };
const child = Object.create(parent);
child.own = 'value';

for (const key in child) {
console.log(key); // 'own', 'inherited'
}

// Только собственные ключи
for (const key in child) {
if (child.hasOwnProperty(key)) {
console.log(key); // 'own'
}
}

Сравнение методов:

МетодСобственныеУнаследованныеСимволыНеперечисляемые
Object.keys()ДаНетНетНет
Object.values()ДаНетНетНет
Object.entries()ДаНетНетНет
Reflect.ownKeys()ДаНетДаДа
for...inДаДаНетНет

Практические примеры:

// Фильтрация ключей
const config = {
apiKey: 'secret',
apiSecret: 'hidden',
timeout: 5000,
retries: 3
};

const publicKeys = Object.keys(config).filter(
key => !key.startsWith('api')
);
console.log(publicKeys); // ['timeout', 'retries']

// Преобразование объекта
const prices = { apple: 1.5, banana: 0.8, orange: 1.2 };

const discountedPrices = Object.fromEntries(
Object.entries(prices).map(([key, value]) => [key, value * 0.9])
);
console.log(discountedPrices);
// { apple: 1.35, banana: 0.72, orange: 1.08 }

// Подсчёт ключей
const isEmpty = (obj: object) => Object.keys(obj).length === 0;

Вопрос 38. Для чего используется слово super в JavaScript?

Таймкод: 00:47:57

Ответ собеседника: Неполный. Упомянул только вызов конструктора родительского класса.

Правильный ответ:

super используется в двух контекстах: вызов конструктора родителя и доступ к методам родителя.

1. Вызов конструктора родителя (super())

Обязателен в конструкторе дочернего класса перед использованием this.

class Animal {
name: string;

constructor(name: string) {
this.name = name;
}
}

class Dog extends Animal {
breed: string;

constructor(name: string, breed: string) {
super(name); // Вызов конструктора Animal
this.breed = breed;
}
}

const dog = new Dog('Rex', 'Labrador');
console.log(dog.name); // 'Rex'
console.log(dog.breed); // 'Labrador'

2. Вызов методов родителя (super.method())

class Animal {
name: string;

constructor(name: string) {
this.name = name;
}

speak(): string {
return `${this.name} makes a sound`;
}

move(): string {
return `${this.name} moves`;
}
}

class Dog extends Animal {
breed: string;

constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}

// Переопределение с расширением поведения
speak(): string {
const parentResult = super.speak(); // Вызов метода родителя
return `${parentResult} and barks`;
}

// Полное переопределение
move(): string {
return `${this.name} runs fast`;
}

// Новый метод, использующий родительский
fetch(): string {
return `${super.move()} to get the ball`;
}
}

const dog = new Dog('Rex', 'Labrador');
console.log(dog.speak()); // 'Rex makes a sound and barks'
console.log(dog.move()); // 'Rex runs fast'
console.log(dog.fetch()); // 'Rex moves to get the ball'

3. Статические методы и super

class BaseController {
static getPath(): string {
return '/api';
}
}

class UserController extends BaseController {
static getPath(): string {
return `${super.getPath()}/users`; // '/api/users'
}

static getFullUrl(id: number): string {
return `${this.getPath()}/${id}`;
}
}

console.log(UserController.getPath()); // '/api/users'
console.log(UserController.getFullUrl(1)); // '/api/users/1'

4. Свойства через super (в методах доступа)

class Rectangle {
width: number;
height: number;

constructor(width: number, height: number) {
this.width = width;
this.height = height;
}

get area(): number {
return this.width * this.height;
}
}

class Square extends Rectangle {
constructor(side: number) {
super(side, side);
}

get area(): number {
// Можно использовать super для доступа к геттеру родителя
console.log(`Rectangle area: ${super.area}`);
return super.area;
}
}

Важные правила:

class Parent {
value: string;

constructor() {
this.value = 'parent';
}
}

class Child extends Parent {
constructor() {
// Ошибка: this недоступен до super()
// this.value = 'child'; // ReferenceError

super(); // Должен быть первым

// Теперь this доступен
this.value = 'child';
}
}

Сравнение с this:

Контекстthissuper
КонструкторЭкземплярКонструктор родителя
МетодЭкземплярПрототип родителя
Статический методКлассРодительский класс

Вопрос 39. Есть ли приватные поля и методы в JavaScript?

Таймкод: 00:48:26

Ответ собеседника: Неполный. Не упомянул синтаксис # из ES2022.

Правильный ответ:

Да, в JavaScript есть нативная поддержка приватных полей и методов через префикс # (ES2022).

Приватные поля и методы через #:

class User {
// Приватное поле
#password: string;

// Публичное поле
name: string;

constructor(name: string, password: string) {
this.name = name;
this.#password = password;
}

// Приватный метод
#validatePassword(password: string): boolean {
return password.length >= 8;
}

// Публичный метод, использующий приватный
changePassword(oldPassword: string, newPassword: string): boolean {
if (this.#password !== oldPassword) {
return false;
}

if (!this.#validatePassword(newPassword)) {
return false;
}

this.#password = newPassword;
return true;
}
}

const user = new User('John', 'secret123');

// Доступ к приватным полям извне — ошибка
// console.log(user.#password); // SyntaxError
// user.#validatePassword('test'); // SyntaxError

// Публичные методы работают
user.changePassword('secret123', 'newpass456'); // true

Приватные статические поля:

class Database {
static #connection: string | null = null;
static #instanceCount = 0;

constructor() {
Database.#instanceCount++;
}

static getConnection(): string {
if (!this.#connection) {
this.#connection = 'connected';
}
return this.#connection;
}

static getInstanceCount(): number {
return this.#instanceCount;
}
}

// Database.#connection; // SyntaxError — приватное
console.log(Database.getConnection()); // 'connected'
console.log(Database.getInstanceCount()); // 0

Способы инкапсуляции в JavaScript:

// 1. Соглашение об именовании (до ES2022)
class OldStyle {
_privateField: string; // Подчёркивание = соглашение

constructor() {
this._privateField = 'value';
}
}

// 2. WeakMap (до ES2022)
const privateData = new WeakMap();

class WithWeakMap {
constructor() {
privateData.set(this, { secret: 'value' });
}

getSecret(): string {
return privateData.get(this).secret;
}
}

// 3. Замыкания (до ES2022)
function createCounter() {
let count = 0; // Приватная переменная через замыкание

return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}

// 4. Символы (до ES2022)
const _secret = Symbol('secret');

class WithSymbol {
[_secret]: string;

constructor() {
this[_secret] = 'value';
}
}

// 5. Приватные поля # (ES2022+)
class Modern {
#secret: string;

constructor() {
this.#secret = 'value';
}
}

Сравнение подходов:

СпособНативная защитаДоступ извнеПоддержка
_prefixНетДаВсе версии
WeakMapДаНетES6+
ЗамыканияДаНетВсе версии
SymbolЧастичноДа (Reflect.ownKeys)ES6+
#privateДаНетES2022+

Проверка наличия приватного поля:

class Example {
#privateField = 'secret';
publicField = 'open';
}

const obj = new Example();

// Проверка через 'in' оператор
console.log('publicField' in obj); // true
// console.log('#privateField' in obj); // SyntaxError

// Проверка через hasOwnProperty
console.log(obj.hasOwnProperty('publicField')); // true
// console.log(obj.hasOwnProperty('#privateField')); // false

Важные особенности:

  • Приватные поля должны быть объявлены в теле класса (не в конструкторе)
  • Нельзя использовать # в именах переменных вне классов
  • Приватные поля не наследуются напрямую, но доступны через методы родителя

Вопрос 40. Есть ли встроенный в браузер метод для глубокого копирования объекта?

Таймкод: 00:48:53

Ответ собеседника: Правильный. Упомянул structuredClone.

Правильный ответ:

Да, в современных браузерах есть встроенный метод structuredClone() для глубокого клонирования.

// structuredClone — глубокое клонирование
const original = {
name: 'John',
age: 30,
address: {
city: 'NYC',
country: 'USA'
},
hobbies: ['reading', 'coding']
};

const clone = structuredClone(original);

// Изменения клона не влияют на оригинал
clone.address.city = 'Boston';
clone.hobbies.push('gaming');

console.log(original.address.city); // 'NYC' (не изменилось)
console.log(original.hobbies); // ['reading', 'coding'] (не изменилось)

Что поддерживает structuredClone:

const complex = {
primitives: {
string: 'text',
number: 42,
boolean: true,
null: null,
undefined: undefined
},
builtInTypes: {
date: new Date('2024-01-01'),
regex: /test/gi,
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
arrayBuffer: new ArrayBuffer(8),
error: new Error('test')
},
nested: {
array: [1, [2, 3], { deep: true }],
object: { a: { b: { c: 'deep' } } }
}
};

const clone = structuredClone(complex);
// Все типы клонируются корректно

Что НЕ поддерживает structuredClone:

// Функции — будут потеряны
const withFunction = {
name: 'test',
method: () => console.log('hello')
};

const clone1 = structuredClone(withFunction);
console.log(clone1.method); // undefined

// DOM-элементы — выбросит ошибку
const withDOM = {
element: document.body
};
// structuredClone(withDOM); // DataCloneError

// Свойства из прототипа — не копируются
class Parent {
inherited = 'value';
}
class Child extends Parent {
own = 'own value';
}

const instance = new Child();
const clone2 = structuredClone(instance);
console.log(clone2.own); // 'own value'
console.log(clone2.inherited); // undefined

Сравнение способов клонирования:

const original = {
name: 'John',
address: { city: 'NYC' },
hobbies: ['reading']
};

// 1. Поверхностное копирование
const spread = { ...original };
// spread.address === original.address — true (ссылка та же)

// 2. JSON.parse/JSON.stringify — глубокое, но с ограничениями
const jsonClone = JSON.parse(JSON.stringify(original));
// Теряет: Date, Map, Set, undefined, функции, циклические ссылки

// 3. structuredClone — глубокое с поддержкой встроенных типов
const structured = structuredClone(original);
// Поддерживает: Date, Map, Set, ArrayBuffer, Error и др.
// Не поддерживает: функции, DOM, прототипы

Сводная таблица:

СпособГлубокоеФункцииDate/Map/SetЦиклические ссылкиПрототипы
spreadНетДаДаДаДа
JSONДаНетНетНетНет
structuredCloneДаНетДаДаНет

Полифил для старых браузеров:

function deepClone<T>(obj: T): T {
if (typeof structuredClone === 'function') {
return structuredClone(obj);
}

// Fallback для старых браузеров
return JSON.parse(JSON.stringify(obj));
}

Вопрос 41. Какие есть способы хранения данных на клиенте?

Таймкод: 00:49:07

Ответ собеседника: Правильный. Назвал основные способы хранения и различия между localStorage и sessionStorage.

Правильный ответ:

Ответ собеседника корректен. Дополним деталями.

Способы хранения данных на клиенте:

СпособОбъёмДоступСрок жизниОтправка на сервер
Cookie~4 КБСинхронныйНастраиваетсяДа (автоматически)
localStorage~5-10 МБСинхронныйБессрочноНет
sessionStorage~5-10 МБСинхронныйВкладкаНет
IndexedDBНеограниченАсинхронныйБессрочноНет
Cache APIНеограниченАсинхронныйНастраиваетсяНет

Cookie:

// Установка cookie
document.cookie = 'theme=dark; max-age=31536000; path=/; Secure; SameSite=Strict';

// Чтение cookie
function getCookie(name: string): string | null {
const match = document.cookie.match(new RegExp(`${name}=([^;]+)`));
return match ? decodeURIComponent(match[1]) : null;
}

// Удаление cookie
document.cookie = 'theme=; max-age=0; path=/';

// Cookie отправляется с каждым запросом
fetch('/api/data', {
credentials: 'include' // Отправлять cookies
});

localStorage vs sessionStorage:

// localStorage — сохраняется между сессиями
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'John' }));
localStorage.setItem('theme', 'dark');

// sessionStorage — только для текущей вкладки
sessionStorage.setItem('tempData', JSON.stringify({ step: 2 }));

// Чтение
const user = JSON.parse(localStorage.getItem('user') || 'null');
const tempData = JSON.parse(sessionStorage.getItem('tempData') || 'null');

// Очистка
localStorage.removeItem('theme');
localStorage.clear(); // Очистить всё

IndexedDB — для больших объёмов данных:

// Открытие базы данных
const request = indexedDB.open('MyDatabase', 1);

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;

// Создание хранилища объектов
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('email', 'email', { unique: true });
};

request.onsuccess = (event) => {
const db = (event.target as IDBOpenDBRequest).result;

// Добавление записи
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
store.add({ id: 1, name: 'John', email: 'john@example.com' });

// Получение записи
const getRequest = store.get(1);
getRequest.onsuccess = () => {
console.log(getRequest.result);
};
};

Cache API — для кэширования ресурсов:

// Кэширование ответов
caches.open('v1').then(cache => {
cache.addAll([
'/api/users',
'/styles/main.css',
'/scripts/app.js'
]);
});

// Получение из кэша
caches.match('/api/users').then(response => {
if (response) {
return response.json();
}
return fetch('/api/users').then(response => {
caches.open('v1').then(cache => cache.put('/api/users', response));
return response.json();
});
});

Когда что использовать:

СценарийРекомендуемое хранилище
Токены авторизацииhttpOnly cookie
Настройки пользователяlocalStorage
Временные данные формыsessionStorage
Большие объёмы данныхIndexedDB
Офлайн-кэшированиеCache API
Корзина покупокIndexedDB или localStorage

Вопрос 42. Что такое критический путь рендеринга?

Таймкод: 00:50:27

Ответ собеседника: Правильный. Описал этапы рендеринга и поведение script с async/defer.

Правильный ответ:

Ответ собеседника полный и корректный. Дополним деталями.

Критический путь рендерing (CRP) — последовательность шагов браузера для отображения первой страницы.

HTML → DOM

CSS → CSSOM

Render Tree

Layout

Paint

Composite

Этапы подробно:

// 1. DOM (Document Object Model)
// Парсинг HTML → построение дерева узлов

// 2. CSSOM (CSS Object Model)
// Парсинг CSS → построение дерева стилей

// 3. Render Tree
// DOM + CSSOM → дерево видимых элементов
// Исключаются: <head>, display: none, мета-теги

// 4. Layout (Reflow)
// Расчёт размеров и позиций каждого элемента

// 5. Paint
// Запикселивание: цвета, тени, градиенты

// 6. Composite
// Слои объединяются в финальное изображение

Влияние script на рендеринг:

<!-- Блокирует парсинг HTML -->
<script src="app.js"></script>
<!-- Парсинг останавливается до загрузки и выполнения -->

<!-- defer: загружается параллельно, выполняется после DOM -->
<script defer src="app.js"></script>

<!-- async: загружается параллельно, выполняется сразу после загрузки -->
<script async src="analytics.js"></script>

<!-- type="module": автоматически defer -->
<script type="module" src="app.js"></script>

Сравнение атрибутов script:

АтрибутЗагрузкаВыполнениеПорядок
НетБлокируетСразуПо порядку
deferПараллельноПосле DOMСохраняется
asyncПараллельноСразу после загрузкиНе гарантируется
type="module"ПараллельноПосле DOMСохраняется

Оптимизация критического пути:

// 1. Инлайн критический CSS
<style>
/* Стили для первого экрана */
.hero { /* ... */ }
.header { /* ... */ }
</style>

// 2. Отложенная загрузка некритического CSS
<link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">

// 3. Предзагрузка ресурсов
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preconnect" href="https://api.example.com">

// 4. Ленивая загрузка изображений
<img src="hero.jpg" loading="eager"> <!-- Приоритетное -->
<img src="below-fold.jpg" loading="lazy"> <!-- Отложенное -->

Метрики производительности:

// Web Vitals
// LCP (Largest Contentful Paint) — время загрузки основного контента
// FID (First Input Delay) — время до первого взаимодействия
// CLS (Cumulative Layout Shift) — стабильность макета

// Измерение
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP:', entry.startTime);
}
}).observe({ type: 'largest-contentful-paint', buffered: true });

Вопрос 43. Какие способы общения между соседними окнами/вкладками существуют?

Таймкод: 00:51:56

Ответ собеседника: Неполный. Назвал postMessage и Broadcast Channel, но не знал про SharedWorker.

Правильный ответ:

Существует несколько способов общения между окнами/вкладками:

1. BroadcastChannel API

Простой способ для обмена сообщениями между вкладками одного origin.

// Создание канала
const channel = new BroadcastChannel('app_channel');

// Отправка сообщения
channel.postMessage({ type: 'LOGOUT', timestamp: Date.now() });

// Получение сообщения
channel.onmessage = (event) => {
console.log('Received:', event.data);
};

// Закрытие
channel.close();

2. SharedWorker

Специальный воркер, к которому могут подключаться несколько вкладок.

// shared-worker.ts
const connections: MessagePort[] = [];

self.onconnect = (event) => {
const port = event.ports[0];
connections.push(port);

port.onmessage = (e) => {
// Рассылка всем подключённым вкладкам
connections.forEach(conn => {
if (conn !== port) {
conn.postMessage(e.data);
}
});
};

port.start();
};

// main.ts
const worker = new SharedWorker('/shared-worker.js');

worker.port.onmessage = (event) => {
console.log('From another tab:', event.data);
};

worker.port.postMessage({ type: 'UPDATE', data: 'hello' });

3. postMessage между окнами

// Родительское окно
const popup = window.open('/popup', 'popup', 'width=400,height=300');

popup.postMessage({ type: 'INIT', data: 'hello' }, '*');

// Popup окно
window.opener.postMessage({ type: 'RESPONSE', data: 'world' }, '*');

// Обработка в обоих окнах
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted.com') return;
console.log('Received:', event.data);
});

4. localStorage с событием storage

// Вкладка 1 — отправка
localStorage.setItem('message', JSON.stringify({
type: 'LOGOUT',
timestamp: Date.now()
}));

// Вкладка 2 — получение
window.addEventListener('storage', (event) => {
if (event.key === 'message') {
const data = JSON.parse(event.newValue || '{}');
console.log('Received:', data);
}
});

5. ServiceWorker

// service-worker.ts
self.addEventListener('message', (event) => {
// Рассылка всем клиентам
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage(event.data);
});
});
});

// main.ts
navigator.serviceWorker.controller.postMessage({ type: 'UPDATE' });

navigator.serviceWorker.addEventListener('message', (event) => {
console.log('From SW:', event.data);
});

Сравнение способов:

СпособСложностьДвустороннийПоддержкаСценарий
BroadcastChannelНизкаяДаСовременные браузерыПростые уведомления
SharedWorkerСредняяДаСовременные браузерыСостояние между вкладками
postMessageСредняяДаВсе браузерыiframe, popup
localStorage + storageНизкаяНет (односторонний)Все браузерыПростые уведомления
ServiceWorkerВысокаяДаСовременные браузерыСложная логика

Когда что использовать:

  • BroadcastChannel — простая рассылка сообщений между вкладками
  • SharedWorker — общее состояние, координация между вкладками
  • postMessage — общение с iframe или popup
  • localStorage + storage — простые уведомления (logout, settings change)

Вопрос 44. Как организовать общение между микрофронтендами на разных технологиях?

Таймкод: 00:53:20

Ответ собеседника: Правильный. Назвал Event Bus на основе кастомных событий.

Правильный ответ:

Ответ собеседника корректен. Дополним подробностями.

1. Event Bus через CustomEvent

// event-bus.ts — общий модуль
type EventHandler = (data: any) => void;

class EventBus {
private listeners: Map<string, Set<EventHandler>> = new Map();

on(event: string, handler: EventHandler): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);

// Возвращаем функцию для отписки
return () => this.off(event, handler);
}

off(event: string, handler: EventHandler): void {
this.listeners.get(event)?.delete(handler);
}

emit(event: string, data?: any): void {
this.listeners.get(event)?.forEach(handler => handler(data));
}
}

export const eventBus = new EventBus();
// React компонент
import { eventBus } from './event-bus';

function ReactComponent() {
const [cartCount, setCartCount] = useState(0);

useEffect(() => {
const unsubscribe = eventBus.on('cart:updated', (data) => {
setCartCount(data.count);
});

return unsubscribe;
}, []);

const addToCart = () => {
eventBus.emit('cart:add', { productId: 123 });
};

return <button onClick={addToCart}>Cart: {cartCount}</button>;
}
// Angular компонент
import { eventBus } from './event-bus';

@Component({ selector: 'app-cart' })
export class CartComponent implements OnInit, OnDestroy {
private unsubscribe: () => void;
items: CartItem[] = [];

ngOnInit() {
this.unsubscribe = eventBus.on('cart:add', (data) => {
this.addProduct(data.productId);
});
}

ngOnDestroy() {
this.unsubscribe();
}

private addProduct(productId: number) {
// Логика добавления
this.items.push({ id: productId });
eventBus.emit('cart:updated', { count: this.items.length });
}
}

2. Через DOM CustomEvent

// Отправка события
document.dispatchEvent(new CustomEvent('micro:event', {
detail: {
type: 'USER_LOGIN',
payload: { userId: 1, name: 'John' }
}
}));

// Подписка
document.addEventListener('micro:event', (event: CustomEvent) => {
const { type, payload } = event.detail;

switch (type) {
case 'USER_LOGIN':
handleUserLogin(payload);
break;
case 'CART_UPDATE':
handleCartUpdate(payload);
break;
}
});

3. Shared State через Store

// shared-store.ts
type Listener = () => void;

class SharedStore<T> {
private state: T;
private listeners: Set<Listener> = new Set();

constructor(initialState: T) {
this.state = initialState;
}

getState(): T {
return this.state;
}

setState(newState: Partial<T>): void {
this.state = { ...this.state, ...newState };
this.listeners.forEach(listener => listener());
}

subscribe(listener: Listener): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
}

export const userStore = new SharedStore({
id: null as number | null,
name: '',
isLoggedIn: false
});

4. Через URL/Query Parameters

// Для простых данных между микрофронтендами
class NavigationService {
navigateWithState(path: string, state: any): void {
const params = new URLSearchParams(state).toString();
window.history.pushState({}, '', `${path}?${params}`);
}

getStateFromUrl(): Record<string, string> {
const params = new URLSearchParams(window.location.search);
return Object.fromEntries(params.entries());
}
}

Сравнение подходов:

ПодходСложностьТипизацияОтладкаКогда использовать
Event BusНизкаяСредняяСредняяПростые события
CustomEventНизкаяНизкаяВысокаяМинимум зависимостей
Shared StoreСредняяВысокаяВысокаяОбщее состояние
URLНизкаяНизкаяВысокаяПростые данные

Рекомендации:

  • Используйте Event Bus для простых событий между микрофронтендами
  • Shared Store — когда нужно общее состояние (корзина, пользователь)
  • Типизируйте события и данные для удобства разработки
  • Не забывайте отписываться от событий при размонтировании компонентов

Вопрос 45. Какие технологии микрофронтендов использовали?

Таймкод: 00:54:09

Ответ собеседника: Неполный. Упомянул Module Federation, но не имел опыта интеграции разных технологий.

Правильный ответ:

Ответ собеседника корректен. Дополним информацией о подходах к интеграции разных технологий.

Module Federation (Webpack 5)

// webpack.config.js — Host (контейнер)
const ModuleFederationPlugin = require('@module-federation/webpack');

module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
products: 'products@http://localhost:3001/remoteEntry.js',
cart: 'cart@http://localhost:3002/remoteEntry.js'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})
]
};

// webpack.config.js — Remote (микрофронтенд)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};

Интеграция разных версий React:

// Проблема: две копии React в разных микрофронтендах
// Решение 1: Singleton в shared
shared: {
react: {
singleton: true, // Только одна копия
requiredVersion: '^18.0.0',
eager: false // Ленивая загрузка
}
}

// Решение 2: Разные версии с алиасами
// webpack.config.js
resolve: {
alias: {
'react$': path.resolve('./node_modules/react'),
'react-17$': path.resolve('./node_modules/react-17')
}
}

Интеграция React + Angular:

// Вариант 1: Web Components
// Angular компонент как Web Component
@NgModule({
declarations: [AngularComponent],
entryComponents: [AngularComponent]
})
export class AppModule {
constructor(private injector: Injector) {
const element = createCustomElement(AngularComponent, { injector });
customElements.define('angular-widget', element);
}
}

// React использует как обычный HTML-элемент
function ReactComponent() {
return <angular-widget data={someData}></angular-widget>;
}
// Вариант 2: iframe изоляция
function MicroFrontend({ url, name }: { url: string; name: string }) {
const iframeRef = useRef<HTMLIFrameElement>(null);

useEffect(() => {
const iframe = iframeRef.current;

// Обработка сообщений от iframe
const handleMessage = (event: MessageEvent) => {
if (event.origin !== new URL(url).origin) return;

if (event.data.type === `${name}:READY`) {
// Отправка начальных данных
iframe?.contentWindow?.postMessage(
{ type: 'INIT', payload: initialData },
url
);
}
};

window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [url, name]);

return <iframe ref={iframeRef} src={url} style={{ border: 'none' }} />;
}

Сравнение подходов к интеграции:

ПодходИзоляцияПроизводительностьСложностьСвязность
Module FederationСредняяВысокаяСредняяСильная
Web ComponentsВысокаяВысокаяСредняяСлабая
iframeПолнаяНизкаяНизкаяСлабая
Module Federation + Web ComponentsВысокаяВысокаяВысокаяСредняя

Подводные камни:

// 1. Конфликт версий зависимостей
// Решение: strictVersion в shared
shared: {
react: {
singleton: true,
strictVersion: true, // Ошибка при несовпадении версии
requiredVersion: '18.2.0'
}
}

// 2. Разные версии React (17 и 18)
// Решение: оборачивание в iframe или Web Components

// 3. Глобальные стили
// Решение: CSS Modules, Shadow DOM, или CSS-in-JS

// 4. Разные роутеры
// Решение: MemoryRouter для микрофронтендов

Вопрос 46. Зачем нужны микрофронтенды?

Таймкод: 00:55:58

Ответ собеседника: Правильный. Назвал независимый деплой и разделение ответственности между командами.

Правильный ответ:

Ответ собеседника корректен. Дополним деталями.

Основные причины использования микрофронтендов:

1. Независимый деплой

// Монолит: один пакет — один релиз
// Изменение в одном модуле → пересборка всего приложения → риск для всего продукта

// Микрофронтенды: каждый деплоится отдельно
// auth-service: v1.2.0 → deploy
// products-service: v2.0.0 → deploy
// cart-service: v1.5.0 → deploy
// Никакого влияния друг на друга

2. Командная автономия

Монолит:
- 50 разработчиков работают в одном репозитории
- Конфликты мержа, блокировки
- Один CI/CD пайплайн на всех

Микрофронтенды:
- Команда Auth: 5 человек, свой репозиторий
- Команда Products: 8 человек, свой репозиторий
- Команда Cart: 4 человека, свой репозиторий
- Каждая команда сама решает когда и что деплоить

3. Технологическая свобода

// Команда Auth выбирает React + TypeScript
// Команда Products выбирает Vue 3
// Команда Cart выбирает Angular
// Нет необходимости согласовывать стек

// Миграция постепенно:
// Шаг 1: Выделить auth в отдельный микрофронтенд на новом стеке
// Шаг 2: Переписать products на новом стеке
// Шаг 3: Монолит становится всё меньше

4. Масштабирование разработки

// Проблемы монолита при росте:
// - Время сборки: 30+ минут
// - Время тестирования: 2+ часа
// - Code review: 200+ строк за раз
// - Onboarding: недели для новых разработчиков

// Преимущества микрофронтендов:
// - Время сборки: 2-5 минут на сервис
// - Время тестирования: 10-15 минут на сервис
// - Code review: 50-100 строк за раз
// - Onboarding: дни для новых разработчиков

5. Изоляция сбоев

// Монолит: ошибка в одном модуле → всё приложение падает
// Микрофронтенды: ошибка в cart → auth и products продолжают работать

// Error Boundary для микрофронтендов
function MicroFrontendErrorBoundary({ name, children }) {
const [hasError, setHasError] = useState(false);

if (hasError) {
return (
<div className="error-fallback">
<h3>{name} временно недоступен</h3>
<button onClick={() => setHasError(false)}>Попробовать снова</button>
</div>
);
}

return (
<ErrorBoundary onError={() => setHasError(true)}>
{children}
</ErrorBoundary>
);
}

Когда НЕ нужны микрофронтенды:

1. Маленькая команда (1-5 человек)
2. Простое приложение (лендинг, блог)
3. Нет проблем с масштабированием
4. Нет необходимости в независимом деплое
5. Ограниченные DevOps ресурсы

Сравнение подходов:

КритерийМонолитМикрофронтенды
Сложность начальной разработкиНизкаяВысокая
Время сборкиРастёт с размеромПостоянное
Независимый деплойНетДа
Технологическая свободаНетДа
Изоляция сбоевНетДа
DevOps сложностьНизкаяВысокая
Межсервисная коммуникацияПростаяСложная

Рекомендация:

Микрофронтенды оправданы при:

  • Большой команде (20+ разработчиков)
  • Необходимости независимых релизов
  • Разнородных требованиях к разным частям приложения
  • Постепенной миграции с монолита

Вопрос 46. Зачем нужны микрофронтенды?

Таймкод: 00:55:58

Ответ собеседника: Правильный. Назвал независимый деплой и разделение ответственности между командами.

Правильный ответ:

Ответ собеседника корректен. Дополним деталями.

Основные причины использования микрофронтендов:

1. Независимый деплой

// Монолит: один пакет — один релиз
// Изменение в одном модуле → пересборка всего приложения → риск для всего продукта

// Микрофронтенды: каждый деплоится отдельно
// auth-service: v1.2.0 → deploy
// products-service: v2.0.0 → deploy
// cart-service: v1.5.0 → deploy
// Никакого влияния друг на друга

2. Командная автономия

Монолит:
- 50 разработчиков работают в одном репозитории
- Конфликты мержа, блокировки
- Один CI/CD пайплайн на всех

Микрофронтенды:
- Команда Auth: 5 человек, свой репозиторий
- Команда Products: 8 человек, свой репозиторий
- Команда Cart: 4 человека, свой репозиторий
- Каждая команда сама решает когда и что деплоить

3. Технологическая свобода

// Команда Auth выбирает React + TypeScript
// Команда Products выбирает Vue 3
// Команда Cart выбирает Angular
// Нет необходимости согласовывать стек

// Миграция постепенно:
// Шаг 1: Выделить auth в отдельный микрофронтенд на новом стеке
// Шаг 2: Переписать products на новом стеке
// Шаг 3: Монолит становится всё меньше

4. Масштабирование разработки

// Проблемы монолита при росте:
// - Время сборки: 30+ минут
// - Время тестирования: 2+ часа
// - Code review: 200+ строк за раз
// - Onboarding: недели для новых разработчиков

// Преимущества микрофронтендов:
// - Время сборки: 2-5 минут на сервис
// - Время тестирования: 10-15 минут на сервис
// - Code review: 50-100 строк за раз
// - Onboarding: дни для новых разработчиков

5. Изоляция сбоев

// Монолит: ошибка в одном модуле → всё приложение падает
// Микрофронтенды: ошибка в cart → auth и products продолжают работать

// Error Boundary для микрофронтендов
function MicroFrontendErrorBoundary({ name, children }) {
const [hasError, setHasError] = useState(false);

if (hasError) {
return (
<div className="error-fallback">
<h3>{name} временно недоступен</h3>
<button onClick={() => setHasError(false)}>Попробовать снова</button>
</div>
);
}

return (
<ErrorBoundary onError={() => setHasError(true)}>
{children}
</ErrorBoundary>
);
}

Когда НЕ нужны микрофронтенды:

1. Маленькая команда (1-5 человек)
2. Простое приложение (лендинг, блог)
3. Нет проблем с масштабированием
4. Нет необходимости в независимом деплое
5. Ограниченные DevOps ресурсы

Сравнение подходов:

КритерийМонолитМикрофронтенды
Сложность начальной разработкиНизкаяВысокая
Время сборкиРастёт с размеромПостоянное
Независимый деплойНетДа
Технологическая свободаНетДа
Изоляция сбоевНетДа
DevOps сложностьНизкаяВысокая
Межсервисная коммуникацияПростаяСложная

Рекомендация:

Микрофронтенды оправданы при:

  • Большой команде (20+ разработчиков)
  • Необходимости независимых релизов
  • Разнородных требованиях к разным частям приложения
  • Постепенной миграции с монолита

Вопрос 47. Что такое CSS-переменные и чем они отличаются от переменных в препроцессорах?

Таймкод: 00:56:32

Ответ собеседника: Правильный. Описал синтаксис CSS-переменных и SCSS, упомянул видимость в DevTools.

Правильный ответ:

Ответ собеседника корректен. Дополним деталями.

CSS-переменные (Custom Properties)

/* Объявление */
:root {
--primary-color: #3498db;
--spacing-unit: 8px;
--font-size-base: 16px;
}

/* Использование */
.button {
background-color: var(--primary-color);
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
font-size: var(--font-size-base);
}

/* Значение по умолчанию */
.element {
color: var(--undefined-var, black); /* black если переменная не определена */
}

SCSS переменные

// Объявление
$primary-color: #3498db;
$spacing-unit: 8px;
$font-size-base: 16px;

// Использование
.button {
background-color: $primary-color;
padding: $spacing-unit ($spacing-unit * 2);
font-size: $font-size-base;
}

// Результат компиляции
.button {
background-color: #3498db;
padding: 8px 16px;
font-size: 16px;
}

Ключевые отличия:

ХарактеристикаCSS-переменныеSCSS переменные
Время жизниRuntime (в браузере)Compile-time
Динамическое изменениеДа (JavaScript)Нет
НаследованиеДа (каскад)Нет
Область видимостиCSS каскадБлок кода
Видимость в DevToolsДаНет
Вычисленияcalc()Нативные операции

Динамическое изменение CSS-переменных:

// JavaScript управление темой
document.documentElement.style.setProperty('--primary-color', '#e74c3c');

// Переключение темы
function setTheme(theme: 'light' | 'dark') {
const root = document.documentElement;

if (theme === 'dark') {
root.style.setProperty('--bg-color', '#1a1a1a');
root.style.setProperty('--text-color', '#ffffff');
} else {
root.style.setProperty('--bg-color', '#ffffff');
root.style.setProperty('--text-color', '#000000');
}
}

Область видимости CSS-переменных:

/* Глобальные */
:root {
--color: blue;
}

/* Локальные */
.card {
--card-padding: 16px;
padding: var(--card-padding);
}

.button {
/* var(--card-padding) не работает здесь */
}

SCSS: преимущества компиляции:

// Вычисления
$base-size: 16px;
$large-size: $base-size * 1.5; // 24px

// Функции
$lightened: lighten($primary-color, 20%);

// Миксины
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}

// Это невозможно с CSS-переменными

Когда что использовать:

  • CSS-переменные — темы, динамические стили, значения, которые меняются в runtime
  • SCSS переменные — константы, вычисления, миксины, функции

Вопрос 48. Что такое Accessibility (a11y) и какие средства используются?

Таймкод: 00:58:40

Ответ собеседника: Правильный. Назвал основные средства: темы, шрифты, tabindex, alt, семантику, ARIA.

Правильный ответ:

Accessibility (a11y) — доступность интерфейса для людей с ограниченными возможностями.

Основные средства:

1. Семантический HTML

<!-- Плохо: всё div'ы -->
<div class="header">...</div>
<div class="nav">...</div>
<div class="main">...</div>

<!-- Хорошо: семантические теги -->
<header>...</header>
<nav aria-label="Главная навигация">...</nav>
<main>
<article>
<h1>Заголовок</h1>
<section aria-labelledby="section-heading">
<h2 id="section-heading">Раздел</h2>
</section>
</article>
</main>
<footer>...</footer>

2. ARIA-атрибуты

<!-- Роли -->
<div role="alert" aria-live="assertive">
Ошибка: неверный пароль
</div>

<!-- Состояния -->
<button aria-expanded="false" aria-controls="menu">
Меню
</button>
<ul id="menu" role="menu" aria-hidden="true">
<li role="menuitem">Пункт 1</li>
<li role="menuitem">Пункт 2</li>
</ul>

<!-- Описания -->
<input type="email" aria-describedby="email-hint" />
<span id="email-hint">Введите email в формате user@example.com</span>

3. Навигация с клавиатуры

<!-- tabindex -->
<div tabindex="0" role="button" onclick="handleClick()">
Кликабельный блок
</div>

<!-- Порядок табуляции -->
<form>
<input tabindex="1" placeholder="Имя" />
<input tabindex="2" placeholder="Email" />
<button tabindex="3">Отправить</button>
</form>
// Обработка клавиатуры
function handleKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'Enter':
case ' ':
event.preventDefault();
handleAction();
break;
case 'Escape':
closeModal();
break;
case 'ArrowDown':
moveFocus(1);
break;
case 'ArrowUp':
moveFocus(-1);
break;
}
}

4. Альтернативные тексты

<!-- Информативные изображения -->
<img src="chart.png" alt="График продаж: рост на 25% за квартал" />

<!-- Декоративные изображения -->
<img src="divider.png" alt="" role="presentation" />

<!-- Иконки с текстом -->
<button>
<svg aria-hidden="true">...</svg>
<span class="sr-only">Закрыть</span>
</button>

5. Доступные формы

<form>
<div>
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-invalid="true"
aria-describedby="email-error"
/>
<span id="email-error" role="alert">
Введите корректный email
</span>
</div>

<fieldset>
<legend>Способ доставки</legend>
<label>
<input type="radio" name="delivery" value="courier" />
Курьером
</label>
<label>
<input type="radio" name="delivery" value="pickup" />
Самовывоз
</label>
</fieldset>
</form>

6. Цветовой контраст

/* Минимальный контраст 4.5:1 для текста */
.text {
color: #333; /* на белом фоне — контраст 12.6:1 */
}

/* Не только цвет для передачи информации */
.error {
color: red;
border-left: 3px solid red; /* Дополнительный индикатор */
}

.error::before {
content: "⚠️ ";
}

7. Skip Links

<a href="#main-content" class="skip-link">
Перейти к основному контенту
</a>

<main id="main-content">
...
</main>

<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background: #000;
color: #fff;
z-index: 100;
}

.skip-link:focus {
top: 0;
}
</style>

Чеклист проверки:

Все изображения имеют alt
Формы имеют label
Доступна навиатура
Контраст соответствует WCAG
ARIA используется корректно
Заголовки в правильном порядке
Фокус виден при навигации

Вопрос 49. Что такое Type Guard в TypeScript?

Таймкод: 00:59:56

Ответ собеседника: Правильный. Определил Type Guard как функцию сужения типа с оператором is.

Правильный ответ:

Type Guard — механизм сужения типа (type narrowing) в TypeScript.

1. Пользовательские Type Guards с is

// Определение Type Guard
function isString(value: unknown): value is string {
return typeof value === 'string';
}

// Использование
function processValue(value: string | number) {
if (isString(value)) {
// value: string
return value.toUpperCase();
}
// value: number
return value.toFixed(2);
}

2. Встроенные Type Guards

// typeof
function format(value: string | number) {
if (typeof value === 'string') {
return value.trim();
}
return value.toString();
}

// instanceof
class User {
name: string = '';
}

class Admin {
name: string = '';
permissions: string[] = [];
}

function greet(entity: User | Admin) {
if (entity instanceof Admin) {
console.log(`Admin ${entity.name} has ${entity.permissions.length} permissions`);
} else {
console.log(`User ${entity.name}`);
}
}

// in operator
interface Cat {
meow(): void;
}

interface Dog {
bark(): void;
}

function makeSound(animal: Cat | Dog) {
if ('meow' in animal) {
animal.meow(); // TypeScript знает что это Cat
} else {
animal.bark(); // TypeScript знает что это Dog
}
}

3. Discriminated Unions

interface Circle {
kind: 'circle';
radius: number;
}

interface Square {
kind: 'square';
side: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
}
}

4. Type Guards для фильтрации

// Фильтрация массива с Type Guard
function isDefined<T>(value: T | undefined | null): value is T {
return value !== undefined && value !== null;
}

const values: (string | undefined)[] = ['a', undefined, 'b', null, 'c'];
const filtered: string[] = values.filter(isDefined);
// ['a', 'b', 'c']

5. Assertion Functions

// Вместо возврата boolean — выбрасывает ошибку
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string');
}
}

function process(value: unknown) {
assertIsString(value);
// value: string
console.log(value.toUpperCase());
}

Практический пример:

interface ApiResponse {
success: boolean;
data?: unknown;
error?: string;
}

function isSuccessResponse(
response: ApiResponse
): response is { success: true; data: unknown } {
return response.success === true;
}

async function fetchUser(id: number) {
const response: ApiResponse = await fetch(`/api/users/${id}`).then(r => r.json());

if (isSuccessResponse(response)) {
// response.data доступен без !
return response.data as User;
}

throw new Error(response.error);
}

Вопрос 50. Что такое Generics в TypeScript?

Таймкод: 01:00:26

Ответ собеседника: Правильный. Определил Generics как механизм с отложенным определением типа.

Правильный ответ:

Generics — параметризованные типы, позволяющие создавать компоненты, работающие с разными типами без потери типизации.

1. Базовый синтаксис

// Функция с Generic
function identity<T>(value: T): T {
return value;
}

const str = identity<string>('hello'); // string
const num = identity<number>(42); // number
const bool = identity(true); // boolean (вывод автоматически)

2. Generic для интерфейсов

interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}

interface User {
id: number;
name: string;
}

interface Product {
id: number;
title: string;
price: number;
}

// Использование
const userResponse: ApiResponse<User> = {
data: { id: 1, name: 'John' },
status: 200
};

const productResponse: ApiResponse<Product> = {
data: { id: 1, title: 'Laptop', price: 999 },
status: 200
};

3. Generic для классов

class Queue<T> {
private items: T[] = [];

enqueue(item: T): void {
this.items.push(item);
}

dequeue(): T | undefined {
return this.items.shift();
}

peek(): T | undefined {
return this.items[0];
}

size(): number {
return this.items.length;
}
}

// Использование
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
const first: number | undefined = numberQueue.dequeue();

const stringQueue = new Queue<string>();
stringQueue.enqueue('hello');

4. Ограничения (Constraints)

// Ограничение по интерфейсу
interface HasLength {
length: number;
}

function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}

logLength('hello'); // 5
logLength([1, 2, 3]); // 3
logLength({ length: 10 }); // 10
// logLength(42); // Ошибка: number не имеет length

// Ограничение по ключу
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user = { id: 1, name: 'John', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'email'); // Ошибка: 'email' не существует

5. Практические примеры

// Generic для API-клиента
class ApiClient {
async get<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json() as T;
}

async post<T, D>(url: string, data: D): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json() as T;
}
}

const api = new ApiClient();

// Тип определяется при вызове
const user = await api.get<User>('/api/users/1');
const newUser = await api.post<User, CreateUserDto>('/api/users', {
name: 'John',
email: 'john@example.com'
});
// Generic для состояния компонента
interface State<T> {
data: T | null;
loading: boolean;
error: string | null;
}

function createInitialState<T>(): State<T> {
return {
data: null,
loading: false,
error: null
};
}

const userState: State<User> = createInitialState<User>();
const productState: State<Product[]> = createInitialState<Product[]>();

6. Utility Types на основе Generics

// Встроенные utility types
type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// Использование
interface User {
id: number;
name: string;
email: string;
password: string;
}

type UserUpdate = Partial<User>; // Все поля опциональны
type UserPublic = Omit<User, 'password'>; // Без password
type UserPreview = Pick<User, 'id' | 'name'>; // Только id и name

Когда использовать Generics:

  • Функции работают с разными типами, но сохраняют связь между входом и выходом
  • Классы и интерфейсы должны быть типизированы гибко
  • Нужна типобезопасность при переиспользовании кода

Вопрос 50. Что такое Generics в TypeScript?

Таймкод: 01:00:26

Ответ собеседника: Правильный. Определил Generics как механизм с отложенным определением типа.

Правильный ответ:

Generics — параметризованные типы, позволяющие создавать компоненты, работающие с разными типами без потери типизации.

1. Базовый синтаксис

// Функция с Generic
function identity<T>(value: T): T {
return value;
}

const str = identity<string>('hello'); // string
const num = identity<number>(42); // number
const bool = identity(true); // boolean (вывод автоматически)

2. Generic для интерфейсов

interface ApiResponse<T> {
data: T;
status: number;
error?: string;
}

interface User {
id: number;
name: string;
}

interface Product {
id: number;
title: string;
price: number;
}

// Использование
const userResponse: ApiResponse<User> = {
data: { id: 1, name: 'John' },
status: 200
};

const productResponse: ApiResponse<Product> = {
data: { id: 1, title: 'Laptop', price: 999 },
status: 200
};

3. Generic для классов

class Queue<T> {
private items: T[] = [];

enqueue(item: T): void {
this.items.push(item);
}

dequeue(): T | undefined {
return this.items.shift();
}

peek(): T | undefined {
return this.items[0];
}

size(): number {
return this.items.length;
}
}

// Использование
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
const first: number | undefined = numberQueue.dequeue();

const stringQueue = new Queue<string>();
stringQueue.enqueue('hello');

4. Ограничения (Constraints)

// Ограничение по интерфейсу
interface HasLength {
length: number;
}

function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}

logLength('hello'); // 5
logLength([1, 2, 3]); // 3
logLength({ length: 10 }); // 10
// logLength(42); // Ошибка: number не имеет length

// Ограничение по ключу
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user = { id: 1, name: 'John', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'email'); // Ошибка: 'email' не существует

5. Практические примеры

// Generic для API-клиента
class ApiClient {
async get<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json() as T;
}

async post<T, D>(url: string, data: D): Promise<T> {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json() as T;
}
}

const api = new ApiClient();

// Тип определяется при вызове
const user = await api.get<User>('/api/users/1');
const newUser = await api.post<User, CreateUserDto>('/api/users', {
name: 'John',
email: 'john@example.com'
});
// Generic для состояния компонента
interface State<T> {
data: T | null;
loading: boolean;
error: string | null;
}

function createInitialState<T>(): State<T> {
return {
data: null,
loading: false,
error: null
};
}

const userState: State<User> = createInitialState<User>();
const productState: State<Product[]> = createInitialState<Product[]>();

6. Utility Types на основе Generics

// Встроенные utility types
type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// Использование
interface User {
id: number;
name: string;
email: string;
password: string;
}

type UserUpdate = Partial<User>; // Все поля опциональны
type UserPublic = Omit<User, 'password'>; // Без password
type UserPreview = Pick<User, 'id' | 'name'>; // Только id и name

Когда использовать Generics:

  • Функции работают с разными типами, но сохраняют связь между входом и выходом
  • Классы и интерфейсы должны быть типизированы гибко
  • Нужна типобезопасность при переиспользовании кода

Вопрос 51. Как типизировать непустую строку или строку с определённым форматом?

Таймкод: 01:01:14

Ответ собеседния: Неполный. Предложил Type Guard, но не знал про Template Literal Types.

Правильный ответ:

TypeScript предоставляет несколько способов типизации строк с ограничениями.

1. Template Literal Types

// Строка, начинающаяся с подчёркивания
type PrefixedKey = `_${string}`;

const validKey: PrefixedKey = '_userId'; // OK
// const invalidKey: PrefixedKey = 'userId'; // Ошибка

// Email тип
type Email = `${string}@${string}.${string}`;

const email: Email = 'user@example.com'; // OK
// const invalid: Email = 'not-email'; // Ошибка

// Строка с определённым префиксом и суффиксом
type ApiEndpoint = `/api/${string}`;

const endpoint: ApiEndpoint = '/api/users'; // OK
// const invalid: ApiEndpoint = '/other'; // Ошибка

2. Branded Types для непустой строки

// Базовый тип с брендом
type NonEmptyString = string & { __brand: 'NonEmptyString' };

// Функция-валидатор
function createNonEmptyString(value: string): NonEmptyString {
if (value.length === 0) {
throw new Error('String cannot be empty');
}
return value as NonEmptyString;
}

// Использование
const valid: NonEmptyString = createNonEmptyString('hello');
// const invalid: NonEmptyString = ''; // Ошибка в runtime

function greet(name: NonEmptyString): string {
return `Hello, ${name}!`;
}

greet(valid); // OK
// greet(''); // Ошибка компиляции

3. Conditional Types с infer

// Извлечение типа из шаблона
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;

type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'

4. Mapped Types для ключей объекта

// Тип для объекта с ключами, начинающимися с подчёркивания
type PrefixedObject = {
[K in `_${string}`]: any;
};

const obj: PrefixedObject = {
_id: 1,
_name: 'John',
// id: 2, // Ошибка
};

// Фильтрация ключей по шаблону
type FilterKeys<T, Pattern extends string> = {
[K in keyof T as K extends Pattern ? K : never]: T[K];
};

interface User {
_id: number;
_email: string;
name: string;
age: number;
}

type InternalFields = FilterKeys<User, `_${string}`>;
// { _id: number; _email: string }

5. Практические примеры

// Типизированные CSS-классы
type BEMModifier = `--${string}`;
type BEMElement = `__${string}`;

function createModifier(name: string): BEMModifier {
return `--${name}` as BEMModifier;
}

// Типизированные URL
type ApiUrl = `https://api.${string}/${string}`;
type WebSocketUrl = `wss://${string}`;

const apiUrl: ApiUrl = 'https://api.example.com/users';
const wsUrl: WebSocketUrl = 'wss://realtime.example.com';

// Типизированные идентификаторы
type UserId = `user_${number}`;
type OrderId = `order_${number}`;

function createUserId(id: number): UserId {
return `user_${id}`;
}

const userId: UserId = createUserId(123);
// const invalid: UserId = 'user_abc'; // Ошибка

6. Комбинирование подходов

// Строгая типизация для API
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoute = `/api/v${number}/${string}`;

interface TypedRequest<TMethod extends HttpMethod, TRoute extends ApiRoute> {
method: TMethod;
route: TRoute;
body?: unknown;
}

type GetUserRequest = TypedRequest<'GET', '/api/v1/users'>;
type CreateUserRequest = TypedRequest<'POST', '/api/v1/users'>;

Сравнение подходов:

ПодходПроверкаСложностьГибкость
Template LiteralКомпиляцияНизкаяСредняя
Branded TypesRuntime + компиляцияСредняяВысокая
Conditional TypesКомпиляцияВысокаяВысокая
Type GuardRuntimeНизкаяСредняя

Когда что использовать:

  • Template Literal Types — простые шаблоны (префиксы, суффиксы)
  • Branded Types — нужна валидация в runtime
  • Conditional Types — сложная логика извлечения типов
  • Type Guards — проверка значений из внешних источников

Вопрос 51. Как типизировать непустую строку или строку с определённым форматом?

Таймкод: 01:01:14

Ответ собеседния: Неполный. Предложил Type Guard, но не знал про Template Literal Types.

Правильный ответ:

TypeScript предоставляет несколько способов типизации строк с ограничениями.

1. Template Literal Types

// Строка, начинающаяся с подчёркивания
type PrefixedKey = `_${string}`;

const validKey: PrefixedKey = '_userId'; // OK
// const invalidKey: PrefixedKey = 'userId'; // Ошибка

// Email тип
type Email = `${string}@${string}.${string}`;

const email: Email = 'user@example.com'; // OK
// const invalid: Email = 'not-email'; // Ошибка

// Строка с определённым префиксом и суффиксом
type ApiEndpoint = `/api/${string}`;

const endpoint: ApiEndpoint = '/api/users'; // OK
// const invalid: ApiEndpoint = '/other'; // Ошибка

2. Branded Types для непустой строки

// Базовый тип с брендом
type NonEmptyString = string & { __brand: 'NonEmptyString' };

// Функция-валидатор
function createNonEmptyString(value: string): NonEmptyString {
if (value.length === 0) {
throw new Error('String cannot be empty');
}
return value as NonEmptyString;
}

// Использование
const valid: NonEmptyString = createNonEmptyString('hello');
// const invalid: NonEmptyString = ''; // Ошибка в runtime

function greet(name: NonEmptyString): string {
return `Hello, ${name}!`;
}

greet(valid); // OK
// greet(''); // Ошибка компиляции

3. Conditional Types с infer

// Извлечение типа из шаблона
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;

type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'

4. Mapped Types для ключей объекта

// Тип для объекта с ключами, начинающимися с подчёркивания
type PrefixedObject = {
[K in `_${string}`]: any;
};

const obj: PrefixedObject = {
_id: 1,
_name: 'John',
// id: 2, // Ошибка
};

// Фильтрация ключей по шаблону
type FilterKeys<T, Pattern extends string> = {
[K in keyof T as K extends Pattern ? K : never]: T[K];
};

interface User {
_id: number;
_email: string;
name: string;
age: number;
}

type InternalFields = FilterKeys<User, `_${string}`>;
// { _id: number; _email: string }

5. Практические примеры

// Типизированные CSS-классы
type BEMModifier = `--${string}`;
type BEMElement = `__${string}`;

function createModifier(name: string): BEMModifier {
return `--${name}` as BEMModifier;
}

// Типизированные URL
type ApiUrl = `https://api.${string}/${string}`;
type WebSocketUrl = `wss://${string}`;

const apiUrl: ApiUrl = 'https://api.example.com/users';
const wsUrl: WebSocketUrl = 'wss://realtime.example.com';

// Типизированные идентификаторы
type UserId = `user_${number}`;
type OrderId = `order_${number}`;

function createUserId(id: number): UserId {
return `user_${id}`;
}

const userId: UserId = createUserId(123);
// const invalid: UserId = 'user_abc'; // Ошибка

6. Комбинирование подходов

// Строгая типизация для API
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoute = `/api/v${number}/${string}`;

interface TypedRequest<TMethod extends HttpMethod, TRoute extends ApiRoute> {
method: TMethod;
route: TRoute;
body?: unknown;
}

type GetUserRequest = TypedRequest<'GET', '/api/v1/users'>;
type CreateUserRequest = TypedRequest<'POST', '/api/v1/users'>;

Сравнение подходов:

ПодходПроверкаСложностьГибкость
Template LiteralКомпиляцияНизкаяСредняя
Branded TypesRuntime + компиляцияСредняяВысокая
Conditional TypesКомпиляцияВысокаяВысокая
Type GuardRuntimeНизкаяСредняя

Когда что использовать:

  • Template Literal Types — простые шаблоны (префиксы, суффиксы)
  • Branded Types — нужна валидация в runtime
  • Conditional Types — сложная логика извлечения типов
  • Type Guards — проверка значений из внешних источников

Вопрос 51. Как типизировать непустую строку или строку с определённым форматом?

Таймкод: 01:01:14

Ответ собеседния: Неполный. Предложил Type Guard, но не знал про Template Literal Types.

Правильный ответ:

TypeScript предоставляет несколько способов типизации строк с ограничениями.

1. Template Literal Types

// Строка, начинающаяся с подчёркивания
type PrefixedKey = `_${string}`;

const validKey: PrefixedKey = '_userId'; // OK
// const invalidKey: PrefixedKey = 'userId'; // Ошибка

// Email тип
type Email = `${string}@${string}.${string}`;

const email: Email = 'user@example.com'; // OK
// const invalid: Email = 'not-email'; // Ошибка

// Строка с определённым префиксом и суффиксом
type ApiEndpoint = `/api/${string}`;

const endpoint: ApiEndpoint = '/api/users'; // OK
// const invalid: ApiEndpoint = '/other'; // Ошибка

2. Branded Types для непустой строки

// Базовый тип с брендом
type NonEmptyString = string & { __brand: 'NonEmptyString' };

// Функция-валидатор
function createNonEmptyString(value: string): NonEmptyString {
if (value.length === 0) {
throw new Error('String cannot be empty');
}
return value as NonEmptyString;
}

// Использование
const valid: NonEmptyString = createNonEmptyString('hello');
// const invalid: NonEmptyString = ''; // Ошибка в runtime

function greet(name: NonEmptyString): string {
return `Hello, ${name}!`;
}

greet(valid); // OK
// greet(''); // Ошибка компиляции

3. Conditional Types с infer

// Извлечение типа из шаблона
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;

type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'

4. Mapped Types для ключей объекта

// Тип для объекта с ключами, начинающимися с подчёркивания
type PrefixedObject = {
[K in `_${string}`]: any;
};

const obj: PrefixedObject = {
_id: 1,
_name: 'John',
// id: 2, // Ошибка
};

// Фильтрация ключей по шаблону
type FilterKeys<T, Pattern extends string> = {
[K in keyof T as K extends Pattern ? K : never]: T[K];
};

interface User {
_id: number;
_email: string;
name: string;
age: number;
}

type InternalFields = FilterKeys<User, `_${string}`>;
// { _id: number; _email: string }

5. Практические примеры

// Типизированные CSS-классы
type BEMModifier = `--${string}`;
type BEMElement = `__${string}`;

function createModifier(name: string): BEMModifier {
return `--${name}` as BEMModifier;
}

// Типизированные URL
type ApiUrl = `https://api.${string}/${string}`;
type WebSocketUrl = `wss://${string}`;

const apiUrl: ApiUrl = 'https://api.example.com/users';
const wsUrl: WebSocketUrl = 'wss://realtime.example.com';

// Типизированные идентификаторы
type UserId = `user_${number}`;
type OrderId = `order_${number}`;

function createUserId(id: number): UserId {
return `user_${id}`;
}

const userId: UserId = createUserId(123);
// const invalid: UserId = 'user_abc'; // Ошибка

6. Комбинирование подходов

// Строгая типизация для API
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoute = `/api/v${number}/${string}`;

interface TypedRequest<TMethod extends HttpMethod, TRoute extends ApiRoute> {
method: TMethod;
route: TRoute;
body?: unknown;
}

type GetUserRequest = TypedRequest<'GET', '/api/v1/users'>;
type CreateUserRequest = TypedRequest<'POST', '/api/v1/users'>;

Сравнение подходов:

ПодходПроверкаСложностьГибкость
Template LiteralКомпиляцияНизкаяСредняя
Branded TypesRuntime + компиляцияСредняяВысокая
Conditional TypesКомпиляцияВысокаяВысокая
Type GuardRuntimeНизкаяСредняя

Когда что использовать:

  • Template Literal Types — простые шаблоны (префиксы, суффиксы)
  • Branded Types — нужна валидация в runtime
  • Conditional Types — сложная логика извлечения типов
  • Type Guards — проверка значений из внешних источников

Вопрос 52. Какие способы изоляции имён классов в CSS существуют?

Таймкод: 01:05:35

Ответ собеседника: Правильный. Назвал CSS-модули, styled-components, Tailwind и БЭМ.

Правильный ответ:

Современные способы:

1. CSS Modules

/* Button.module.css */
.button {
padding: 10px 20px;
}

.primary {
background: blue;
}
import styles from './Button.module.css';

// Генерирует уникальный класс: Button_button__3x7a2
<button className={styles.button}>Click</button>

2. CSS-in-JS (styled-components, emotion)

import styled from 'styled-components';

const Button = styled.button`
padding: 10px 20px;
background: ${props => props.primary ? 'blue' : 'gray'};
`;

// Генерирует уникальный класс: sc-bdVaJa
<Button primary>Click</Button>

3. Tailwind CSS

// Утилитарные классы, конфликтов практически нет
<button className="px-5 py-2 bg-blue-500 text-white rounded">
Click
</button>

4. Shadow DOM (Web Components)

class MyComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.button { padding: 10px 20px; }
</style>
<button class="button">Click</button>
`;
}
}

«Древние» техники:

5. БЭМ (Блок-Элемент-Модификатор)

/* Блок */
.menu { }

/* Элемент */
.menu__item { }
.menu__link { }

/* Модификатор */
.menu__item--active { }
.menu--vertical { }

6. Префиксы вручную

/* Использование уникального префикса проекта/компонента */
.myapp-button { }
.myapp-header { }
.user-card-avatar { }

7. Инлайн-стили

// Полная изоляция, но ограниченные возможности
<div style={{ padding: '10px', background: 'blue' }}>Content</div>

8. iframe

<!-- Полная изоляция через отдельный документ -->
<iframe src="isolated-component.html"></iframe>

9. CSS Scoping (экспериментальный)

<style scoped>
.button { padding: 10px; }
</style>

Сравнение подходов:

ПодходИзоляцияПроизводительностьСложность
CSS ModulesВысокаяВысокаяНизкая
CSS-in-JSПолнаяСредняяСредняя
TailwindВысокаяВысокаяНизкая
Shadow DOMПолнаяВысокаяВысокая
БЭМЧастичнаяВысокаяНизкая
ПрефиксыЧастичнаяВысокаяНизкая
Инлайн-стилиПолнаяВысокаяНизкая

Рекомендации:

  • Новые проекты: CSS Modules или Tailwind
  • Библиотека компонентов: CSS-in-JS
  • Максимальная изоляция: Shadow DOM
  • Legacy-проекты: БЭМ с автоматической проверкой через линтеры

Вопрос 53. Что такое веб-компоненты?

Таймкод: 01:06:43

Ответ собеседника: Неполный. Описал идею кастомных элементов, но не упомянул Shadow DOM, Custom Elements, HTML Templates.

Правильный ответ:

Веб-компоненты — набор веб-стандартов для создания переиспользуемых кастомных HTML-элементов с инкапсуляцией стилей и поведения.

Три ключевые технологии:

1. Custom Elements (Пользовательские элементы)

// Определение кастомного элемента
class MyButton extends HTMLElement {
constructor() {
super();
this.textContent = 'Click me';
this.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('my-click', {
detail: { timestamp: Date.now() }
}));
});
}
}

// Регистрация
customElements.define('my-button', MyButton);

// Использование в HTML
// <my-button></my-button>
// С жизненным циклом
class UserCard extends HTMLElement {
static get observedAttributes() {
return ['name', 'email'];
}

constructor() {
super();
this.attachShadow({ mode: 'open' });
}

// Вызывается при добавлении в DOM
connectedCallback() {
this.render();
}

// Вызывается при удалении из DOM
disconnectedCallback() {
console.log('UserCard removed');
}

// Вызывается при изменении атрибута
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}

render() {
const name = this.getAttribute('name') || 'Unknown';
const email = this.getAttribute('email') || '';

this.shadowRoot.innerHTML = `
<style>
.card { padding: 16px; border: 1px solid #ddd; }
.name { font-weight: bold; }
</style>
<div class="card">
<div class="name">${name}</div>
<div class="email">${email}</div>
</div>
`;
}
}

customElements.define('user-card', UserCard);

2. Shadow DOM (Теневое DOM-дерево)

class IsolatedComponent extends HTMLElement {
constructor() {
super();

// Создание Shadow DOM
const shadow = this.attachShadow({ mode: 'open' });

// Стили изолированы от внешнего документа
shadow.innerHTML = `
<style>
.container {
background: lightblue;
padding: 20px;
}
/* Стили НЕ применяются к внешним элементам */
p { color: red; }
</style>
<div class="container">
<slot></slot> <!-- Слот для контента -->
</div>
`;
}
}

customElements.define('isolated-component', IsolatedComponent);
<!-- Использование слотов -->
<isolated-component>
<p>Этот текст будет в слоте</p>
</isolated-component>

<!-- Именованные слоты -->
<template id="my-template">
<style>
.header { font-size: 24px; }
.content { padding: 10px; }
</style>
<div class="header">
<slot name="header">Default Header</slot>
</div>
<div class="content">
<slot>Default content</slot>
</div>
</template>

<my-component>
<h2 slot="header">Custom Header</h2>
<p>Custom content</p>
</my-component>

3. HTML Templates

<!-- Шаблон не отображается на странице -->
<template id="my-card-template">
<style>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
margin: 10px;
}
.title { font-size: 18px; font-weight: bold; }
.description { color: #666; }
</style>
<div class="card">
<div class="title"></div>
<div class="description"></div>
</div>
</template>
// Клонирование и использование шаблона
class CardComponent extends HTMLElement {
constructor() {
super();
const template = document.getElementById('my-card-template');
const content = template.content.cloneNode(true);

content.querySelector('.title').textContent = this.getAttribute('title');
content.querySelector('.description').textContent = this.getAttribute('desc');

this.appendChild(content);
}
}

customElements.define('card-component', CardComponent);

Практический пример — полноценный компонент:

class ModalDialog extends HTMLElement {
static get observedAttributes() {
return ['open', 'title'];
}

constructor() {
super();
this.attachShadow({ mode: 'open' });
this._isOpen = false;
}

connectedCallback() {
this.render();
this.setupEventListeners();
}

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'open') {
this._isOpen = newValue !== null;
this.updateVisibility();
}
if (name === 'title') {
const titleEl = this.shadowRoot.querySelector('.modal-title');
if (titleEl) titleEl.textContent = newValue;
}
}

setupEventListeners() {
this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => {
this.removeAttribute('open');
this.dispatchEvent(new CustomEvent('close'));
});

this.shadowRoot.querySelector('.overlay').addEventListener('click', () => {
this.removeAttribute('open');
this.dispatchEvent(new CustomEvent('close'));
});
}

updateVisibility() {
const modal = this.shadowRoot.querySelector('.modal');
modal.style.display = this._isOpen ? 'flex' : 'none';
}

render() {
this.shadowRoot.innerHTML = `
<style>
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
z-index: 1000;
}
.overlay {
position: absolute;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.content {
position: relative;
background: white;
padding: 20px;
border-radius: 8px;
min-width: 300px;
}
.close-btn {
position: absolute;
top: 10px;
right: 10px;
border: none;
background: none;
font-size: 20px;
cursor: pointer;
}
</style>
<div class="modal">
<div class="overlay"></div>
<div class="content">
<button class="close-btn">&times;</button>
<h2 class="modal-title">${this.getAttribute('title') || 'Modal'}</h2>
<slot></slot>
</div>
</div>
`;
this.updateVisibility();
}
}

customElements.define('modal-dialog', ModalDialog);
<!-- Использование -->
<modal-dialog title="Confirm Action">
<p>Are you sure you want to delete this item?</p>
<button id="confirm-btn">Delete</button>
</modal-dialog>

<script>
const modal = document.querySelector('modal-dialog');
const confirmBtn = document.getElementById('confirm-btn');

confirmBtn.addEventListener('click', () => {
modal.setAttribute('open', '');
});

modal.addEventListener('close', () => {
console.log('Modal closed');
});
</script>

Преимущества веб-компонентов:

  • Полная инкапсуляция стилей через Shadow DOM
  • Переиспользуемость без фреймворков
  • Нативная поддержка браузерами
  • Совместимость с любыми фреймворками (React, Vue, Angular)

Ограничения:

  • Нет реактивности «из коробки»
  • Сложнее управлять состоянием
  • SSR требует дополнительных решений
  • Меньше экосистемы по сравнению с фреймворками

Вопрос 53. Что такое веб-компоненты?

Таймкод: 01:06:43

Ответ собеседника: Неполный. Описал идею кастомных элементов, но не упомянул Shadow DOM, Custom Elements, HTML Templates.

Правильный ответ:

Веб-компоненты — набор веб-стандартов для создания переиспользуемых кастомных HTML-элементов с инкапсуляцией стилей и поведения.

Три ключевые технологии:

1. Custom Elements (Пользовательские элементы)

// Определение кастомного элемента
class MyButton extends HTMLElement {
constructor() {
super();
this.textContent = 'Click me';
this.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('my-click', {
detail: { timestamp: Date.now() }
}));
});
}
}

// Регистрация
customElements.define('my-button', MyButton);

// Использование в HTML
// <my-button></my-button>
// С жизненным циклом
class UserCard extends HTMLElement {
static get observedAttributes() {
return ['name', 'email'];
}

constructor() {
super();
this.attachShadow({ mode: 'open' });
}

// Вызывается при добавлении в DOM
connectedCallback() {
this.render();
}

// Вызывается при удалении из DOM
disconnectedCallback() {
console.log('UserCard removed');
}

// Вызывается при изменении атрибута
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}

render() {
const name = this.getAttribute('name') || 'Unknown';
const email = this.getAttribute('email') || '';

this.shadowRoot.innerHTML = `
<style>
.card { padding: 16px; border: 1px solid #ddd; }
.name { font-weight: bold; }
</style>
<div class="card">
<div class="name">${name}</div>
<div class="email">${email}</div>
</div>
`;
}
}

customElements.define('user-card', UserCard);

2. Shadow DOM (Теневое DOM-дерево)

class IsolatedComponent extends HTMLElement {
constructor() {
super();

// Создание Shadow DOM
const shadow = this.attachShadow({ mode: 'open' });

// Стили изолированы от внешнего документа
shadow.innerHTML = `
<style>
.container {
background: lightblue;
padding: 20px;
}
/* Стили НЕ применяются к внешним элементам */
p { color: red; }
</style>
<div class="container">
<slot></slot> <!-- Слот для контента -->
</div>
`;
}
}

customElements.define('isolated-component', IsolatedComponent);
<!-- Использование слотов -->
<isolated-component>
<p>Этот текст будет в слоте</p>
</isolated-component>

<!-- Именованные слоты -->
<template id="my-template">
<style>
.header { font-size: 24px; }
.content { padding: 10px; }
</style>
<div class="header">
<slot name="header">Default Header</slot>
</div>
<div class="content">
<slot>Default content</slot>
</div>
</template>

<my-component>
<h2 slot="header">Custom Header</h2>
<p>Custom content</p>
</my-component>

3. HTML Templates

<!-- Шаблон не отображается на странице -->
<template id="my-card-template">
<style>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
margin: 10px;
}
.title { font-size: 18px; font-weight: bold; }
.description { color: #666; }
</style>
<div class="card">
<div class="title"></div>
<div class="description"></div>
</div>
</template>
// Клонирование и использование шаблона
class CardComponent extends HTMLElement {
constructor() {
super();
const template = document.getElementById('my-card-template');
const content = template.content.cloneNode(true);

content.querySelector('.title').textContent = this.getAttribute('title');
content.querySelector('.description').textContent = this.getAttribute('desc');

this.appendChild(content);
}
}

customElements.define('card-component', CardComponent);

Практический пример — полноценный компонент:

class ModalDialog extends HTMLElement {
static get observedAttributes() {
return ['open', 'title'];
}

constructor() {
super();
this.attachShadow({ mode: 'open' });
this._isOpen = false;
}

connectedCallback() {
this.render();
this.setupEventListeners();
}

attributeChangedCallback(name, oldValue, newValue) {
if (name === 'open') {
this._isOpen = newValue !== null;
this.updateVisibility();
}
if (name === 'title') {
const titleEl = this.shadowRoot.querySelector('.modal-title');
if (titleEl) titleEl.textContent = newValue;
}
}

setupEventListeners() {
this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => {
this.removeAttribute('open');
this.dispatchEvent(new CustomEvent('close'));
});

this.shadowRoot.querySelector('.overlay').addEventListener('click', () => {
this.removeAttribute('open');
this.dispatchEvent(new CustomEvent('close'));
});
}

updateVisibility() {
const modal = this.shadowRoot.querySelector('.modal');
modal.style.display = this._isOpen ? 'flex' : 'none';
}

render() {
this.shadowRoot.innerHTML = `
<style>
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
z-index: 1000;
}
.overlay {
position: absolute;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.content {
position: relative;
background: white;
padding: 20px;
border-radius: 8px;
min-width: 300px;
}
.close-btn {
position: absolute;
top: 10px;
right: 10px;
border: none;
background: none;
font-size: 20px;
cursor: pointer;
}
</style>
<div class="modal">
<div class="overlay"></div>
<div class="content">
<button class="close-btn">&times;</button>
<h2 class="modal-title">${this.getAttribute('title') || 'Modal'}</h2>
<slot></slot>
</div>
</div>
`;
this.updateVisibility();
}
}

customElements.define('modal-dialog', ModalDialog);
<!-- Использование -->
<modal-dialog title="Confirm Action">
<p>Are you sure you want to delete this item?</p>
<button id="confirm-btn">Delete</button>
</modal-dialog>

<script>
const modal = document.querySelector('modal-dialog');
const confirmBtn = document.getElementById('confirm-btn');

confirmBtn.addEventListener('click', () => {
modal.setAttribute('open', '');
});

modal.addEventListener('close', () => {
console.log('Modal closed');
});
</script>

Преимущества веб-компонентов:

  • Полная инкапсуляция стилей через Shadow DOM
  • Переиспользуемость без фреймворков
  • Нативная поддержка браузерами
  • Совместимость с любыми фреймворками (React, Vue, Angular)

Ограничения:

  • Нет реактивности «из коробки»
  • Сложнее управлять состоянием
  • SSR требует дополнительных решений
  • Меньше экосистемы по сравнению с фреймворками

Вопрос 54. Что понимаете под архитектурой фронтенд-приложения?

Таймкод: 01:08:16

Ответ собеседника: Правильный. Назвал модульную архитектуру, FSD и Atomic Design.

Правильный ответ:

Архитектура фронтенд-приложения — это организация кода, структура модулей, потоки данных и взаимодействие компонентов.

Основные архитектурные подходы:

1. Feature-Sliced Design (FSD)

src/
├── shared/ # Переиспользуемый код
│ ├── ui/ # UI-компоненты
│ ├── lib/ # Утилиты
│ └── config/ # Конфигурация
├── entities/ # Бизнес-сущности
│ ├── user/
│ └── product/
├── features/ # Пользовательские фичи
│ ├── auth/
│ └── cart/
├── widgets/ # Компоненты страниц
│ └── header/
└── pages/ # Страницы
├── home/
└── profile/

Правила FSD:

  • Слои могут импортировать только нижележащие слои
  • Shared не зависит от других слоёв
  • Entities не знают о features и widgets
  • Pages могут использовать всё

2. Atomic Design

atoms/ # Базовые элементы (Button, Input, Label)
molecules/ # Комбинации атомов (SearchField, FormField)
organisms/ # Сложные компоненты (Header, ProductCard)
templates/ # Шаблоны страниц
pages/ # Конкретные страницы с данными

3. MVVM / MVC на фронтенде

Model → State management (Redux, Zustand)
View → React/Vue компоненты
ViewModel → Custom hooks / Composables

4. Clean Architecture

presentation/ # UI компоненты
domain/ # Бизнес-логика, entities
data/ # API, repositories

5. Micro Frontends

shell/ # Контейнер приложения
├── auth-mfe/ # Микрофронтенд авторизации
├── products-mfe/# Микрофронтенд продуктов
└── cart-mfe/ # Микрофронтенд корзины

Выбор архитектуры зависит от:

  • Размера команды
  • Сложности домена
  • Требований к масштабируемости
  • Необходимости независимого деплоя

Вопрос 55. Какие существуют способы рендеринга приложения и в чём их разница?

Таймкод: 01:10:22

Ответ собеседника: Правильный. Назвал CSR, SSR, SSG и ISR.

Правильный ответ:

Существует четыре основных подхода к рендерингу веб-приложений, каждый имеет свои преимущества и ограничения.

1. CSR (Client-Side Rendering)

При CSR сервер отправляет минимальный HTML-файл и JavaScript-бандл, а весь рендеринг происходит в браузере.

<!-- index.html -->
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div idroot"></div>
<script src="bundle.js"></script>
</body>
</html>
// React пример CSR
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Плюсы: быстрая навигация после загрузки, богатый интерактивный интерфейс.

Минусы: плохой SEO, долгий First Contentful Paint, зависимость от JavaScript.

2. SSR (Server-Side Rendering)

Сервер генерирует полный HTML для каждого запроса.

// Next.js пример SSR
export async function getServerSideProps(context) {
const { req, res, params } = context;
const data = await fetch(`https://api.example.com/users/${params.id}`);
const user = await data.json();

return {
props: { user },
};
}

export default function UserPage({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}

Плюсы: отличный SEO, быстрый First Contentful Paint, работает без JavaScript.

Минусы: высокая нагрузка на сервер, медленнее TTFB (Time to First Byte).

3. SSG (Static Site Generation)

HTML генерируется на этапе сборки и отдаётся как статические файлы.

// Next.js пример SSG
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();

return {
props: { posts },
// Опционально: ревалидация каждые 60 секунд
// revalidate: 60,
};
}

export async function getStaticPaths() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();

const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}));

return { paths, fallback: false };
}

export default function PostPage({ posts }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

Плюсы: максимальная производительность, можно раздавать через CDN, безопасность.

Минусы: долгая сборка при большом количестве страниц, данные могут устаревать.

4. ISR (Incremental Static Regeneration)

Гибридный подход: страницы генерируются статически, но могут обновляться на сервере.

// Next.js пример ISR
export async function getStaticProps() {
const res = await fetch('https://api.example.com/products');
const products = await res.json();

return {
props: { products },
// Ревал

Вопрос 56. Задача на порядок выполнения кода: в каком порядке выполнятся console.log, Promise.resolve, setTimeout?

Таймкод: 01:13:05

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

Правильный ответ:

Порядок выполнения кода в JavaScript определяется EventLoop и двумя типами очередей задач.

Принцип работы EventLoop:

1. Синхронный код выполняется первым

console.log('1'); // Выполняется сразу
console.log('2'); // Выполняется сразу

2. Микрозадачи (Microtask Queue)

  • Promise.then/catch/finally
  • queueMicrotask()
  • MutationObserver
  • process.nextTick (Node.js, высший приоритет)

3. Макрозадачи (Macrotask/Task Queue)

  • setTimeout/setInterval
  • setImmediate (Node.js)
  • I/O операции
  • UI rendering (браузер)
  • requestAnimationFrame (браузер)

Пример с разбором:

console.log('start');

setTimeout(() => {
console.log('setTimeout');
}, 0);

Promise.resolve()
.then(() => {
console.log('promise 1');
})
.then(() => {
console.log('promise 2');
});

console.log('end');

Порядок выполнения:

start → синхронный код
end → синхронный код
promise 1 → микрозадача
promise 2 → микрозадача
setTimeout → макрозадача

Более сложный пример:

console.log('1');

setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);

Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});

console.log('6');

Порядок: 1 → 6 → 4 → 2 → 3 → 5

Разбор:

  1. 1 — синхронный
  2. 6 — синхронный
  3. 4 — микрозадача из Promise
  4. 2 — макрозадача из setTimeout
  5. 3 — микрозадача, добавлена во время выполнения макрозадачи
  6. 5 — макрозадача, добавлена во время выполнения микрозадачи

Правило: После каждой макрозадачи выполняются ВСЕ микрозадачи из очереди, прежде чем начнётся следующая макрозадача.

Визуализация цикла:

┌─────────────────────────────┐
│ Синхронный код │
└──────────────┬──────────────┘

┌─────────────────────────────┐
│ Микрозадачи (все) │◄────┐
│ - Promise.then │ │
│ - queueMicrotask │ │
└──────────────┬──────────────┘ │
▼ │
┌─────────────────────────────┐ │
│ Макрозадача (одна) │ │
│ - setTimeout │─────┘
│ - setInterval │
└──────────────┬──────────────┘

(повтор)

Практический совет: Для точного понимания порядка выполнения можно использовать инструмент Loupe (http://latentflip.com/loupe/), который визуализирует работу EventLoop.

Вопрос 56. Задача на порядок выполнения кода: в каком порядке выполнятся console.log, Promise.resolve, setTimeout?

Таймкод: 01:13:05

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

Правильный ответ:

Порядок выполнения кода в JavaScript определяется EventLoop и двумя типами очередей задач.

Принцип работы EventLoop:

1. Синхронный код выполняется первым

console.log('1'); // Выполняется сразу
console.log('2'); // Выполняется сразу

2. Микрозадачи (Microtask Queue)

  • Promise.then/catch/finally
  • queueMicrotask()
  • MutationObserver
  • process.nextTick (Node.js, высший приоритет)

3. Макрозадачи (Macrotask/Task Queue)

  • setTimeout/setInterval
  • setImmediate (Node.js)
  • I/O операции
  • UI rendering (браузер)
  • requestAnimationFrame (браузер)

Пример с разбором:

console.log('start');

setTimeout(() => {
console.log('setTimeout');
}, 0);

Promise.resolve()
.then(() => {
console.log('promise 1');
})
.then(() => {
console.log('promise 2');
});

console.log('end');

Порядок выполнения:

start → синхронный код
end → синхронный код
promise 1 → микрозадача
promise 2 → микрозадача
setTimeout → макрозадача

Более сложный пример:

console.log('1');

setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);

Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});

console.log('6');

Порядок: 1 → 6 → 4 → 2 → 3 → 5

Разбор:

  1. 1 — синхронный
  2. 6 — синхронный
  3. 4 — микрозадача из Promise
  4. 2 — макрозадача из setTimeout
  5. 3 — микрозадача, добавлена во время выполнения макрозадачи
  6. 5 — макрозадача, добавлена во время выполнения микрозадачи

Правило: После каждой макрозадачи выполняются ВСЕ микрозадачи из очереди, прежде чем начнётся следующая макрозадача.

Визуализация цикла:

┌─────────────────────────────┐
│ Синхронный код │
└──────────────┬──────────────┘

┌─────────────────────────────┐
│ Микрозадачи (все) │◄────┐
│ - Promise.then │ │
│ - queueMicrotask │ │
└──────────────┬──────────────┘ │
▼ │
┌─────────────────────────────┐ │
│ Макрозадача (одна) │ │
│ - setTimeout │─────┘
│ - setInterval │
└──────────────┬──────────────┘

(повтор)

Практический совет: Для точного понимания порядка выполнения можно использовать инструмент Loupe (http://latentflip.com/loupe/), который визуализирует работу EventLoop.

Вопрос 56. Задача на порядок выполнения кода: в каком порядке выполнятся console.log, Promise.resolve, setTimeout?

Таймкод: 01:13:05

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

Правильный ответ:

Порядок выполнения кода в JavaScript определяется EventLoop и двумя типами очередей задач.

Принцип работы EventLoop:

1. Синхронный код выполняется первым

console.log('1'); // Выполняется сразу
console.log('2'); // Выполняется сразу

2. Микрозадачи (Microtask Queue)

  • Promise.then/catch/finally
  • queueMicrotask()
  • MutationObserver
  • process.nextTick (Node.js, высший приоритет)

3. Макрозадачи (Macrotask/Task Queue)

  • setTimeout/setInterval
  • setImmediate (Node.js)
  • I/O операции
  • UI rendering (браузер)
  • requestAnimationFrame (браузер)

Пример с разбором:

console.log('start');

setTimeout(() => {
console.log('setTimeout');
}, 0);

Promise.resolve()
.then(() => {
console.log('promise 1');
})
.then(() => {
console.log('promise 2');
});

console.log('end');

Порядок выполнения:

start → синхронный код
end → синхронный код
promise 1 → микрозадача
promise 2 → микрозадача
setTimeout → макрозадача

Более сложный пример:

console.log('1');

setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);

Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});

console.log('6');

Порядок: 1 → 6 → 4 → 2 → 3 → 5

Разбор:

  1. 1 — синхронный
  2. 6 — синхронный
  3. 4 — микрозадача из Promise
  4. 2 — макрозадача из setTimeout
  5. 3 — микрозадача, добавлена во время выполнения макрозадачи
  6. 5 — макрозадача, добавлена во время выполнения микрозадачи

Правило: После каждой макрозадачи выполняются ВСЕ микрозадачи из очереди, прежде чем начнётся следующая макрозадача.

Визуализация цикла:

┌─────────────────────────────┐
│ Синхронный код │
└──────────────┬──────────────┘

┌─────────────────────────────┐
│ Микрозадачи (все) │◄────┐
│ - Promise.then │ │
│ - queueMicrotask │ │
└──────────────┬──────────────┘ │
▼ │
┌─────────────────────────────┐ │
│ Макрозадача (одна) │ │
│ - setTimeout │─────┘
│ - setInterval │
└──────────────┬──────────────┘

(повтор)

Практический совет: Для точного понимания порядка выполнения можно использовать инструмент Loupe (http://latentflip.com/loupe/), который визуализирует работу EventLoop.

Вопрос 56. Задача на порядок выполнения кода: в каком порядке выполнятся console.log, Promise.resolve, setTimeout?

Таймкод: 01:13:05

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

Правильный ответ:

Порядок выполнения кода в JavaScript определяется EventLoop и двумя типами очередей задач.

Принцип работы EventLoop:

1. Синхронный код выполняется первым

console.log('1'); // Выполняется сразу
console.log('2'); // Выполняется сразу

2. Микрозадачи (Microtask Queue)

  • Promise.then/catch/finally
  • queueMicrotask()
  • MutationObserver
  • process.nextTick (Node.js, высший приоритет)

3. Макрозадачи (Macrotask/Task Queue)

  • setTimeout/setInterval
  • setImmediate (Node.js)
  • I/O операции
  • UI rendering (браузер)
  • requestAnimationFrame (браузер)

Пример с разбором:

console.log('start');

setTimeout(() => {
console.log('setTimeout');
}, 0);

Promise.resolve()
.then(() => {
console.log('promise 1');
})
.then(() => {
console.log('promise 2');
});

console.log('end');

Порядок выполнения:

start → синхронный код
end → синхронный код
promise 1 → микрозадача
promise 2 → микрозадача
setTimeout → макрозадача

Более сложный пример:

console.log('1');

setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);

Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});

console.log('6');

Порядок: 1 → 6 → 4 → 2 → 3 → 5

Разбор:

  1. 1 — синхронный
  2. 6 — синхронный
  3. 4 — микрозадача из Promise
  4. 2 — макрозадача из setTimeout
  5. 3 — микрозадача, добавлена во время выполнения макрозадачи
  6. 5 — макрозадача, добавлена во время выполнения микрозадачи

Правило: После каждой макрозадачи выполняются ВСЕ микрозадачи из очереди, прежде чем начнётся следующая макрозадача.

Визуализация цикла:

┌─────────────────────────────┐
│ Синхронный код │
└──────────────┬──────────────┘

┌─────────────────────────────┐
│ Микрозадачи (все) │◄────┐
│ - Promise.then │ │
│ - queueMicrotask │ │
└──────────────┬──────────────┘ │
▼ │
┌─────────────────────────────┐ │
│ Макрозадача (одна) │ │
│ - setTimeout │─────┘
│ - setInterval │
└──────────────┬──────────────┘

(повтор)

Практический совет: Для точного понимания порядка выполнения можно использовать инструмент Loupe (http://latentflip.com/loupe/), который визуализирует работу EventLoop.

Вопрос 57. В каких случаях монорепозиторий полезен, а в каких вреден?

Таймкод: 01:21:05

Ответ собеседника: Правильный. Назвал случаи полезности (совместная разработка UI Kit и приложения) и вредности (небольшие проекты и MVP).

Правильный ответ:

Монорепозиторий (monorepo) — подход к организации кода, при котором несколько проектов хранятся в одном репозитории.

Когда монорепозиторий полезен:

1. Общая кодовая база между проектами

  • UI Kit и приложение в одном репозитории — можно менять компоненты и сразу видеть влияние
  • Общие утилити, типы конфигурации используются несколькими пакетами
  • Микросервисы с общими библиотеками и контрактами

2. Атомарные изменения

  • Изменение API и всех потребителей в одном коммите
  • Обновление зависимости и всех мест использования одновременно
  • Рефакторинг, затрагивающий несколько пакетов

3. Упрощение онбординга

  • Один репозиторий для клонирования
  • Единая система сборки и тестирования
  • Единые стандарты кода и линтеры

4. Координация между командами

  • Легко отслеживать зависимости между командами
  • Простая синхронизация релизов
  • Единое место для code review

Когда монорепозиторий вреден:

1. Маленькие проекты и MVP

  • Нет необходимости в сложной инфраструктуре
  • Избыточная сложность настройки инструментов
  • Медленнее начало разработки

2. Проекты с разными циклами релизов

  • Один пакет релизится ежедневно, другой раз в год
  • Сложно разделить доступ к разным частям репозитория
  • Нужна изоляция между командами

3. Проекты с разными технологическими стеками

  • Frontend на React, backend на Go, ML на Python
  • Разные системы сборки, тестирования, CI/CD
  • Сложно поддерживать единую конфигурацию

4. Проблемы с масштабированием

  • Большой размер репозитория замедляет клонирование
  • CI/CD пайплайны становятся медленными
  • Сложно управлять правами доступа

Инструменты для работы с монорепозиторием:

Nx — продвинутый инструмент с кэшированием и анализом графа зависимостей

// nx.json
{
"npmScope": "myorg",
"affected": {
"defaultBase": "main"
},
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "lint"]
}
}
}
}

Turborepo — быстрый раннер задач с кэшированием

// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
}
}
}

Lerna — классический инструмент для управления пакетами

// lerna.json
{
"version": "independent",
"npmClient": "npm",
"command": {
"publish": {
"conventionalCommits": true
}
}
}

Workspaces (npm/yarn/pnpm) — базовая поддержка на уровне пакетного менеджера

// package.json
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}

Рекомендации по структуре:

monorepo/
├── apps/
│ ├── web/ # Основное веб-приложение
│ ├── admin/ # Админ-панель
│ └── mobile/ # Мобильное приложение
├── packages/
│ ├── ui-kit/ # Общие компоненты
│ ├── utils/ # Общие утилиты
│ ├── types/ # Общие типы
│ └── config/ # Общие конфигурации
├── tools/ # Скрипты и инструменты
└── package.json

Критерии выбора монорепозитория:

  • Более 3 пакетов с общими зависимостями
  • Необходимость атомарных изменений
  • Команда больше 5 человек
  • Готовность инвестировать в инфраструктуру

Вопрос 58. Использовали ли вкладку Performance в Chrome DevTools? Для чего и с каким результатом?

Таймкод: 01:22:24

Ответ собеседника: Правильный. Описал использование Performance рекордера для записи таймлайна, оптимизации производительности и отладки бага с валидацией формы.

Правильный ответ:

Вкладка Performance в Chrome DevTools — мощный инструмент для анализа производительности веб-приложений в реальном времени.

Основные возможности Performance:

1. Запись и анализ таймлайна

При записи захватывается полная информация о работе страницы:

  • Main — основной поток выполнения JavaScript
  • GPU — работа графического процессора
  • Network — сетевые запросы
  • Frames — кадры рендеринга

2. Ключевые метрики производительности

  • FPS (Frames Per Second) — количество кадров в секунду, зелёная линия означает 60fps
  • CPU — загрузка процессора по типам задач
  • NET — время загрузки ресурсов
  • HEAP — использование памяти

3. Анализ конкретных проблем

Долгие задачи (Long Tasks) — блокировка основного потока более 50ms:

// Пример проблемы — тяжёлые вычисления в основном потоке
function processLargeArray(array) {
return array.map(item => {
// Тяжёлая операция
return heavyComputation(item);
});
}

// Решение — вынесение в Web Worker
// worker.js
self.onmessage = function(e) {
const result = e.data.map(item => heavyComputation(item));
self.postMessage(result);
};

// main.js
const worker = new Worker('worker.js');
worker.postMessage(largeArray);
worker.onmessage = (e) => {
console.log('Result:', e.data);
};

Layout Thrashing — многократные пересчёт макета:

// Плохо — чередование чтения и записи
elements.forEach(el => {
const height = el.offsetHeight; // Чтение
el.style.height = height + 10 + 'px'; // Запись — вызывает reflow
});

// Хорошо — батчинг операций
const heights = elements.map(el => el.offsetHeight); // Все чтения
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // Все записи
});

4. Практические сценарии использования

Оптимизация рендеринга:

// Проблема — частые обновления DOM
function updateUI(data) {
data.forEach(item => {
document.getElementById('list').innerHTML += `<div>${item}</div>`;
});
}

// Решение — использование DocumentFragment
function updateUIOptimized(data) {
const fragment = document.createDocumentFragment();
data.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
fragment.appendChild(div);
});
document.getElementById('list').appendChild(fragment);
}

Анализ утечек памяти:

Вкладка Memory в сочетании с Performance позволяет выявить утечки:

// Пример утечки — забытые таймеры
class Component {
constructor() {
this.data = [];
this.timer = setInterval(() => {
this.fetchData();
}, 1000);
}

// Забыли очистить таймер при уничтожении
// destroy() {
// clearInterval(this.timer);
// }
}

// Правильная реализация
class Component {
constructor() {
this.data = [];
this.timer = setInterval(() => {
this.fetchData();
}, 1000);
}

destroy() {
clearInterval(this.timer);
this.data = null;
}
}

5. Горячие клавиши и советы

  • Ctrl+Shift+E — начать/остановить запись
  • W/S — зум таймлайна
  • A/D — навигация по таймлайну
  • Флажок "Screenshots" — захват скриншотов каждого кадра
  • Флажок "Memory" — запись использования памяти

6. Типичные результаты оптимизации

После анализа и оптимизации обычно улучшаются:

  • First Contentful Paint (FCP) — время до первого отображения контента
  • Largest Contentful Paint (LCP) — время до отображения крупнейшего элемента
  • Time to Interactive (TTI) — время до полной интерактивности
  • Cumulative Layout Shift (CLS) — стабильность макета

Отладка багов:

Performance помогает находить причины:

  • Медленной работы интерфейса
  • Зависаний при определённых действиях
  • Неожиданного поведения из-за асинхронных операций
  • Проблем с валидацией (как в примере собеседника)

Вопрос 59. Как оптимизировать рендеринг гигантского списка (~2000 элементов) в React? В чём идея виртуализации?

Таймкод: 01:23:27

Ответ собеседника: Правильный. Описал виртуализацию как отрисовку только видимых элементов, упомянул уменьшение нагрузки на Layout/Painting/Compositing и подтвердил замерами в Performance.

Правильный ответ:

Идея виртуализации

Виртуализация (virtualization) — техника при которой в DOM-дереве находятся только те элементы, которые видны во viewport'е плюс небольшой буфер. Остальные элементы выгружаются из DOM и создаются динамически при скролле.

Почему это работает:

Браузер тратит ресурсы на каждый DOM-узел:

  • Layout — расчёт позиций и размеров
  • Paint отрисовка пикселей
  • Composite — объединение слоёв

2000 элементов = 2000 операций layout/paint. При виртуализации — только ~20-30 видимых элементов.

Реализация виртуализации вручную:

import React, { useState, useRef, useEffect, useMemo } from 'react';

function VirtualizedList({ items, itemHeight = 50, containerHeight = 400 }) {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef(null);

// Количество видимых элементов + буфер
const visibleCount = Math.ceil(containerHeight / itemHeight);
const bufferSize = 5;

// Начальный и конечный индексы
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferSize);
const endIndex = Math.min(
items.length,
startIndex + visibleCount + bufferSize * 2
);

// Видимые элементы
const visibleItems = useMemo(() => {
return items.slice(startIndex, endIndex).map((item, index) => ({
...item,
index: startIndex + index
}));
}, [items, startIndex, endIndex]);

// Общий контейнер для правильного скролла
const totalHeight = items.length * itemHeight;
const offsetY = startIndex * itemHeight;

const handleScroll = (e) => {
setScrollTop(e.target.scrollTop);
};

return (
<div
ref={containerRef}
onScroll={handleScroll}
style={{
height: containerHeight,
overflow: 'auto',
position: 'relative'
}}
>
{/* Пустой div для создания правильной высоты скролла */}
<div style={{ height: totalHeight, position: 'relative' }}>
{/* Контейнер для видимых элементов */}
<div
style={{
position: 'absolute',
top: offsetY,
left: 0,
right: 0
}}
>
{visibleItems.map((item) => (
<div
key={item.id}
style={{
height: itemHeight,
display: 'flex',
alignItems: 'center',
borderBottom: '1px solid #eee'
}}
>
{item.content}
</div>
))}
</div>
</div>
</div>
);
}

Использование готовых библиотек:

react-window — лёгкая и быстрая библиотека от Brian Vaughn:

import { FixedSizeList } from 'react-window';

function MyList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);

return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}

react-virtualized — более функциональная библиотека:

import { List, AutoSizer } from 'react-virtualized';

function MyList({ items }) {
const rowRenderer = ({ key, index, style }) => (
<div key={key} style={style}>
{items[index].name}
</div>
);

return (
<AutoSizer>
{({ height, width }) => (
<List
height={height}
rowCount={items.length}
rowHeight={50}
rowRenderer={rowRenderer}
width={width}
/>
)}
</AutoSizer>
);
}

@tanstack/react-virtual — современная библиотека с поддержкой динамических размеров:

import { useVirtualizer } from '@tanstack/react-virtual';

function MyList({ items }) {
const parentRef = useRef(null);

const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5
});

return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div
style={{
height: virtualizer.getTotalSize(),
position: 'relative'
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}

Альтернативы виртуализации:

1. Пагинация — разбиение на страницы:

function PaginatedList({ items, pageSize = 50 }) {
const [page, setPage] = useState(0);

const visibleItems = items.slice(page * pageSize, (page + 1) * pageSize);

return (
<div>
{visibleItems.map(item => (
<div key={item.id}>{item.name}</div>
))}
<button
onClick={() => setPage(p => Math.max(0, p - 1))}
disabled={page === 0}
>
Previous
</button>
<button
onClick={() => setPage(p => p + 1)}
disabled={(page + 1) * pageSize >= items.length}
>
Next
</button>
</div>
);
}

2. Infinite scroll — подгрузка при скролле:

function InfiniteScrollList({ fetchMore, hasMore, items }) {
const observerRef = useRef(null);

const lastItemRef = useCallback(node => {
if (observerRef.current) observerRef.current.disconnect();

observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
fetchMore();
}
});

if (node) observerRef.current.observe(node);
}, [hasMore, fetchMore]);

return (
<div>
{items.map((item, index) => (
<div
key={item.id}
ref={index === items.length - 1 ? lastItemRef : null}
>
{item.name}
</div>
))}
</div>
);
}

Когда что использовать:

ПодходКогда использовать
ВиртуализацияНужен бесконечный скролл, все данные доступны
ПагинацияПользователь редко просматривает все данные
Infinite scrollСоциальные сети, ленты новостей
WindowingТаблиды с большим количеством строк