ОЧЕНЬ ЖЕСТКОЕ РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ НА MIDDLE/SENIOR FRONTEND РАЗРАБОТЧИКА С ЗП 320К!
Сегодня мы разберём собеседование на позицию 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 или контракты
Но в этих случаях это уже не чистый рефакторинг, а рефакторинг + исправление.
Как убедиться что поведение не изменилось:
- Запустить тесты до рефакторинга — все должны проходить
- Вносить маленькие изменения
- Запускать тесты после каждого изменения
- Если тест упал — либо откатить изменение, либо обновить тест если поведение действительно должно измениться
Рефакторинг — это навык, который требует практики. Хороший рефакторинг делает код понятнее для человека, не меняя то, что он делает для компьютера.
Вопрос 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):
- Определяем границы поиска:
leftиright - Находим средний элемент:
mid = (left + right) / 2 - Сравниваем искомое значение со средним элементом
- Если равны — нашли элемент
- Если искомое меньше — ищем в левой половине
- Если искомое больше — ищем в правой половине
- Повторяем пока не найдём или границы не сомкнутся
Визуализация:
Ищем 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 шагов!
Сравнение с линейным поиском:
| Размер массива | Линейный поиск | Бинарный поиск |
|---|---|---|
| 100 | 100 | 7 |
| 10,000 | 10,000 | 14 |
| 1,000,000 | 1,000,000 | 20 |
| 1,000,000,000 | 1,000,000,000 | 30 |
Практическое применение:
// Встроенные методы в стандартных библиотеках
// 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 }
- Ключ проходит через хэш-функцию
- Хэш-функция возвращает число (хэш)
- Хэш преобразуется в индекс массива:
index = hash % array.length - Значение хранится по этому индексу
Реализация простой хэш-таблицы:
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. Стойкость к коллизиям — сложно найти два разных входа с одинаковым хэшем
Популярные хэш-функции:
| Алгоритм | Размер выхода | Применение |
|---|---|---|
| MD5 | 128 бит | Устарел, не для безопасности |
| SHA-1 | 160 бит | Устарел, не для безопасности |
| SHA-256 | 256 бит | Безопасность, блокчейн, сертификаты |
| SHA-3 | 可变 | Новый стандарт |
| bcrypt | 可变 | Хранение паролей |
| xxHash | 32/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.
Правильный ответ:
Ответ собеседника корректен. Рассмотрим инкапсуляцию более детально.
Определение
Инкапсуляция — это механизм, который:
- Объединяет данные и методы работы с ними в единую сущность (класс)
- Скрывает внутреннюю реализацию от внешнего мира
- Предоставляет контролируемый доступ через публичный интерфейс
Модификаторы доступа в 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>;
}
Ключевые идеи абстракции:
- Сложность скрыта — пользователь видит только необходимый интерфейс
- Реализация может меняться — пока интерфейс не изменился, код работает
- Повторное использование — одна абстракция — много реализаций
- Тестирование — легко подменить реализацию моком
Отличие от других принципов:
- Инкапсуляция — скрывает данные (КАК хранить)
- Абстракция — скрывает реализацию (КАК делать)
- Полиморфизм — позволяет разным объектам иметь одинаковый интерфейс (ЧТО делать)
Абстракция — это способ борьбы со сложностью: мы думаем о задачах на высоком уровне, не отвлекаясь на детали реализации.
Вопрос 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) — это функция, которая:
- Принимает другую функцию как аргумент, или
- Возвращает функцию как результат, или
- И то, и другое
Это возможно благодаря концепции «функции как объекты первого класса» (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 — лучше дублировать, чем создавать ненужную абстракцию |
| SOLID | YAGNI ограничивает чрезмерное применение 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 Docs | Operational Transformation |
| Figma | CRDT |
| Notion | CRDT |
| Confluence | OT + LWW |
| VS Code Live Share | Custom 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. Реализации в базах данных:
| База данных | Подход к распределению |
|---|---|
| CockroachDB | Multi-Region, Spanner-like |
| YugabyteDB | Raft consensus, Geo-partitioning |
| Cassandra | Tunable consistency, Hinted Handoff |
| MongoDB | Replica Sets, Sharding |
| PostgreSQL | Logical 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 │
│ (Данные) │ │ (Отображение)│ │ (Логика) │
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↑
└──────────── Уведомления ─────────────┘
Поток данных:
- Пользователь взаимодействует с View
- View передаёт действие Controller
- Controller обновляет Model
- Model уведомляет View об изменениях
- 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>
);
}
Сравнение паттернов:
| Аспект | MVC | MVP | MVVM |
|---|---|---|---|
| View активность | Активный | Пассивный | Пассивный |
| Связь View-Model | Прямая | Через Presenter | Через Data Binding |
| Тестируемость | Средняя | Высокая | Высокая |
| Сложность | Низкая | Средняя | Средняя |
| Фреймворки | Express, Rails | Android (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:
| Аспект | REST | GraphQL |
|---|---|---|
| Эндпоинты | Много (ресурсы) | Один (/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 });
});
Сравнение протоколов:
| Протокол | Направление | Случай использования |
|---|---|---|
| REST | Request-Response | CRUD API |
| GraphQL | Request-Response | Гибкие запросы |
| WebSocket | Duplex | Чат, игры, торги |
| SSE | Server → Client | Уведомления, ленты |
| gRPC | Duplex | Микросервисы |
| Webhooks | Server → 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 | Когда использовать |
|---|---|---|---|---|
| 200 | OK | Да | Нет | GET, PUT, PATCH, DELETE |
| 201 | Created | Да | Да | POST (создание ресурса) |
| 202 | Accepted | Опционально | Опционально | Долгие операции |
| 204 | No 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 автоматически очищается
Сравнение характеристик:
| Характеристика | Map | WeakMap |
|---|---|---|
| Типы ключей | Любые | Только объекты |
| Итерация | Да (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:
| Контекст | this | super |
|---|---|---|
| Конструктор | Экземпляр | Конструктор родителя |
| Метод | Экземпляр | Прототип родителя |
| Статический метод | Класс | Родительский класс |
Вопрос 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 Types | Runtime + компиляция | Средняя | Высокая |
| Conditional Types | Компиляция | Высокая | Высокая |
| Type Guard | Runtime | Низкая | Средняя |
Когда что использовать:
- 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 Types | Runtime + компиляция | Средняя | Высокая |
| Conditional Types | Компиляция | Высокая | Высокая |
| Type Guard | Runtime | Низкая | Средняя |
Когда что использовать:
- 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 Types | Runtime + компиляция | Средняя | Высокая |
| Conditional Types | Компиляция | Высокая | Высокая |
| Type Guard | Runtime | Низкая | Средняя |
Когда что использовать:
- 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">×</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">×</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— синхронный6— синхронный4— микрозадача из Promise2— макрозадача из setTimeout3— микрозадача, добавлена во время выполнения макрозадачи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— синхронный6— синхронный4— микрозадача из Promise2— макрозадача из setTimeout3— микрозадача, добавлена во время выполнения макрозадачи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— синхронный6— синхронный4— микрозадача из Promise2— макрозадача из setTimeout3— микрозадача, добавлена во время выполнения макрозадачи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— синхронный6— синхронный4— микрозадача из Promise2— макрозадача из setTimeout3— микрозадача, добавлена во время выполнения макрозадачи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 | Таблиды с большим количеством строк |
