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

РЕАЛЬНОЕ FRONTEND СОБЕСЕДОВАНИЕ С КУЧЕЙ ДУШНОЙ ТЕОРИИ НА 300К MIDDLE/SENIOR ФРОНТЕНД!

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

Сегодня мы разберём собеседование на позицию Middle+/Senior Frontend-разработчика в крупную финтех-компанию, где кандидат Виктор последовательно прошёл двухчасовое интервью, включающее теоретический блок по протоколм сетям, HTML, CSS, JavaScript, TypeScript, React, SEO и SSR, а также практическую часть с задачами на понимание event loop, алгоритмическую задачу и локализацию утечки памяти. Виктор продемонстрировал уверенное владение базовыми концепциями, способность рассуждать и находить решения, хотя в некоторых местах его знания оказались поверхностными или неточными, что характерно для кандидата уровня Middle с отдельными зонами роста в сторону Senior.

Вопрос 1. Что такое протокол HTTP и для каких целей он используется?

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

Ответ собеседника: Правильный. HTTP — это протокол прикладного уровня для передачи данных. Изначально для HTML-документов, сейчас — для произвольных данных, например JSON. Однонаправленный (клиент-сервер), работает поверх TCP.

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

HTTP (HyperText Transfer Protocol) — это текстовый протокол прикладного уровня модели OSI/ISO, предназначенный для передачи данных между клиентом и сервером. Изначально создан для передачи HTML-документов, но сейчас используется для передачи практически любых данных: JSON, XML, бинарных файлов, потокового видео и т.д.

Основные характеристики HTTP:

  • Клиент-серверная модель: клиент (браузер, мобильное приложение, другой сервер) отправляет запрос, сервер возвращает ответ.
  • Stateless (без состояния): каждый запрос обрабатывается сервером независимо, сервер не хранит информацию о предыдущих запросах клиента. Для сохранения состояния используются куки, токены, сессии.
  • Работает поверх TCP (по умолчанию порт 80 для HTTP, 443 для HTTPS). В HTTP/3 используется QUIC поверх UDP.
  • Текстовый формат (в версиях 1.x и 2 — бинарный фрейминг, но семантика остаётся текстовой).

Эволюция версий:

  • HTTP/1.0 — новое TCP-соединение на каждый запрос.
  • HTTP/1.1 — постоянные соединения (keep-alive), конвейеризация, заголовок Host.
  • HTTP/2 — бинарный протокол, мультиплексирование запросов в одном соединении, сжатие заголовков (HPACK), server push.
  • HTTP/3 — на базе QUIC (UDP), устранение блокировки начала очереди (head-of-line blocking).

Структура запроса:

GET /api/users/1 HTTP/1.1
Host: example.com
Accept: application/json
Authorization: Bearer <token>

Структура ответа:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 42

{"id": 1, "name": "John"}

Коды состояния:

  • 2xx — успех (200 OK, 201 Created, 204 No Content)
  • 3xx — перенаправление (301 Moved Permanently, 302 Found)
  • 4xx — ошибка клиента (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests)
  • 5xx — ошибка сервера (500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable)

HTTP в Go:

// Простой HTTP-сервер
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "Hello"})
})

http.ListenAndServe(":8080", nil)

// HTTP-клиент
resp, err := http.Get("https://api.example.com/users/1")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)

Применение:

HTTP — это основа веб-разработки: REST API, GraphQL, gRPC-Web, WebSocket (после апгрейда), загрузка файлов, веб-страницы. Практически любое клиент-серверное взаимодействие в интернете так или иначе использует HTTP.

Вопрос 2. Из каких элементов состоит HTTP-запрос?

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

Ответ собеседника: Неполный. Состоит из стартовой строки (метод, путь, версия протокола) и опционально — тела (body) у некоторых запросов.

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

HTTP-запрос состоит из четырёх основных элементов:

1. Стартовая строка (Start Line / Request Line)

Содержит три компонента, разделённых пробелами:

METHOD /path?query=value HTTP/1.1
  • Метод — действие, которое нужно выполнить: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE, CONNECT.
  • URI/путь — идентификатор ресурса: /api/users/1?page=2.
  • Версия протокола: HTTP/1.0, HTTP/1.1, HTTP/2.

2. Заголовки (Headers)

Набор пар ключ-значение, описывающих параметры запроса. Каждый заголовок на отдельной строке:

Host: example.com
Content-Type: application/json
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Length: 42
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: session_id=abc123

Основные категории заголовков:

  • General — общие: Connection, Date, Transfer-Encoding
  • Request — о запросе: Host, Authorization, Accept, User-Agent
  • Entity — о теле: Content-Type, Content-Length, Content-Encoding

3. Пустая строка (Empty Line)

Разделяет заголовки от тела запроса. Обязательна даже если тело отсутствует. Представляет собой символ \r\n.

4. Тело запроса (Body) — опционально

Содержит данные, передаваемые на сервер. Присутствует не во всех запросах:

  • GET, HEAD, DELETE, OPTIONS — обычно без тела
  • POST, PUT, PATCH — как правило, с телом
{"name": "John", "email": "john@example.com"}

Полный пример HTTP-запроса:

POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Accept: application/json
Authorization: Bearer token123
Content-Length: 52

{"name": "John", "email": "john@example.com"}

Разбор запроса в Go:

http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
// Стартовая строка
method := r.Method // "POST"
url := r.URL.Path // "/api/users"
proto := r.Proto // "HTTP/1.1"

// Заголовки
contentType := r.Header.Get("Content-Type") // "application/json"
auth := r.Header.Get("Authorization") // "Bearer token123"

// Тело
body, _ := io.ReadAll(r.Body)
// []byte(`{"name": "John", "email": "john@example.com"}`)
})

Ответ HTTP-ответа имеет аналогичную структуру, но вместо стартовой строки запроса содержит строку статуса: HTTP/1.1 200 OK.

Вопрос 3. Можно ли в GET-запрос положить тело (body)?

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

Ответ собеседника: Правильный. Технически можно, но нарушает семантику HTTP. Некоторые браузеры удаляют body из GET-запроса, но на сервере можно реализовать обработку.

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

Краткий ответ: Технически да, спецификация HTTP не запрещает это явно, но это считается антипаттерном и приводит к проблемам.

Детальное объяснение:

Спецификация RFC 7231 говорит, что тело в GET-запросе не имеет определённой семантики. Это означает:

  • Протокол не запрещает тело в GET.
  • Но сервер вправе проигнорировать такое тело.
  • Промежуточные прокси и CDN могут отбросить тело.

Проблемы с телом в GET:

  • Кэширование — GET-запросы кэшируются, но ключ кэша обычно не учитывает тело. Два запроса с разным телом, но одинаковым URL, могут вернуть закэшированный ответ от другого запроса.
  • Прокси и CDN — могут отбросить тело или отклонить запрос.
  • Браузерыfetch() и XMLHttpRequest в некоторых браузерах не позволяют отправить тело с GET.
  • Инструменты — curl позволяет, но многие HTTP-клиенты и библиотеки — нет.

Когда тело в GET может быть оправдано:

Единственный общепринятый случай — Elasticsearch, который использует GET с телом для сложных поисковых запросов:

GET /index/_search
{
"query": {
"match": {
"title": "golang"
}
}
}

Правильная альтернатива — использовать POST:

Если нужно передать сложные параметры для выборки, лучше использовать POST с телом, даже если по семантике это «чтение» данных:

// Антипаттерн — GET с телом
// Лучше использовать POST для сложных запросов

http.HandleFunc("/api/search", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

var req struct {
Query string `json:"query"`
Filters []string `json:"filters"`
Page int `json:"page"`
}

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// Логика поиска...
})

Итог: Положить тело в GET можно, но не стоит. Для простых параметров — query string (?key=value), для сложных — POST.

Вопрос 4. Что такое CORS (политика одного источника)?

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

Ответ собеседника: Правильный. CORS — это политика браузера, определяющая через HTTP-заголовки, может ли фронтенд на одном домене делать запросы к бэкенду на другом домене. Регулируется заголовками Access-Control-Allow-Origin, Allow-Headers, Allow-Methods, Allow-Credentials.

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

CORS (Cross-Origin Resource Sharing) — это механизм безопасности браузера, который контролирует, может веб-приложение, загруженное с одного источника (origin), выполнять запросы к ресурсу с другого источника.

Same-Origin Policy (SOP):

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

  • Протокол (http vs https)
  • Домен (example.com vs api.example.com)
  • Порт (8080 vs 3000)

Примеры:

  • https://example.comhttps://example.com/apiтот же источник
  • https://example.comhttps://api.example.comдругой источник (другой домен)
  • https://example.comhttp://example.comдругой источник (другой протокол)

Как работает CORS:

Для «простых» запросов (GET, POST с определёнными Content-Type) браузер отправляет запрос с заголовком Origin, и сервер отвечает заголовками CORS.

Для «непростых» запросов (PUT, DELETE, нестандартные заголовки) браузер сначала отправляет preflight-запрос методом OPTIONS.

Ключевые заголовки CORS:

  • Access-Control-Allow-Origin — разрешённые источники (* или https://example.com)
  • Access-Control-Allow-Methods — разрешённые методы (GET, POST, PUT, DELETE)
  • Access-Control-Allow-Headers — разрешённые заголовки (Content-Type, Authorization)
  • Access-Control-Allow-Credentials — разрешены ли куки и авторизационные заголовки (true)
  • Access-Control-Max-Age — время кэширования preflight-ответа (в секундах)

Пример preflight-запроса:

OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

Ответ сервера на preflight:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400

Реализация CORS в Go:

// Ручная реализация через middleware
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://frontend.example.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")

// Обработка preflight
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}

next.ServeHTTP(w, r)
})
}

// Использование с библиотекой rs/cors
import "github.com/rs/cors"

handler := cors.New(cors.Options{
AllowedOrigins: []string{"https://frontend.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 86400,
}).Handler(mux)

Важные нюансы:

  • * в Access-Control-Allow-Origin нельзя использовать вместе с Allow-Credentials: true.
  • CORS — это ограничение браузера, сервер-к-сервер запросы не проверяют CORS.
  • CORS не защищает от CSRF — это разные механизмы безопасности.

Вопрос 5. Для чего нужен метод OPTIONS?

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

Ответ собеседния: Правильный. OPTIONS связан с CORS. Перед кросс-доменным запросом отправляется preflight-запрос с методом OPTIONS, определяющий, можно ли выполнить основной запрос. Preflight отправляется только для непростых запросов.

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

Метод OPTIONS выполняет две основные функции: запрос информации о возможностях сервера и выполнение preflight-проверки в CORS.

1. Preflight-запрос в CORS:

Это основное практическое применение. Браузер отправляет OPTIONS-запрос перед «непростым» кросс-доменным запросом, чтобы узнать разрешённые методы, заголовки и источники.

Preflight отправляется, если запрос:

  • Использует метод PUT, DELETE, PATCH
  • Содержит заголовки, отличные от Accept, Accept-Language, Content-Language, Content-Type
  • Имеет Content-Type отличный от application/x-www-form-urlencoded, multipart/form-data, text/plain

2. Запрос возможностей сервера (Discovery):

OPTIONS позволяет клиенту узнать, какие методы и возможности поддерживает сервер для конкретного ресурса без выполнения фактического действия:

OPTIONS /api/users/1 HTTP/1.1
Host: example.com

Ответ:

HTTP/1.1 204 No Content
Allow: GET, PUT, DELETE, OPTIONS

3. Проверка доступности (Health Check):

Иногда OPTIONS используется для проверки живости сервера, аналогично HEAD, но с возможностью получить CORS-заголовки.

Обработка OPTIONS в Go:

http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodOptions:
w.Header().Set("Allow", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
w.WriteHeader(http.StatusNoContent)

case http.MethodGet:
// Логика получения пользователей

case http.MethodPost:
// Логика создания пользователя

default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})

Разница между простыми и сложными запросами:

Простые запросы (без preflight):

  • Методы: GET, HEAD, POST
  • Content-Type: text/plain, application/x-www-form-urlencoded, multipart/form-data
  • Нет кастомных заголовков

Сложные запросы (с preflight):

  • Методы: PUT, DELETE, PATCH
  • Content-Type: application/json
  • Кастомные заголовки: Authorization, X-Custom-Header

Вопрос 6. Что такое простой запрос в контексте CORS?

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

Ответ собеседника: Правильный. Простой запрос — это GET, POST или HEAD без дополнительных заголовков.

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

Простой (simple) запрос — это HTTP-запрос, который не вызывает у браузера отправку preflight-запроса через OPTIONS. Он напрямую отправляется на сервер, и браузер проверяет CORS-заголовки уже в основном ответе.

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

1. Допустимый метод:

  • GET
  • HEAD
  • POST

2. Допустимые заголовки (только из списка CORS-safelisted):

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (но только с определёнными значениями, см. ниже)
  • Range (с простыми значениями)

Любой другой заголовок (например, Authorization, X-Requested-With, X-Custom-Header) делает запрос сложным.

3. Допустимый Content-Type (если присутствует):

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain

Примеры:

Простой запрос:

fetch('https://api.example.com/users', {
method: 'GET'
});

fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'name=John'
});

Сложный запрос (требует preflight):

// Из-за метода DELETE
fetch('https://api.example.com/users/1', { method: 'DELETE' });

// Из-за Content-Type: application/json
fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John' })
});

// Из-за кастомного заголовка
fetch('https://api.example.com/users', {
method: 'GET',
headers: { 'Authorization': 'Bearer token' }
});

Почему это важно:

Простые запросы быстрее, так как не требуют дополнительного round-trip (preflight). При проектировании API стоит учитывать это — если возможно, использовать простые запросы для повышения производительности.

Вопрос 7. В чём отличия версий протокола HTTP/1.1 и HTTP/2?

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

Ответ собеседника: Правильный. HTTP/1.1 — текстовый, HTTP/2 — бинарный. HTTP/2 поддерживает мультиплексирование в одном TCP-соединении, в отличие от HTTP/1.1 с отдельными соединениями. Также HTTP/2 имеет продвинутое сжатие заголовков и Server Push.

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

HTTP/2 — это значительное усовершенствование HTTP/1.1, направленное на повышение производительности. Основные отличия:

1. Бинарный протокол vs текстовый:

HTTP/1.1 использует текстовый формат, который человекочитаем, но медленнее парсится. HTTP/2 использует бинарный фрейминг — данные разбиваются на бинарные фреймы (HEADERS frame, DATA frame), что ускоряет обработку и снижает вероятность ошибок парсинга.

2. Мультиплексирование:

Это ключевое отличие. В HTTP/1.1 в одном TCP-соединении запросы обрабатываются последовательно (хотя keep-alive позволяет переиспользовать соединение). Браузеры открывают 6-8 параллельных TCP-соединений к одному хосту для обхода этого ограничения.

HTTP/2 позволяет отправлять множество запросов и ответов параллельно в рамках одного TCP-соединения через систему стримов (streams). Это устраняет блокировку начала очереди (head-of-line blocking) на уровне приложения.

HTTP/1.1: [Req1]→[Res1]→[Req2]→[Res2]→[Req3]→[Res3]
HTTP/2: [Req1][Req2][Req3]→[Res2][Res1][Res3] // параллельно

3. Сжатие заголовков (HPACK):

HTTP/1.1 передаёт заголовки в каждом запросе без сжатия (куки могут занимать килобайты). HTTP/2 использует алгоритм HPACK — статическое и динамическое сжатие заголовков. Повторяющиеся заголовки передаются как индексы из таблицы, что значительно уменьшает объём трафика.

4. Server Push:

HTTP/2 позволяет серверу проактивно отправлять ресурсы клиенту, пока тот ещё не запросил их. Например, при загрузке HTML страницы сервер может сразу отправить CSS и JS файлы.

// Сервер пушит CSS вместе с HTML
// Клиент получает оба ресурса без дополнительного запроса

5. Приоритизация стримов:

HTTP/2 позволяет клиенту задавать приоритеты и веса для стримов, чтобы сервер знал, какие ресурсы важнее:

Stream 1 (HTML): weight=256
Stream 2 (CSS): weight=128
Stream 3 (JS): weight=64
Stream 4 (img): weight=32

6. Обязательное шифрование (de facto):

Спецификация HTTP/2 не требует TLS, но все браузеры поддерживают HTTP/2 только поверх HTTPS (через ALPN).

Сравнительная таблица:

ХарактеристикаHTTP/1.1HTTP/2
ФорматТекстовыйБинарный
Соединения6-8 параллельных TCP1 TCP с мультиплексированием
ЗаголовкиБез сжатияHPACK
Server PushНетДа
ПриоритизацияНетДа
Head-of-line blockingДа (на уровне HTTP)Нет (на уровне HTTP, но есть на уровне TCP)

HTTP/2 в Go:

// Автоматически поддерживает HTTP/2 при использовании TLS
server := &http.Server{
Addr: ":443",
Handler: mux,
}
server.ListenAndServeTLS("cert.pem", "key.pem")

Ограничение HTTP/2:

Head-of-line blocking на уровне TCP остаётся — если потерян один TCP-пакет, все стримы в этом соединении блокируются. Это решается в HTTP/3 через QUIC (UDP).

Вопрос 8. Что такое WebSocket и чем он отличается от HTTP?

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

Ответ собеседния: Правильный. WebSocket — это двунаправленный протокол, в отличие от однонаправленного HTTP. Обе стороны могут отправлять сообщения. TCP-соединение устанавливается один раз и держится открытым, в отличие от HTTP с новым соединением на каждый запрос.

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

WebSocket — это протокол полнодуплексной (full-duplex) связи поверх одного TCP-соединения, предназначенный для обмена сообщениями в реальном времени между клиентом и сервером.

Ключевые отличия от HTTP:

1. Направление связи:

  • HTTP: запрос-ответ (клиент всегда инициирует). Сервер не может сам отправить данные клиенту.
  • WebSocket: полнодуплексный — обе стороны могут отправлять сообщения в любой момент без запроса от другой стороны.

2. Соединение:

  • HTTP: каждое взаимодействие — отдельный цикл запрос-ответ (даже с keep-alive в HTTP/1.1).
  • WebSocket: одно TCP-соединение устанавливается через HTTP-апгрейд и остаётся открытым всё время.

3. Заголовки:

  • HTTP: полные заголовки отправляются с каждым запросом/ответом (накладные расходы).
  • WebSocket: заголовки отправляются только при установке соединения (handshake), далее — минимальные фреймы (2-14 байт).

4. Формат данных:

  • HTTP: текстовый или бинарный с чёткой структурой (заголовки + тело).
  • WebSocket: фреймы с минимальным оверхедом, поддержка текста и бинарных данных.

Установка WebSocket-соединения:

Начинается как HTTP-запрос с заголовком Upgrade:

GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Сервер отвечает:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

После этого соединение переключается на WebSocket-протокол.

Использование WebSocket:

  • Чаты и мессенджеры
  • Онлайн-игры
  • Торговые площадки (котировки в реальном времени)
  • Совместное редактирование документов
  • Уведомления push
  • Дашборды с живыми данными

WebSocket в Go:

import "github.com/gorilla/websocket"

var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // В продакшене нужна проверка!
},
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Upgrade error:", err)
return
}
defer conn.Close()

for {
// Чтение сообщения от клиента
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Read error:", err)
break
}

log.Printf("Received: %s", message)

// Отправка ответа
response := []byte("Echo: " + string(message))
if err := conn.WriteMessage(messageType, response); err != nil {
log.Println("Write error:", err)
break
}
}
}

http.HandleFunc("/ws", wsHandler)

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

  • Long Polling — клиент отправляет запрос, сервер держит его открытым, пока не появятся данные.
  • Server-Sent Events (SSE) — однонаправленный (сервер → клиент), работает поверх HTTP, проще реализовать.
  • WebTransport — новый протокол на базе QUIC, поддерживает как надёжную, так и ненадёжную доставку.

Когда выбирать WebSocket vs SSE:

  • WebSocket — когда нужна двусторонняя связь (чат, игры).
  • SSE — когда данные идут только от сервера (уведомления, ленты), проще реализовать, автоматическое переподключение из коробки.

Вопрос 9. Что такое хендшейк в контексте WebSocket?

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

Ответ собеседника: Правильный. Хендшейк — это процесс установки WebSocket-соединения, при котором браузер отправляет HTTP-запрос с заголовком Upgrade: websocket для переключения протокола.

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

WebSocket handshake — это процесс переключения протокола с HTTP на WebSocket, происходящий при первоначальном подключении. Это мост между веб-инфраструктурой и WebSocket-протоколом.

Шаги handshake:

1. Клиент отправляет HTTP-запрос с заголовками апгрейда:

GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Ключевые заголовки:

  • Upgrade: websocket — указывает на желание сменить протокол
  • Connection: Upgrade — прокси должны пробрасывать этот заголовок
  • Sec-WebSocket-Key — base64-кодированный случайный 16-байтовый ключ (nonce)
  • Sec-WebSocket-Version — версия протокола (13 — актуальная)

2. Сервер проверяет запрос и формирует ответ:

Сервер вычисляет Sec-WebSocket-Accept — хеш от ключа клиента с магическим GUID:

accept = base64(sha1(secWebSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))

3. Сервер отвечает 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Зачем нужен Sec-WebSocket-Key:

Это защита от атак:

  • Предотвращает кэширующие прокси от повторного использования старого WebSocket-ответа
  • Гарантирует, что сервер действительно понимает WebSocket-протокол (а не случайно ответил 101)

Реализация handshake в Go:

import (
"crypto/sha1"
"encoding/base64"
"net/http"
)

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
// Проверяем заголовки
if r.Header.Get("Upgrade") != "websocket" {
http.Error(w, "Expected websocket", http.StatusBadRequest)
return
}

key := r.Header.Get("Sec-WebSocket-Key")
if key == "" {
http.Error(w, "Missing key", http.StatusBadRequest)
return
}

// Вычисляем accept
guid := "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
h := sha1.New()
h.Write([]byte(key + guid))
accept := base64.StdEncoding.EncodeToString(h.Sum(nil))

// Отправляем ответ
w.Header().Set("Upgrade", "websocket")
w.Header().Set("Connection", "Upgrade")
w.Header().Set("Sec-WebSocket-Accept", accept)
w.WriteHeader(http.StatusSwitchingProtocols)

// Далее — WebSocket-протокол поверх hijacked соединения
}

// На практике используют библиотеки (gorilla/websocket, nhooyr/websocket)

Ошибки handshake:

  • 400 Bad Request — неверные заголовки
  • 426 Upgrade Required — сервер требует апгрейд, но клиент его не запросил
  • 403 Forbidden — проверка Origin не прошла

Расширения и подпротоколы:

В handshake можно согласовать дополнительные параметры:

Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Protocol: chat, superchat
  • Sec-WebSocket-Protocol — подпротокол прикладного уровня (например, формат сообщений)
  • Sec-WebSocket-Extensions — расширения (сжатие permessage-deflate)

Вопрос 10. Какие существуют альтернативы WebSocket-соединению?

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

Ответ собеседника: Правильный. Альтернативы: Server-Sent Events (SSE), короткий поллинг (polling) и длинный поллинг (long polling).

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

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

1. Short Polling (Короткий поллинг):

Клиент периодически отправляет HTTP-запросы на сервер с фиксированным интервалом.

// Каждые 3 секунды спрашиваем сервер о новых данных
setInterval(async () => {
const response = await fetch('/api/updates');
const data = await response.json();
updateUI(data);
}, 3000);

Плюсы: простота реализации, работает везде. Минусы: высокий трафик, задержка до одного интервала, нагрузка на сервер.

2. Long Polling (Длинный поллинг):

Клиент отправляет запрос, сервер держит его открытым до появления новых данных или таймаута. После получения ответа клиент сразу отправляет новый запрос.

async function longPoll() {
try {
const response = await fetch('/api/updates');
const data = await response.json();
updateUI(data);
} catch (err) {
console.error(err);
}
// Сразу отправляем новый запрос
longPoll();
}

longPoll();
func updatesHandler(w http.ResponseWriter, r *http.Request) {
// Ждём данные из канала или таймаут
select {
case data := <-updatesChan:
json.NewEncoder(w).Encode(data)
case <-time.After(30 * time.Second):
w.WriteHeader(http.StatusNoContent)
}
}

Плюсы: меньше трафика чем short polling, работает везде. Минусы: нагрузка на сервер (держит соединения), задержка при переподключении.

3. Server-Sent Events (SSE):

Однонаправленный канал от сервера к клиенту через HTTP. Сервер отправляет поток событий, клиент слушает.

const eventSource = new EventSource('/api/events');

eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
updateUI(data);
};

eventSource.addEventListener('price-update', (event) => {
updatePrice(JSON.parse(event.data));
});
func eventsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
return
}

for {
select {
case data := <-eventsChan:
fmt.Fprintf(w, "event: price-update\n")
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
case <-r.Context().Done():
return
}
}
}

Плюсы: автоматическое переподключение, простота, работает поверх HTTP, поддержка типов событий. Минусы: только сервер → клиент, максимум 6 соединений к домену в HTTP/1.1.

4. WebTransport:

Новый протокол на базе QUIC (HTTP/3), поддерживает как надёжную (streams), так и ненадёжную (datagrams) доставку.

const transport = new WebTransport("https://example.com:4433/ws");

await transport.ready;

const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();

await writer.write(new TextEncoder().encode("Hello"));

Плюсы: низкая задержка, поддержка ненадёжной доставки (игрoвой трафик, видео). Минусы: новая технология, ограниченная поддержка браузерами.

Сравнительная таблица:

ТехнологияНаправлениеПротоколЗадержкаСложность
Short PollingДвустороннийHTTPВысокая (интервал)Низкая
Long PollingДвустороннийHTTPСредняяСредняя
SSEСервер→КлиентHTTPНизкаяНизкая
WebSocketДвустороннийWSНизкаяСредняя
WebTransportДвустороннийQUICОчень низкаяВысокая

Рекомендации по выбору:

  • SSE — если данные идут только от сервера (уведомления, ленты, котировки)
  • WebSocket — если нужна двусторонняя связь (чат, игры, совместное редактирование)
  • Long Polling — как fallback для старых браузеров или ограниченных сетей
  • Short Polling — только если допустима задержка в несколько секунд

Вопрос 11. В чём различия между SSE и WebSocket?

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

Ответ собеседника: Правильный. SSE — однонаправленный (только сервер отправляет клиенту), WebSocket — двунаправленный. SSE передаёт данные в текстовом формате.

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

SSE и WebSocket — оба предназначены для обмена данными в реальном времени, но имеют принципиальные архитектурные различия.

1. Направление связи:

  • SSE: однонаправленный — только сервер → клиент. Клиент не может отправить данные через SSE-соединение.
  • WebSocket: полнодуплексный — обе стороны могут отправлять сообщения в любой момент.

2. Протокол:

  • SSE: работает поверх обычного HTTP. Сервер отправляет поток с Content-Type: text/event-stream.
  • WebSocket: собственный протокол (ws:// или wss://), начинается с HTTP handshake, затем переключается на бинарный фрейминг.

3. Формат данных:

  • SSE: только текст (UTF-8). Формат событий:
    event: user-update
    data: &#123;"id": 1, "name": "John"&#125;
    id: 42
    retry: 3000
  • WebSocket: текст и бинарные данные. Свободный формат — вы сами определяете структуру сообщений.

4. Автоматическое переподключение:

  • SSE: встроено в браузер. При разрыве соединения EventSource автоматически переподключается. Поддержка retry для настройки интервала и Last-Event-ID для возобновления с последнего события.
  • WebSocket: нет встроенного механизма. Нужно реализовывать вручную.

5. Ограничения соединений:

  • SSE: в HTTP/1.1 браузер ограничивает 6 соединениями к одному домену. SSE занимает одно из них.
  • WebSocket: нет такого ограничения, каждое WebSocket-соединение считается отдельно.

6. Прокси и файрволы:

  • SSE: работает через стандартный HTTP, совместим с любыми прокси, CDN, балансировщиками.
  • WebSocket: может иметь проблемы с некоторыми прокси и CDN, которые не поддерживают Upgrade-заголовок.

7. Передача данных от клиента к серверу:

  • SSE: клиент может отправлять данные только через отдельные HTTP-запросы (fetch/XHR).
  • WebSocket: клиент отправляет данные через то же соединение.

Пример SSE:

const source = new EventSource('/api/events');

source.onmessage = (e) => console.log(e.data);
source.addEventListener('price', (e) => updatePrice(JSON.parse(e.data)));
func sseHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")

flusher := w.(http.Flusher)
for msg := range broadcastChan {
fmt.Fprintf(w, "event: price\n")
fmt.Fprintf(w, "data: %s\n\n", msg)
flusher.Flush()
}
}

Пример WebSocket:

const ws = new WebSocket('wss://example.com/ws');
ws.onmessage = (e) => console.log(e.data);
ws.send(JSON.stringify({ action: 'subscribe', channel: 'prices' }));

Когда что выбирать:

  • SSE: уведомления, ленты новостей, котировки, лог-стриминг — когда данные идут только от сервера.
  • WebSocket: чаты, онлайн-игры, совместное редактирование — когда нужна двусторонняя связь с минимальной задержкой.

Вопрос 12. Для чего нужна семантическая вёрстка?

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

Ответ собеседника: Правильный. Семантическая вёрстка нужна для SEO-оптимизации и доступности (accessibility), чтобы скринридеры могли корректно читать содержимое сайта.

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

Семантическая вёрстка — это подход к разметке HTML, при котором используются теги, соответствующие смыслу содержимого, а не только его внешнему виду.

Основные цели:

1. SEO (Search Engine Optimization):

Поисковые системы анализируют HTML-структуру для понимания содержания страницы. Семантические теги помогают поисковикам правильно индексировать контент:

  • <header>, <footer>, <main>, <nav> — определяют структуру страницы
  • <article>, <section> — выделяют самостоятельные блоки контента
  • <h1><h6> — формируют иерархию заголовков

2. Accessibility (Доступность):

Скринридеры (JAWS, NVDA, VoiceOver) используют семантические теги для навигации:

  • <nav> — скринридер может сразу перейти к навигации
  • <main> — позволяет пропустить повторяющиеся блоки
  • <button> vs <div onclick> — скринридер объявит элемент как кнопку только при использовании семантического тега

3. Читаемость и поддерживаемость кода:

Семантическая разметка делает код понятнее для разработчиков:

<!-- Несемантическая вёрстка -->
<div class="header">
<div class="nav">
<div class="nav-item">Home</div>
</div>
</div>
<div class="main-content">
<div class="article">
<div class="article-title">Title</div>
</div>
</div>

<!-- Семантическая вёрстка -->
<header>
<nav>
<a href="/">Home</a>
</nav>
</header>
<main>
<article>
<h1>Title</h1>
</article>
</main>

4. ARIA и встроенная семантика:

Современные HTML-теги уже содержат встроенные ARIA-роли:

<!-- Избыточно: роль уже встроена -->
<nav role="navigation">...</nav>

<!-- Достаточно: -->
<nav>...</nav>

Основные семантические теги:

  • <header> — шапка страницы или секции
  • <nav> — навигационные ссылки
  • <main> — основное содержимое (один на страницу)
  • <article> — самостоятельный контент (пост, новость)
  • <section> — тематическая группа контента
  • <aside> — боковая панель, связанный контент
  • <footer> — подвал страницы или секции
  • <figure>, <figcaption> — иллюстрация с подписью
  • <time> — дата/время
  • <address> — контактная информация

Практическое значение для Go-разработчика:

При генерации HTML на сервере (шаблонизация) важно использовать семантические теги:

// Шаблон с семантической разметкой
const tpl = `
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
<header>
<nav>
{{range .NavItems}}
<a href="{{.URL}}">{{.Label}}</a>
{{end}}
</nav>
</header>
<main>
{{range .Articles}}
<article>
<h2>{{.Title}}</h2>
<time datetime="{{.Date}}">{{.FormattedDate}}</time>
<p>{{.Content}}</p>
</article>
{{end}}
</main>
<footer>
<p>&copy; 2024</p>
</footer>
</body>
</html>
`

Семантическая вёрстка — это не только frontend-ответственность. Бэкенд-разработчик должен понимать её важность для правильной генерации HTML на стороне сервера.

Вопрос 13. Для чего используется тег article и как его правильно применять?

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

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

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

Тег <article> предназначен для разметки самостоятельных, независимых блоков контента, которые имеют смысл сами по себе даже вне контекста страницы.

Ключевой критерий:

Контент внутри <article> должен быть самодостаточным. Если его можно вырезать со страницы и вставить на другую страницу — он не потеряет смысл, значит <article> уместен.

Когда использовать <article>:

  • Пост в блоге
  • Новостная статья
  • Комментарий пользователя
  • Карточка товара
  • Запись в социальной сети
  • Виджет (погода, курс валют)
  • Интерактивная карточка на дашборде

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

  • Навигационные элементы — <nav>
  • Боковая панель — <aside>
  • Просто группировка стилей — <div>

Правильное использование:

<!-- Страница блога с несколькими статьями -->
<main>
<article>
<header>
<h2>Заголовок статьи</h2>
<time datetime="2024-01-15">15 января 2024</time>
</header>
<p>Содержание статьи...</p>
<footer>
<p>Автор: Иван Петров</p>
</footer>
</article>

<article>
<header>
<h2>Другая статья</h2>
<time datetime="2024-01-14">14 января 2024</time>
</header>
<p>Содержание другой статьи...</p>
</article>
</main>

Вложенные article:

<article> может быть вложен в другой <article> — например, комментарий к статье:

<article>
<h2>Статья о Go</h2>
<p>Содержание статьи...</p>

<section>
<h3>Комментарии</h3>
<article>
<header>
<strong>Алексей</strong>
<time datetime="2024-01-15T10:30">15 янв, 10:30</time>
</header>
<p>Отличная статья!</p>
</article>
<article>
<header>
<strong>Мария</strong>
<time datetime="2024-01-15T11:00">15 янв, 11:00</time>
</header>
<p>Спасибо, было полезно.</p>
</article>
</section>
</article>

article vs section:

  • <article> — самостоятельный, переиспользуемый контент
  • <section> — тематическая группировка контента, зависимая от контекста
<!-- section внутри article: главы статьи -->
<article>
<h1>Руководство по Go</h1>
<section>
<h2>Введение</h2>
<p>...</p>
</section>
<section>
<h2>Горутины</h2>
<p>...</p>
</section>
</article>

<!-- article внутри section: список статей -->
<section>
<h2>Последние публикации</h2>
<article><h3>Статья 1</h3></article>
<article><h3>Статья 2</h3></article>
</section>

Генерация из Go-шаблонов:

type Article struct {
Title string
Date time.Time
Author string
Content string
}

const articleTpl = `
{{range .Articles}}
<article>
<header>
<h2>{{.Title}}</h2>
<time datetime="{{.Date.Format "2006-01-02"}}">
{{.Date.Format "02.01.2006"}}
</time>
</header>
<p>{{.Content}}</p>
<footer>Автор: {{.Author}}</footer>
</article>
{{end}}
`

SEO-эффект:

Поисковые системы лучше понимают структуру контента при правильном использовании <article>, что может положительно влиять на ранжирование и отображение в поисковой выдаче (расширенные сниппеты).

Вопрос 14. Для чего нужен тег main и как его правильно применять?

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

Ответ собеседника: Правильный. Тег main используется для обозначения основного контента сайта. В типичной раскладке содержится между header и footer. В main размещается основной контент: фильтры, список товаров, пагинация и т.д.

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

Тег <main> определяет основное, уникальное содержимое документа. Это контент, который непосредственно относится к главной теме страницы или расширяет её.

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

1. Только один на страницу:

На каждой странице должен быть ровно один <main>. Это требование спецификации HTML.

2. Не должен быть вложен в другие семантические теги:

<main> не должен находиться внутри <article>, <aside>, <footer>, <header>, <nav>.

3. Содержит уникальный контент:

В <main> размещается то, что отличает эту страницу от других. Повторяющиеся элементы (шапка, навигация, подвал, боковые панели) — вне <main>.

Правильная структура страницы:

<body>
<header>
<nav><!-- Навигация --></nav>
</header>

<main>
<!-- Уникальный контент страницы -->
<h1>Каталог товаров</h1>
<section>
<h2>Фильтры</h2>
<!-- Фильтры -->
</section>
<section>
<h2>Список товаров</h2>
<!-- Товары -->
</section>
</main>

<aside>
<!-- Боковая панель: реклама, дополнительные ссылки -->
</aside>

<footer>
<!-- Подвал -->
</footer>
</body>

Неправильное использование:

<!-- НЕПРАВИЛЬНО: два main -->
<main>Контент 1</main>
<main>Контент 2</main>

<!-- НЕПРАВИЛЬНО: main внутри article -->
<article>
<main>Содержимое</main>
</article>

<!-- НЕПРАВИЛЬНО: навигация внутри main -->
<main>
<nav>Ссылки на другие страницы</nav>
<h1>Контент</h1>
</main>

Значение для accessibility:

Скринридеры позволяют пользователям напрямую перейти к <main>, минуя повторяющиеся элементы (шапка, навигация). Это значительно ускоряет навигацию для людей с ограниченными возможностями.

В отличие от <div id="main">, тег <main> имеет встроенную ARIA-роль main, что делает ARIA-атрибут role="main" избыточным:

<!-- Достаточно: -->
<main>
<h1>Заголовок</h1>
</main>

<!-- Избыточно: -->
<main role="main">
<h1>Заголовок</h1>
</main>

Генерация в Go-шаблонах:

const pageTpl = `
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
<header>
<nav>
{{range .NavItems}}
<a href="{{.URL}}">{{.Label}}</a>
{{end}}
</nav>
</header>

<main>
<h1>{{.Title}}</h1>
{{range .Products}}
<article>
<h2>{{.Name}}</h2>
<p>{{.Price}}</p>
</article>
{{end}}
</main>

<footer>
<p>&copy; 2024</p>
</footer>
</body>
</html>
`

Итог: <main> — это навигационный ориентир для скринридеров и поисковых систем, обозначающий уникальный контент страницы. Один на страницу, без вложенности в другие семантические теги.

Вопрос 15. Как правильно использовать атрибут alt для изображения и для чего он необходим?

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

Ответ собеседника: Правильный. Атрибут alt задаёт альтернативный текст для изображения на случай, если оно не загрузилось или указан неправильный URL. Пользователь видит этот текст вместо пустого места. Также alt используется скринридерами для озвучивания изображений.

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

Атрибут alt (alternative text) предоставляет текстовое описание изображения, которое отображается или озвучивается, когда изображение недоступно.

Зачем нужен alt:

1. Accessibility (доступность):

Скринридеры озвучивают содержимое alt пользователям с нарушениями зрения. Без alt скринридер либо промолчит, либо прочитает имя файла (image_00342.png), что бесполезно.

2. Отказоустойчивость:

Если изображение не загрузилось (битая ссылка, медленное соединение), браузер показывает текст alt вместо пустого места.

3. SEO:

Поисковые системы не умеют «видеть» изображения, но понимают текст в alt. Это помогает изображениям индексироваться в поиске по картинкам.

Правила написания alt:

<!-- ПРАВИЛЬНО: описательный alt -->
<img src="golden-retriever.jpg" alt="Золотистый ретривер играет с мячом в парке">

<!-- ПРАВИЛЬНО: для функциональной иконки -->
<img src="search-icon.svg" alt="Поиск">

<!-- ПРАВИЛЬНО: пустой alt для декоративных изображений">
<img src="divider.png" alt="">

<!-- НЕПРАВИЛЬНО: пустой alt без причины -->
<img src="product-photo.jpg" alt="">

<!-- НЕПРАВИЛЬНО: избыточный alt -->
<img src="cat.jpg" alt="изображение кота">

<!-- НЕПРАВИЛЬНО: слишком длинный alt -->
<img src="chart.png" alt="График продаж за 2024 год по месяцам: январь — 100, февраль — 150, март — 200...">
<!-- Для сложных изображений используйте aria-describedby -->

Когда alt должен быть пустым (alt=""):

  • Декоративные изображения (разделители, фоновые элементы)
  • Изображения, смысл которых полностью передаётся окружающим текстом
  • Иконки, рядом с которыми есть текстовый аналог

Когда alt обязателен:

  • Информационные изображения (фотографии, иллюстрации)
  • Изображения-ссылки или кнопки
  • Графики и диаграммы (краткое описание)

Сложные изображения:

Для графиков и диаграмм с подробным описанием используйте aria-describedby:

<img src="sales-chart.png"
alt="График продаж за 2024 год"
aria-describedby="chart-description">
<p id="chart-description" class="visually-hidden">
Продажи выросли с 100 в январе до 500 в декабре.
Пик пришёлся на ноябрь — 600 единиц.
</p>

Генерация в Go:

type Image struct {
Src string
Alt string
IsDecorative bool
}

func renderImage(img Image) string {
if img.IsDecorative {
return fmt.Sprintf(`<img src="%s" alt="">`, html.EscapeString(img.Src))
}
return fmt.Sprintf(`<img src="%s" alt="%s">`,
html.EscapeString(img.Src),
html.EscapeString(img.Alt))
}

Итог: alt — обязательный атрибут для доступности и SEO. Описывайте содержание и функцию изображения, используйте пустой alt только для декоративных элементов.

Вопрос 16. Что такое атрибут role и как он применяется? Почему нельзя просто использовать семантические теги?

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

Ответ собеседника: Правильный. Атрибут role задаёт роль HTML-элементу, особенно полезен для несемантических тегов. Например, если кнопка создана через div, скринридер не поймёт, что это кнопка, но с role='button' — поймёт. Потребность возникает потому, что не все элементы можно реализовать семантическими тегами, у нативных элементов есть дефолтные стили, которые нужно сбрасывать, а в UI-библиотеках не все элементы сделаны на семантических тегах.

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

Атрибут role из спецификации ARIA (Accessible Rich Internet Applications) определяет назначение элемента для вспомогательных технологий (скринридеров, программ распознавания речи).

Зачем нужен role:

Скринридеры используют семантическую информацию для навигации и озвучивания. Атрибут role явно указывает, какую роль выполняет элемент:

<!-- Без role: скринридер озвучит как просто текст -->
<div onclick="submit()">Отправить</div>

<!-- С role: скринридер озвучит как кнопку -->
<div role="button" tabindex="0" onclick="submit()">Отправить</div>

Категории ролей:

Widget roles (интерактивные элементы):

  • button — кнопка
  • checkbox — чекбокс
  • link — ссылка
  • textbox — текстовое поле
  • tab, tabpanel, tablist — вкладки
  • dialog — модальное окно
  • alert — важное уведомление

Document structure roles:

  • navigation — навигация
  • main — основной контент
  • search — форма поиска
  • article — статья
  • complementary — дополнительный контент (aside)

Live region roles:

  • alert — срочное уведомление
  • status — статусное сообщение
  • log — лог
  • timer — таймер

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

1. Ограниченный набор нативных тегов:

Не для всех паттернов есть семантический тег. Например, нет нативных тегов для табов, аккордеонов, тултипов, деревьев.

<!-- Нет нативного тега для табов -->
<div role="tablist">
<button role="tab" aria-selected="true">Вкладка 1</button>
<button role="tab" aria-selected="false">Вкладка 2</button>
</div>
<div role="tabpanel">Содержимое вкладки 1</div>

2. Дефолтные стили и поведение:

Нативные элементы имеют встроенные стили и поведение, которые не всегда нужны:

<!-- Нативная кнопка: дефолтные стили, фокус, Enter/Space -->
<button>Отправить</button>

<!-- div с role="button": нет дефолтных стилей, нужно всё реализовать вручную -->
<div role="button" tabindex="0">Отправить</div>

3. UI-библиотеки и фреймворки:

Многие компоненты в библиотеках реализованы через <div> с нужными role и ARIA-атрибутами, чтобы не наследовать нативное поведение.

4. Кастомные виджеты:

Сложные компоненты (автодополнение, дерево файлов, слайдер) невозможно реализовать на нативных тегах — нужен role в сочетании с другими ARIA-атрибутами.

Важное правило:

Если есть нативный семантический тег — используйте его. Он уже содержит встроенную роль, поведение, обработку клавиатуры:

<!-- ПРАВИЛЬНО: нативный тег -->
<button onclick="save()">Сохранить</button>

<!-- НЕПРАВИЛЬНО: div с role вместо button -->
<div role="button" tabindex="0" onclick="save()">Сохранить</div>

Нативный <button> имеет:

  • Встроенную роль button
  • Обработку Enter и Space
  • Стили фокуса
  • Корректное поведение в формах

Полный пример кастомного виджета:

<!-- Кастомный чекбокс с ARIA -->
<div role="checkbox"
aria-checked="false"
tabindex="0"
id="custom-checkbox">
Принять условия
</div>

<script>
document.getElementById('custom-checkbox').addEventListener('click', function() {
const checked = this.getAttribute('aria-checked') === 'true';
this.setAttribute('aria-checked', !checked);
});
</script>

Связь с Go:

При генерации HTML на сервере важно правильно расставлять role:

type Tab struct {
ID string
Label string
Content string
Active bool
}

func renderTabs(tabs []Tab) string {
var tablist strings.Builder
tablist.WriteString(`<div role="tablist">`)
for _, tab := range tabs {
selected := "false"
if tab.Active {
selected = "true"
}
tablist.WriteString(fmt.Sprintf(
`<button role="tab" aria-selected="%s" aria-controls="%s">%s</button>`,
selected, tab.ID, html.EscapeString(tab.Label),
))
}
tablist.WriteString(`</div>`)
return tablist.String()
}

Итог: role — это способ добавить семантику элементам, для которых нет нативных тегов. Но при наличии подходящего семантического тега — всегда предпочитайте его.

Вопрос 17. Что такое тег aside и когда его следует использовать?

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

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

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

Тег <aside> предназначен для контента, который косвенно связан с основным содержимым, но может существовать отдельно от него. Часто визуально отображается как боковая панель (sidebar), но это не обязательное условие.

Когда использовать <aside>:

1. Боковая панель (sidebar):

<body>
<main>
<article>
<h1>Статья о Go</h1>
<p>Основной контент...</p>
</article>
</main>

<aside>
<h2>Рекомендуемые статьи</h2>
<ul>
<li><a href="/go-concurrency">Конкурентность в Go</a></li>
<li><a href="/go-patterns">Паттерны проектирования</a></li>
</ul>
</aside>
</body>

2. Рекламные блоки:

<aside aria-label="Реклама">
<div class="ad-banner">...</div>
</aside>

3. Блок с дополнительной информацией:

<article>
<h1>Профиль пользователя</h1>
<p>Основная информация...</p>

<aside>
<h2>Статистика</h2>
<p>Публикаций: 42</p>
<p>Подписчиков: 1500</p>
</aside>
</article>

4. Цитаты, выделенные из текста (pull quotes):

<article>
<p>Длинный текст статьи...</p>

<aside>
<blockquote>
«Ключевая цитата из статьи»
</blockquote>
</aside>

<p>Продолжение статьи...</p>
</article>

5. Группа навигационных ссылок:

<aside>
<nav aria-label="Дополнительная навигация">
<h2>Ссылки</h2>
<ul>
<li><a href="/about">О нас</a></li>
<li><a href="/contacts">Контакты</a></li>
</ul>
</nav>
</aside>

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

  • Основной контент — <main>
  • Навигация по сайту — <nav>
  • Самостоятельный контент — <article>
  • Просто группировка для стилей — <div>

Важный нюанс:

<aside> внутри <article> — контент, связанный с этой статьёй, но не являющийся её частью. <aside> вне <article> — контент, не связанный напрямую со статьёй.

<!-- aside внутри article: связанный контент -->
<article>
<h1>Статья</h1>
<p>Текст статьи...</p>
<aside>
<p><strong>Примечание:</strong> Данные актуальны на 2024 год.</p>
</aside>
</article>

<!-- aside вне article: несвязанный контент -->
<main>
<article>
<h1>Статья</h1>
<p>Текст статьи...</p>
</article>
</main>
<aside>
<h2>Реклама</h2>
<p>Содержимое не относится к статье</p>
</aside>

Accessibility:

<aside> имеет встроенную ARIA-роль complementary. Скринридер может пропустить этот блок при навигации. Рекомендуется добавлять aria-label для ясности:

<aside aria-label="Боковая панель">
...
</aside>

Генерация в Go:

type SidebarItem struct {
Title string
URL string
}

func renderSidebar(items []SidebarItem) string {
var sb strings.Builder
sb.WriteString(`<aside aria-label="Дополнительные ссылки">`)
sb.WriteString(`<h2>Рекомендуем</h2><ul>`)
for _, item := range items {
sb.WriteString(fmt.Sprintf(
`<li><a href="%s">%s</a></li>`,
html.EscapeString(item.URL),
html.EscapeString(item.Title),
))
}
sb.WriteString(`</ul></aside>`)
return sb.String()
}

Итог: <aside> — для контента, косвенно связанного с основным. Не обязательно боковая панель по расположению, главное — по смыслу.

Вопрос 18. Что означает термин accessibility в контексте HTML?

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

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

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

Accessibility (доступность, в контексте веба — a11y) — это практика создания веб-приложений, которые могут использовать все люди, включая людей с ограниченными возможностями.

Категории ограничений, которые учитывает accessibility:

1. Нарушения зрения:

  • Слепота — пользователи скринридеров (JAWS, NVDA, VoiceOver)
  • Слабое зрение — пользователи увеличения, высококонтрастных тем
  • Цветовая слепота — не могут различать определённые цвета

2. Нарушения слуха:

  • Глухие и слабослышащие — нужны субтитры и текстовые альтернативы для аудио/видео

3. Моторные нарушения:

  • Не могут использовать мышь — навигация только с клавиатуры
  • Используют переключатели (switches), устройства отслеживания взгляда
  • Ограниченная мелкая моторика — нужны крупные кликабельные области

4. Когнитивные нарушения:

  • Сложности с пониманием сложных интерфейсов
  • Сложности с фокусировкой внимания
  • Нарушения памяти

Основные принципы WCAG (Web Content Accessibility Guidelines):

Perceivable (Воспринимаемость):

  • Текстовые альтернативы для нетекстового контента (alt, aria-label)
  • Субтитры для видео
  • Контрастность текста не менее 4.5:1
  • Контент адаптируется при увеличении до 200%

Operable (Управляемость):

  • Всё доступно с клавиатуры
  • Нет контента, вызывающего судороги (мигание более 3 раз в секунду)
  • Достаточное время для чтения и взаимодействия
  • Навигационные подсказки (skip links)

Understandable (Понятность):

  • Текст читаем и понятен
  • Интерфейс работает предсказуемо
  • Помощь в предотвращении и исправлении ошибок

Robust (Надёжность):

  • Совместимость с вспомогательными технологиями
  • Корректная семантическая разметка

Практические примеры accessibility в HTML:

<!-- Навигация для пропуска повторяющихся блоков -->
<a href="#main-content" class="skip-link">Перейти к основному контенту</a>

<!-- Правильная структура заголовков -->
<h1>Заголовок страницы</h1>
<h2>Раздел</h2>
<h3>Подраздел</h3>

<!-- Доступные формы -->
<label for="email">Email</label>
<input type="email" id="email" name="email" required aria-describedby="email-hint">
<p id="email-hint">Введите ваш email для получения уведомлений</p>

<!-- Доступные кнопки -->
<button aria-expanded="false" aria-controls="menu">
Открыть меню
</button>
<nav id="menu" hidden>
<ul role="list">
<li><a href="/">Главная</a></li>
</ul>
</nav>

<!-- Доступные уведомления -->
<div role="alert" aria-live="polite">
Форма успешно отправлена
</div>

<!-- Доступные таблицы -->
<table>
<caption>Продажи по месяцам</caption>
<thead>
<tr>
<th scope="col">Месяц</th>
<th scope="col">Сумма</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Январь</th>
<td>$1000</td>
</tr>
</tbody>
</table>

Уровни соответствия WCAG:

  • A — минимальный уровень
  • AA — стандарт для большинства сайтов, требуется законодательством во многих странах
  • AAA — максимальный уровень

Accessibility для Go-разработчика:

// Генерация доступной формы
type FormField struct {
ID string
Label string
Type string
Required bool
Description string
}

func renderFormField(field FormField) string {
var sb strings.Builder

sb.WriteString(fmt.Sprintf(`<label for="%s">%s</label>`,
html.EscapeString(field.ID),
html.EscapeString(field.Label)))

required := ""
if field.Required {
required = ` required aria-required="true"`
}

ariaDesc := ""
if field.Description != "" {
ariaDesc = fmt.Sprintf(` aria-describedby="%s-hint"`, field.ID)
}

sb.WriteString(fmt.Sprintf(`<input type="%s" id="%s" name="%s"%s%s>`,
html.EscapeString(field.Type),
html.EscapeString(field.ID),
html.EscapeString(field.ID),
required,
ariaDesc))

if field.Description != "" {
sb.WriteString(fmt.Sprintf(`<p id="%s-hint">%s</p>`,
html.EscapeString(field.ID),
html.EscapeString(field.Description)))
}

return sb.String()
}

Инструменты проверки:

  • axe DevTools — расширение для браузера
  • Lighthouse — встроен в Chrome DevTools
  • WAVE — онлайн-инструмент проверки
  • NVDA/VoiceOver — скринридеры для ручного тестирования

Итог: Accessibility — это не только про людей с инвалидностью. Доступные интерфейсы лучше для всех: навигация с клавиатуры удобна для опытных пользователей, семантическая разметка улучшает SEO, чёткая структура помогает всем быстрее находить информацию.

Вопрос 19. Что такое скринридер?

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

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

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

Скринридер (screen reader) — это программное обеспечение, которое преобразует содержимое экрана в речь или тактильный вывод (шрифт Брайля), позволяя людям с нарушениями зрения взаимодействовать с компьютером и веб-сайтами.

Как работает скринридер:

Скринридер анализирует DOM-дерево страницы и формирует доступное дерево (accessibility tree), которое озвучивается синтезатором речи или выводится на дисплей Брайля.

Основные скринридеры:

  • JAWS (Windows) — коммерческий, один из самых популярных
  • NVDA (Windows) — бесплатный, открытый исходный код
  • VoiceOver (macOS, iOS) — встроен в систему от Apple
  • TalkBack (Android) — встроен в Android
  • Orca (Linux) — бесплатный, для GNOME

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

  • Клавиатура — основной способ навигации (Tab, стрелки, горячие клавиши)
  • Дисплей Брайля — тактильный вывод для пользователей, владеющих шрифтом Брайля
  • Горячие клавиши — быстрая навигация по заголовкам, ссылкам, формам

Что озвучивает скринридер:

  • Семантическую структуру (заголовки, списки, таблицы)
  • Текст ссылок и кнопок
  • alt атрибуты изображений
  • ARIA-атрибуты (aria-label, aria-expanded, aria-live)
  • Состояние элементов (отмечен ли чекбокс, раскрыт ли аккордеон)

Пример навигации скринридером:

"H1: Каталог товаров"
"Navigation: Главная, Каталог, Контакты"
"H2: Фильтры"
"Checkbox: Категория Электроника, не отмечен"
"H2: Список товаров"
"Link: Смартфон Xiaomi 14, от 45 000 рублей"
"Link: Ноутбук MacBook Pro, от 150 000 рублей"
"Footer: Подвал сайта"

Что важно для корректной работы скринридера:

<!-- ПРАВИЛЬНО: скринридер поймёт как кнопку -->
<button>Отправить</button>

<!-- НЕПРАВИЛЬНО: скринридер не поймет как кнопку -->
<div class="btn" onclick="submit()">Отправить</div>

<!-- ПРАВИЛЬНО: ссылка с понятным текстом -->
<a href="/products/123">Смартфон Xiaomi 14</a>

<!-- НЕПРАВИЛЬНО: непонятный текст ссылки -->
<a href="/products/123">Подробнее</a>
<!-- Лучше: -->
<a href="/products/123" aria-label="Смартфон Xiaomi 14 — подробнее">
Подробнее
</a>

<!-- ПРАВИЛЬНО: интерактивный элемент с состоянием -->
<button aria-expanded="false" aria-controls="details">
Показать детали
</button>
<div id="details" hidden>...</div>

<!-- НЕПРАВИЛЬНО: нет информации о состоянии -->
<button onclick="toggle()">Показать детали</button>

Live regions для динамического контента:

<!-- Скринридер автоматически озвучит изменения -->
<div aria-live="polite" aria-atomic="true" id="cart-summary">
В корзине 3 товара
</div>

<!-- Для срочных уведомлений -->
<div role="alert" aria-live="assertive">
Ошибка: недостаточно товара на складе
</div>

Как тестировать со скринридером:

  1. Отключить мышь
  2. Навигация только Tab/Shift+Tab
  3. Стрелки для перемещения по контенту
  4. Горячие клавиши для заголовков (H), ссылок (L), форм (F)

Итог: Скринридер — основной инструмент навигации для незрячих пользователей. Корректная семантическая HTML-разметка, ARIA-атрибуты и текстовые альтернативы обеспечивают полноценную работу с сайтом.

Вопрос 20. Какие ещё функции выполняют скринридеры, помимо озвучки?

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

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

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

Скринридеры выполняют множество функций, выходящих за рамки простой озвучки текста.

1. Вывод на дисплей Брайля:

Многие незрячие пользователи предпочитают шрифт Брайля речи. Скринридер выводит содержимое на подключаемый дисплей Брайля — устройство с поднимающимися шариками, которое пользователь читает пальцами.

2. Режимы навигации:

Скринридеры предоставляют различные режимы для эффективной навигации:

  • Общий обзор — быстрое перемещение по странице
  • По заголовкам — переход между H1-H6 (клавиша H)
  • По ссылкам — переход между ссылками (клавиша L для списков, K для ссылок)
  • По формам — переход между элементами форм (клавиша F)
  • По таблицам — навигация по ячейкам таблицы (Ctrl+Alt+стрелки)
  • По ориентирам — переход между семантическими регионами (main, nav, aside)
  • По списку элементов — открытие списка всех заголовков/ссылок/форм на странице (Insert+F7 в NVDA)

3. Озвучивание метаинформации:

Скринридер сообщает контекст элемента:

  • "Ссылка: Главная"
  • "Кнопка: Отправить, нажмите Enter"
  • "Чекбокс: Принять условия, не отмечен"
  • "Заголовок уровня 2: Контакты"
  • "Изображение: Золотистый ретривер играет с мячом"
  • "Таблица: Продажи по месяцам, 4 строки, 3 столбца"

4. Настройка речи:

Пользователь может настроить:

  • Скорость речи (часто значительно выше, чем обычное чтение)
  • Голос и язык
  • Уровень пунктуации (озвучивать или нет)
  • Громкость
  • Интонацию

5. Визуальные подсказки (для слабовидящих):

  • Фокус курсора — крупная подсветка текущего элемента
  • Лупа — увеличение области вокруг курсора (встроена в ОС, интегрируется со скринридером)
  • Высококонтрастный режим — инверсия цветов или контрастные темы
  • Курсор мыши — озвучивание элемента под курсором

6. Интерактивные режимы:

  • Режим форм — при фокусе на поле ввода скринридер переключается в режим, где символы вводятся напрямую, а не интерпретируются как команды
  • Виртуальный буфер — в вебе создаётся виртуальная копия страницы для навигации
  • Обзорный режим — свободное перемещение по контенту стрелками

7. Озвучивание клавиатурных команд:

  • Озвучивание нажатых клавиш (полезно для контроля ввода)
  • Подсказки по горячим клавишам конкретного элемента

8. Работа с приложениями:

Скринридеры работают не только с браузерами:

  • Офисные приложения (Word, Excel)
  • Почтовые клиенты
  • Терминалы и командная строка
  • Мобильные приложения (через TalkBack/VoiceOver)

9. Список элементов:

Быстрый доступ к структуре страницы:

NVDA + F7 → Список элементов:
- Ссылки (12)
- Заголовки (8)
- Ориентиры (4)
- Формы (3)
- Кнопки (5)

10. Озвучивание стилей текста:

В некоторых режимах скринридер озвучивает:

  • Жирный текст
  • Курсив
  • Подчёркивание
  • Изменение шрифта
  • Цвет (если настроено)

Практическое значение для разработчика:

Понимание работы скринридера помогает писать доступный код:

<!-- Скринридер объявит: "Ссылка: Подробнее о товаре Смартфон Xiaomi 14" -->
<a href="/products/123">
<span class="visually-hidden">Подробнее о товаре</span>
Смартфон Xiaomi 14
</a>

<!-- Скринридер объявит: "Диалог: Подтверждение удаления" -->
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true">
<h2 id="dialog-title">Подтверждение удаления</h2>
<p>Вы уверены, что хотите удалить товар?</p>
<button>Удалить</button>
<button>Отмена</button>
</div>

Итог: Скринридер — это не просто «программа чтения вслух». Это сложная система навигации, которая предоставляет пользователю полную информацию о структуре и состоянии интерфейса через речь и тактильный вывод.

Вопрос 21. Как скрыть содержимое от скринридера, оставив его видимым в интерфейсе?

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

Ответ собеседования: Правильный. Для скрытия элемента от скринридеров используется атрибут aria-hidden='true'. В отличие от display: none, элемент остаётся видимым в интерфейсе, но недоступным для вспомогательных технологий.

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

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

1. aria-hidden="true"

Основной способ — атрибут aria-hidden="true". Элемент остаётся видимым, но исключается из accessibility tree.

<!-- Декоративная иконка, не несущая смысловой нагрузки -->
<button>
<span aria-hidden="true">🔍</span>
Поиск
</button>

<!-- Декоративные элементы -->
<div class="decorative-pattern" aria-hidden="true">
<!-- Визуальный узор, не важный для понимания -->
</div>

Важное ограничение:

Нельзя использовать aria-hidden="true" на элементах, которые могут получить фокус (интерактивные элементы, элементы с tabindex). Это создаст ситуацию, когда фокус попадёт в «невидимый» для скринридера элемент.

<!-- НЕПРАВИЛЬНО -->
<button aria-hidden="true">Нажми меня</button>

<!-- ПРАВИЛЬНО: если нужно скрыть кнопку от скринридера,
используйте другие методы -->

2. Скрытие текста визуально, но оставление для скринридера:

Обратная задача — часто используется паттерн «visually hidden»:

.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
<!-- Иконка-кнопка с текстовой подсказкой для скринридера -->
<button>
<svg aria-hidden="true"><!-- иконка --></svg>
<span class="visually-hidden">Добавить в корзину</span>
</button>

3. Сравнение методов скрытия:

МетодВизуальноСкринридерВ DOMФокус
display: noneСкрытСкрытДаНет
visibility: hiddenСкрытСкрытДаНет
hidden атрибутСкрытСкрытДаНет
aria-hidden="true"ВидимСкрытДаДа
.visually-hiddenСкрытДоступенДаДа
opacity: 0Скрыт*ДоступенДаДа

4. Практические примеры использования aria-hidden:

<!-- Декоративные эмодзи в кнопках -->
<button aria-label="Удалить файл">
<span aria-hidden="true">🗑️</span>
</button>

<!-- Визуальные украшения в карточках -->
<article>
<h2>Название статьи</h2>
<div class="decorative-border" aria-hidden="true"></div>
<p>Содержание статьи...</p>
</article>

<!-- Дублирующий визуальный элемент -->
<table>
<caption class="visually-hidden">Продажи по месяцам</caption>
<div aria-hidden="true" class="visual-chart">
<!-- Визуальный график, данные из которого дублируются в таблице -->
</div>
</table>

<!-- Иконки в навигации -->
<nav>
<a href="/">
<svg aria-hidden="true"><!-- иконка домой --></svg>
<span>Главная</span>
</a>
</nav>

5. Генерация в Go:

// Утилита для создания visually-hidden текста
func IconButton(icon, label string) string {
return fmt.Sprintf(
`<button>
<span aria-hidden="true">%s</span>
<span class="visually-hidden">%s</span>
</button>`,
html.EscapeString(icon),
html.EscapeString(label),
)
}

// Использование
html := Button("🔍", "Поиск")
// <button><span aria-hidden="true">🔍</span>
// <span class="visually-hidden">Поиск</span></button>

Итог: aria-hidden="true" — основной инструмент для скрытия визуальных, но декоративных элементов от скринридеров. Не применяйте к фокусируемым элементам. Для обратной задачи (скрыть визуально, оставить для скринридера) используйте паттерн .visually-hidden.

Вопрос 22. Что такое изоляция стилей и какие инструменты применяются для изоляции стилей?

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

Ответ собеседования: Правильный. Изоляция стилей — механизм для избежания конфликтов между компонентами, чтобы стили одного не перезаписывали другого. Используются CSS-модули, Styled Components, Tailwind, BEM.

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

Изоляция стилей (style scoping / style isolation) — это механизм, предотвращающий влияние стилей одного компонента на стили другого. Проблема возникает из-за глобальной природы CSS — все правила применяются ко всему документу.

Проблема глобальных CSS:

/* Компонент А */
.button { background: blue; }

/* Компонент Б — случайно переопределяет стиль А */
.button { background: red; }

Инструменты и подходы к изоляции:

1. CSS Modules:

Генерирует уникальные хеши для имён классов на этапе сборки.

/* Button.module.css */
.button { background: blue; }
import styles from './Button.module.css';

function Button() {
return <button className={styles.button}>Click</button>;
}
// Результат: <button class="Button_button_x7k2a">Click</button>

2. CSS-in-JS (Styled Components, Emotion):

Стили генерируются динамически с уникальными классами.

import styled from 'styled-components';

const Button = styled.button`
background: blue;
&:hover { background: darkblue; }
`;

function App() {
return <Button>Click</Button>;
}
// Генерирует: <button class="sc-bczRLJ hKsFvv">Click</button>

3. Tailwind CSS:

Утилитарный подход — стили применяются напрямую через классы, конфликты минимальны.

function Button() {
return (
<button className="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2">
Click
</button>
);
}

4. BEM (Block Element Modifier):

Методология именования для ручной изоляции.

/* Block: card */
.card { }
/* Element: card__title */
.card__title { }
/* Modifier: card--highlighted */
.card--highlighted { }

/* Block: button */
.button { }
.button--primary { }

5. Shadow DOM:

Нативный браузерный механизм изоляции в Web Components.

class MyComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button { background: blue; }
/* Эти стили НЕ влияют на внешний документ */
</style>
<button>Click</button>
`;
}
}

6. iframe:

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

Сравнение подходов:

ПодходИзоляцияПроизводительностьСложностьSSR
CSS ModulesАвтоматическаяВысокаяНизкаяДа
CSS-in-JSАвтоматическаяСредняяСредняяДа
TailwindУтилитарнаяВысокаяНизкаяДа
BEMРучнаяВысокаяНизкаяДа
Shadow DOMПолнаяВысокаяВысокаяНет*

Практический пример проблемы и решения:

/* Без изоляции: конфликт */
/* user-card.css */
.avatar { border-radius: 50%; width: 100px; }

/* product-card.css */
.avatar { border-radius: 8px; width: 200px; }
/* Победит тот, кто загрузился последним */

/* CSS Modules: конфликта нет */
/* user-card.module.css → .avatar_a3f2 */
/* product-card.module.css → .avatar_b7k1 */

Итог: Изоляция стилей критически важна для крупных проектов с множеством компонентов. CSS Modules и CSS-in-JS обеспечивают автоматическую изоляцию, BEM — ручную через соглашения, Shadow DOM — нативную браузерную изоляцию.

Вопрос 23. В чём разница между методами use и import в контексте SCSS-препроцессора?

Таймкод: 00:18:48

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

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

@import и @use — два механизма подключения SCSS-файлов, но они работают принципиально по-разному. @use — современная замена @import, рекомендуемая с 2020 года.

1. Повторное подключение:

// _variables.scss
$primary: blue;

// style-a.scss
@use 'variables';
// color: variables.$primary;

// style-b.scss
@use 'variables';
// color: variables.$primary;

// main.scss
@use 'style-a';
@use 'style-b';
// Файл _variables.scss будет загружен только ОДИН раз

// С @import — дублирование
@import 'variables'; // загружен
@import 'variables'; // загружен повторно — дублирование кода

2. Пространства имён (namespaces):

@use создаёт пространство имён, предотвращая конфликты имён:

// _colors.scss
$primary: blue;

// _theme.scss
$primary: red;

// main.scss
@use 'colors';
@use 'theme';

.element {
color: colors.$primary; // blue
background: theme.$primary; // red
}

С @import оба файла окажутся в глобальном пространстве и произойдёт конфликт — победит последний.

3. Приватные члены:

@use позволяет создавать приватные переменные, миксины и функции через префикс - или _:

// _helpers.scss
$_private-var: 42; // приватная переменная

@function _private-fn($n) { // приватная функция
@return $n * 2;
}

@mixin public-mixin() { // публичный миксин
padding: _private-fn(10px);
}

// main.scss
@use 'helpers';
// helpers.$_private-var — ошибка, переменная приватная
// helpers._private-fn(5) — ошибка, функция приватная
// @include helpers.public-mixin() — работает

4. Настройка по умолчанию через @use with:

// _config.scss
$primary-color: blue !default;
$font-size: 16px !default;

// main.scss
@use 'config' with (
$primary-color: red,
$font-size: 18px
);
// Переопределяем значения по умолчанию

5. Переименование пространства имён:

@use 'variables' as v;
@use 'mixins' as *; // без пространства имён

.element {
color: v.$primary; // с алиасом
@include clearfix(); // без префикса
}

6. Совместимость с @forward:

@use работает в связке с @forward для создания библиотек:

// _index.scss — публичный API библиотеки
@forward 'variables';
@forward 'mixins';
@forward 'functions';

// main.scss
@use 'library'; // получаем доступ ко всем модулям библиотеки

Сравнительная таблица:

Характеристика@import@use
ДублированиеДаНет (загружается один раз)
Пространство имёнНетДа
Приватные членыНетДа
Конфликты имёнВозможныПредотвращены
Настройка по умолчаниюНетДа (with)
СтатусDeprecatedРекомендуемый

Миграция с @import на @use:

// Было (@import)
@import 'variables';
@import 'mixins';
.element {
color: $primary;
@include clearfix();
}

// Стало (@use)
@use 'variables';
@use 'mixins';
.element {
color: variables.$primary;
@include mixins.clearfix();
}

// Или без пространства имён
@use 'variables' as *;
@use 'mixins' as *;
.element {
color: $primary;
@include clearfix();
}

Итог: @use — современный и рекомендуемый способ оргазации SCSS-кода. Решает проблемы дублирования, конфликтов имён и позволяет создавать чистые публичные API для модулей. @import помечен как deprecated и будет удалён в будущих версиях Dart Sass.

Вопрос 24. Расскажите про понятие специфичности в CSS и приведите порядок приоритетов селекторов.

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

Ответ собеседования: Правильный. Специфичность — приоритет селектора, определяющий, какой стиль будет применён. По убыванию: !important (10000), инлайн-стили (1000), ID (100), класс (10), HTML-элемент (1).

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

Специфичность (specificity) — это алгоритм, по которому браузер определяет, какой CSS-правило применить, когда несколько правил претендуют на один и тот же элемент.

Формула расчёта:

Специфичность представляется как кортеж (a, b, c, d), где:

УровеньЗначениеПримеры
aИнлайн-стилиstyle="color: red" → a=1
bID-селекторы#header, #nav
cКлассы, атрибуты, псевдоклассы.button, [type="text"], :hover
dЭлементы, псевдоэлементыdiv, p, ::before

Сравнение идёт слева направо: сначала a, потом b, и т.д.

Примеры расчёта:

/* (0,0,0,1) = 1 */
div { }

/* (0,0,1,0) = 10 */
.button { }

/* (0,0,1,1) = 11 */
div.button { }

/* (0,1,0,0) = 100 */
#header { }

/* (0,1,1,1) = 111 */
#header.nav.primary { }

/* (0,0,2,1) = 21 */
ul.menu li { }

/* (0,2,1,0) = 210 */
#sidebar #widget.button { }

/* (1,0,0,0) = 10000 */
/* Инлайн-стиль в HTML: style="color: red" */

Порядок приоритетов (от низкого к высокому):

  1. Универсальный селектор * — (0,0,0,0)
  2. Элементы и псевдоэлементыdiv, ::before — (0,0,0,1)
  3. Классы, псевдоклассы, атрибуты.class, :hover, [attr] — (0,0,1,0)
  4. ID#id — (0,1,0,0)
  5. Инлайн-стилиstyle="" — (1,0,0,0)
  6. !important — переопределяет всё

Важные нюансы:

1. !important:

.button { color: blue !important; } // Победит всё кроме другого !important
#app .button { color: red !important; } // Победит предыдущий !important

!important инвертирует приоритет — побеждает тот, у кого !important стоит в более специфичном селекторе.

2. Равная специфичность — побеждает последний:

.button { color: blue; }
.button { color: red; } // Победит этот, так как он ниже в файле

3. :where() не добавляет специфичности:

/* (0,0,1,0) = 10 */
.button { color: blue; }

/* (0,0,1,0) = 10 — :where() не добавляет вес */
:where(#header) .button { color: red; }
/* Победит .button, так как специфичность равна, но он выше в файле...
нет, победит этот, так как он ниже */

4. :is() и :not() берут наиболее специфичный аргумент:

/* (0,1,0,0) = 100 — берётся #header */
:is(#header, .nav) .button { }

/* (0,1,0,0) = 100 — берётся #header */
:not(#header) .button { }

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

<div id="app">
<button class="btn primary">Click</button>
</div>
/* (0,0,1,0) = 10 */
.btn { background: gray; }

/* (0,1,1,0) = 110 */
#app .btn { background: blue; }

/* (0,1,2,0) = 120 */
#app .btn.primary { background: green; } // ПОБЕДИТ

/* (0,2,0,0) = 200 */
#app #btn { background: red; }

Антипаттерны:

/* НЕ НАДО: чрезмерная специфичность */
div.container > ul#nav > li.item > a.link { }

/* ЛУЧШЕ: минимально необходимая специфичность */
.nav-link { }

/* НЕ НАДО: !important как костыль */
.button { color: red !important; }

/* ЛУЧШЕ: увеличить специфичность селектора */
.app .button { color: red; }

Инструменты для расчёта:

Онлайн-калькулятор специфичности: https://specificity.keegan.st/

Итог: Специфичность — это взвешенная система приоритетов. Используйте минимально необходимую специфичность, избегайте !important и чрезмерной вложенности селекторов. Это делает CSS предсказуемым и поддерживаемым.

Вопрос 25. Что такое область видимости и какие типы областей видимости существуют в JavaScript?

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

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

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

Область видимости (scope) — это набор правил, определяющих, где и как можно получить доступ к переменным, функциям и объектам в коде.

Типы областей видимости:

1. Глобальная область видимости (Global Scope):

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

const appName = "MyApp"; // глобальная переменная

function showApp() {
console.log(appName); // доступна
}

showApp(); // "MyApp"

2. Функциональная область видимости (Function/Local Scope):

Переменные, объявленные внутри функции. Доступны только внутри этой функции.

function greet() {
const message = "Hello"; // локальная переменная
console.log(message); // "Hello"
}

greet();
console.log(message); // ReferenceError: message is not defined

3. Блочная область видимости (Block Scope):

Переменные, объявленные внутри блока {} с помощью let или const.

if (true) {
let blockVar = "inside block";
const alsoBlock = "also inside";
var notBlock = "escapes block";
}

console.log(blockVar); // ReferenceError
console.log(alsoBlock); // ReferenceError
console.log(notBlock); // "escapes block" — var НЕ имеет блочной области

4. Лексическая область видимости (Lexical Scope):

Вложенные функции имеют доступ к переменным внешних функций. Область видимости определяется по месту объявления, а не вызова.

const outer = "outer";

function outerFunction() {
const inner = "inner";

function innerFunction() {
const deepest = "deepest";
console.log(outer); // "outer" — доступ из глобальной
console.log(inner); // "inner" — доступ из внешней функции
console.log(deepest); // "deepest" — доступ из текущей
}

innerFunction();
}

outerFunction();

5. Модульная область видимости (Module Scope):

В ES6-модулях переменные на верхнем уровне файла НЕ попадают в глобальную область.

// module.js
const moduleVar = "module scoped"; // не глобальная
export const publicVar = "public";

// main.js
import { publicVar } from './module.js';
console.log(moduleVar); // ReferenceError

Разница var, let, const:

// var — функциональная область видимости, всплывает (hoisting)
function example() {
console.log(x); // undefined (всплыла, но не инициализирована)
var x = 10;
console.log(x); // 10
}

// let — блочная область, временная мёртвая зона (TDZ)
function example2() {
// console.log(y); // ReferenceError: TDZ
let y = 20;
console.log(y); // 20
}

// const — блочная область, нельзя переприсвоить
function example3() {
const z = 30;
// z = 40; // TypeError: Assignment to constant variable
}

Цепочка областей видимости (Scope Chain):

Когда переменная не найдена в текущей области, движок поднимается по цепочке внешних областей:

const a = "global";

function outer() {
const b = "outer";

function inner() {
const c = "inner";
console.log(a); // "global" — найдено в глобальной области
console.log(b); // "outer" — найдено в outer
console.log(c); // "inner" — найдено в текущей области
}

inner();
}

outer();

Замыкание (Closure):

Функция «запоминает» область видимости, в которой была создана, даже после того как эта область завершила выполнение:

function counter() {
let count = 0;

return function increment() {
count++;
return count;
};
}

const myCounter = counter();
console.log(myCounter()); // 1
console.log(myCounter()); // 2
console.log(myCounter()); // 3
// count сохраняется благодаря замыканию

Практические проблемы:

// Проблема: var в цикле
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Вывод: 3, 3, 3 — одна переменная i для всех итераций

// Решение: let создаёт новую переменную на каждой итерации
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Вывод: 0, 1, 2

Итог: Понимание областей видимости критически важно для предсказуемой работы с переменными. Используйте const по умолчанию, let когда нужно переприсваивать, избегайте var. Замыкания — мощный инструмент, основанный на лексической области видимости.

Вопрос 26. Для чего нужен класс Set в JavaScript?

Таймкод: 00:22:35

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

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

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

Создание Set:

// Пустой Set
const set = new Set();

// Из массива (автоматически удаляет дубликаты)
const unique = new Set([1, 2, 2, 3, 3, 3]);
console.log(unique); // Set(3) {1, 2, 3}

// Из строки
const chars = new Set("hello");
console.log(chars); // Set(4) {"h", "e", "l", "o"}

Основные методы:

const set = new Set();

// Добавление элемента
set.add(1);
set.add(2);
set.add(2); // игнорируется, уже есть
set.add(3);

// Проверка наличия
set.has(1); // true
set.has(99); // false

// Удаление элемента
set.delete(2); // true (удалён)
set.delete(99); // false (не найден)

// Размер
set.size; // 2

// Очистка
set.clear();

// Итерация
const nums = new Set([1, 2, 3]);
for (const num of nums) {
console.log(num);
}

nums.forEach((value) => console.log(value));

// Преобразование в массив
const arr = [...nums]; // [1, 2, 3]
const arr2 = Array.from(nums); // [1, 2, 3]

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

1. Удаление дубликатов из массива:

const arr = [1, 2, 2, 3, 3, 4, 4, 5];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4, 5]

2. Проверка наличия элемента (быстрее чем массив):

const allowedIds = new Set([1, 2, 3, 4, 5]);

// O(1) — константное время
allowedIds.has(3); // true

// Для массива — O(n) линейное время
[1, 2, 3, 4, 5].includes(3); // true, но медленнее на больших данных

3. Объединение множеств (union):

const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);

const union = new Set([...setA, ...setB]);
console.log(union); // Set(5) {1, 2, 3, 4, 5}

4. Пересечение (intersection):

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set(2) {3, 4}

5. Разность (difference):

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set(2) {1, 2}

Set vs Array:

ОперацияArraySet
Проверка наличия (has/includes)O(n)O(1)
ДобавлениеO(1)O(1)
УдалениеO(n)O(1)
УникальностьНетДа
Доступ по индексуДаНет
Порядок элементовДаДа (порядок вставки)

Set vs Object для уникальных значений:

// Object — только строковые ключи
const obj = {};
obj[1] = true;
obj["1"] = true; // перезаписали! Число 1 и строка "1" — один ключ

// Set — различает типы
const set = new Set();
set.add(1);
set.add("1");
console.log(set); // Set(2) {1, "1"} — два разных значения

// Set может хранить любые значения
set.add({a: 1});
set.add([1, 2]);
set.add(null);
set.add(undefined);

WeakSet:

Слабая версия Set — хранит только объекты, не препятствует сборщику мусора:

const weakSet = new WeakSet();

let obj = { id: 1 };
weakSet.add(obj);
weakSet.has(obj); // true

obj = null; // объект может быть удалён сборщиком мусора

// WeakSet не поддерживает итерацию и .size

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

Вопрос 27. В чём разница между коллекциями Map и WeakMap?

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

Ответ собеседования: Правильный. Map и WeakMap — коллекции ключ-значение, в которых ключом могут быть любые типы. В WeakMap ключ — только ссылочные типы, которые могут быть удалены сборщиком мусора. Нельзя итерироваться по WeakMap.

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

Map и WeakMap — обе хранят пары ключ-значение, но имеют принципиальные различия в управлении памятью и возможностях.

Map:

const map = new Map();

// Ключом может быть что угодно
map.set("string", "value");
map.set(42, "number key");
map.set(true, "boolean key");
map.set({id: 1}, "object key");
map.set(() => {}, "function key");
map.set(null, "null key");
map.set(undefined, "undefined key");

// Получение значения
map.get("string"); // "value"

// Проверка наличия
map.has(42); // true

// Удаление
map.delete("string");

// Размер
map.size; // 6

// Итерация
for (const [key, value] of map) {
console.log(key, value);
}

map.forEach((value, key) => console.log(key, value));

// Ключи, значения, записи
map.keys();
map.values();
map.entries();

// Очистка
map.clear();

WeakMap:

const weakMap = new WeakMap();

let user = { id: 1, name: "John" };
let order = { id: 101 };

// Ключом может быть ТОЛЬКО объект
weakMap.set(user, "admin data");
weakMap.set(order, "order details");

// Получение
weakMap.get(user); // "admin data"

// Проверка
weakMap.has(order); // true

// Удаление
weakMap.delete(user);

// НЕТ метода size
// НЕТ методов keys(), values(), entries()
// НЕТ метода forEach()
// НЕТ метода clear()
// НЕЛЬЗЯ итерировать

Ключевые различия:

ХарактеристикаMapWeakMap
Тип ключаЛюбойТолько объекты
ИтерацияДаНет
.sizeДаНет
.clear()ДаНет
Слабые ссылки на ключиНетДа
Сборка мусора ключейНетДа

Слабые ссылки и сборщик мусора:

let user = { id: 1 };
const weakMap = new WeakMap();

weakMap.set(user, "sensitive data");
console.log(weakMap.has(user)); // true

// Когда user больше не используется
user = null;

// Сборщик мусора может удалить объект и связанные данные из WeakMap
// Память будет освобождена автоматически

С Map объект будет храниться в памяти, пока существует Map, даже если на объект нет других ссылок.

Практические сценарии использования WeakMap:

1. Приватные данные для объектов:

const privateData = new WeakMap();

class User {
constructor(name, password) {
this.name = name;
// Пароль хранится в WeakMap, недоступен извне
privateData.set(this, { password, loginCount: 0 });
}

getPassword() {
return privateData.get(this).password;
}

incrementLogin() {
privateData.get(this).loginCount++;
}
}

const user = new User("John", "secret123");
console.log(user.password); // undefined — нет доступа
console.log(user.getPassword()); // "secret123"

2. Кэширование без утечек памяти:

const cache = new WeakMap();

function processObject(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}

const result = expensiveCalculation(obj);
cache.set(obj, result);
return result;
}

// Когда obj удаляется, кэш автоматически очищается

3. Хранение метаданных DOM-элементов:

const elementData = new WeakMap();

function setupElement(element) {
elementData.set(element, {
clickCount: 0,
lastClick: null
});

element.addEventListener('click', () => {
const data = elementData.get(element);
data.clickCount++;
data.lastClick = Date.now();
});
}

// Когда элемент удаляется из DOM, метаданные тоже удаляются

Когда использовать Map vs WeakMap:

  • Map — когда нужна итерация, подсчёт элементов, полный контроль над жизненным циклом.
  • WeakMap — когда нужно привязать данные к объектам без риска утечек памяти, и итерация не нужна.

Итог: WeakMap — специализированная коллекция для случаев, когда данные должны «исчезать» вместе с объектом-ключом. Это предотвращает утечки памяти и обеспечивает автоматическую очистку связанных данных.

Вопрос 28. Что такое ссылочный тип данных? Приведите примеры.

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

Ответ собеседования: Правильный. Ссылочный тип данных — это объект (object). К нему относятся: Set, Map, WeakMap, функции, объекты, классы и т.д.

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

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

Два типа данных в JavaScript:

Примитивные (primitive) типы — хранятся по значению:

  • string
  • number
  • bigint
  • boolean
  • undefined
  • symbol
  • null (технически typeof возвращает "object", но это примитив)

Ссылочные (reference) типы — хранятся по ссылке:

  • Object — обычные объекты { }
  • Array — массивы [ ]
  • Function — функции
  • Date — даты
  • RegExp — регулярные выражения
  • Map, Set, WeakMap, WeakSet
  • Promise, Error, ArrayBuffer и другие встроенные объекты
  • Экземпляры классов

Разница в поведении:

// Примитивы — копируются по значению
let a = 10;
let b = a; // b получает КОПИЮ значения
b = 20;
console.log(a); // 10 — a не изменилось

// Ссылочные типы — копируется ссылка
let obj1 = { name: "John" };
let obj2 = obj1; // obj2 указывает на ТОТ ЖЕ объект
obj2.name = "Jane";
console.log(obj1.name); // "Jane" — obj1 тоже изменился!

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

Примитивы:
a → [10]
b → [10] // отдельная копия

Ссылочные типы:
obj1 → { name: "John" } ← obj2 // обе ссылки на один объект

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

// Примитивы сравниваются по значению
10 === 10; // true
"hello" === "hello"; // true

// Ссылочные типы сравниваются по ссылке
{} === {}; // false — разные объекты
[] === []; // false — разные массивы

const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
arr1 === arr2; // false — разные ссылки, хотя содержимое одинаковое

const arr3 = arr1;
arr1 === arr3; // true — одна и та же ссылка

Мутация и ссылки:

const user = { name: "John", age: 30 };

// Мутация — изменение содержимого объекта
user.age = 31; // Все ссылки на этот объект увидят изменение

// Переприсваивание — создание нового объекта
const newUser = user; // копируем ссылку
newUser = { name: "Jane" }; // newUser теперь указывает на другой объект
// user не изменился

Поведение в функциях:

// Примитив — функция работает с копией
function increment(num) {
num++;
return num;
}

let x = 10;
increment(x);
console.log(x); // 10 — не изменился

// Ссылочный тип — функция работает с оригиналом
function updateUser(user) {
user.name = "Updated";
}

const myUser = { name: "John" };
updateUser(myUser);
console.log(myUser.name); // "Updated" — изменился!

Поверхностное копирование vs глубокое:

// Поверхностное копирование — копируется только первый уровень
const original = { name: "John", address: { city: "NYC" } };
const copy = { ...original };

copy.name = "Jane"; // не влияет на original
copy.address.city = "LA"; // ВЛИЯЕТ на original! — вложенный объект по ссылке

// Глубокое копирование — копируются все уровни
const deepCopy = JSON.parse(JSON.stringify(original));
// Или с помощью structuredClone (современный браузер)
const deepCopy2 = structuredClone(original);

Итог: Понимание ссылочных типов критически важно для предсказуемой работы с данными. Мутация объекта видна всем ссылкам на него. Для иммутабельности используйте копирование или библиотеки вроде Immer.

Вопрос 29. В чём разница между методом Object.freeze и константой (const)?

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

Ответ собеседования: Правильный. Object.freeze полностью блокирует объект — после его вызова нельзя изменять свойства. Константа (const) не позволяет переприсвоить ссылку, но внутренние поля объекта можно свободно изменять.

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

const и Object.freeze решают разные задачи и работают на разных уровнях.

const — защита переменной от переприсваивания:

const user = { name: "John" };

// Нельзя переприсвоить переменную
// user = { name: "Jane" }; // TypeError: Assignment to constant variable

// Но можно свободно менять содержимое объекта
user.name = "Jane"; // OK
user.age = 30; // OK
delete user.name; // OK
user.address = {}; // OK

Object.freeze — защита объекта от мутаций:

const user = Object.freeze({ name: "John", age: 30 });

// Нельзя менять свойства
// user.name = "Jane"; // Тихо игнорируется (или TypeError в strict mode)
// user.age = 31; // Тихо игнорируется
// delete user.name; // Тихо игнорируется
// user.address = {}; // Тихо игнорируется

console.log(user); // { name: "John", age: 30 } — не изменился

Различия на разных уровнях:

// Object.freeze — ПОВЕРХНОСТНОЕ замораживание
const config = Object.freeze({
api: { url: "https://api.example.com", timeout: 5000 },
theme: "dark"
});

// Верхний уровень защищён
// config.theme = "light"; // игнорируется

// Вложенные объекты НЕ защищены!
config.api.url = "https://evil.com"; // Работает!
console.log(config.api.url); // "https://evil.com"

Глубокое замораживание (deep freeze):

function deepFreeze(obj) {
// Получаем все свойства (включая неперечислимые)
const propNames = Object.getOwnPropertyNames(obj);

for (const name of propNames) {
const value = obj[name];
if (value && typeof value === "object" && !Object.isFrozen(value)) {
deepFreeze(value);
}
}

return Object.freeze(obj);
}

const config = deepFreeze({
api: { url: "https://api.example.com", timeout: 5000 },
theme: "dark"
});

// Теперь всё защищено
// config.api.url = "https://evil.com"; // игнорируется

Сравнительная таблица:

ОперацияconstObject.freezeconst + Object.freeze
Переприсваивание переменнойНетДаНет
Изменение свойствДаНетНет
Добавление свойствДаНетНет
Удаление свойствДаНетНет
Мутация вложенных объектовДаДаНет (с deepFreeze)

Object.isFrozen — проверка:

const frozen = Object.freeze({ a: 1 });
const normal = { a: 1 };

Object.isFrozen(frozen); // true
Object.isFrozen(normal); // false

Seal vs Freeze vs PreventExtensions:

// Object.seal — нельзя добавлять/удалять свойства, но можно менять
const sealed = Object.seal({ a: 1, b: 2 });
sealed.a = 10; // OK
// sealed.c = 3; // Нельзя добавить
// delete sealed.a; // Нельзя удалить

// Object.freeze — нельзя ничего (seal + значения read-only)
const frozen = Object.freeze({ a: 1 });
// frozen.a = 10; // Нельзя изменить

// Object.preventExtensions — нельзя добавлять, но можно менять и удалять
const ext = Object.preventExtensions({ a: 1 });
ext.a = 10; // OK
delete ext.a; // OK
// ext.b = 2; // Нельзя добавить

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

// Константы конфигурации
const CONFIG = Object.freeze({
API_URL: "https://api.example.com",
MAX_RETRIES: 3,
TIMEOUT: 5000,
});

// Неизменяемые данные
const INITIAL_STATE = deepFreeze({
users: [],
loading: false,
error: null,
});

// Enums
const STATUS = Object.freeze({
PENDING: "pending",
APPROVED: "approved",
REJECTED: "rejected",
});

Итог: const защищает переменную от переприсваивания, Object.freeze защищает объект от мутаций. Для полной иммутабельности используйте их вместе. Помните, что Object.freeze — поверхностный, для глубокой заморозки нужна рекурсия.

Вопрос 30. В чём разница между тернарным оператором и оператором нулевого слияния (??)?

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

Ответ собеседования: Правильный. Оператор нулевого слияния (??) возвращает правый операнд, только если левый равен null или undefined. Тернарный оператор — сокращённый if/else, проверяет произвольное условие и возвращает один из двух операндов.

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

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

Тернарный оператор (?:):

Полная замена if/else для выражений. Проверяет любое условие на истинность.

const age = 20;
const status = age >= 18 ? "adult" : "minor";

// Эквивалент:
let status;
if (age >= 18) {
status = "adult";
} else {
status = "minor";
}

// Вложенные тернарные операторы (не рекомендуется для читаемости)
const score = 85;
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";

Оператор нулевого слияния (??):

Возвращает правый операнд только если левый — null или undefined.

const value = null;
const result = value ?? "default";
console.log(result); // "default"

const value2 = 0;
const result2 = value2 ?? "default";
console.log(result2); // 0 — НЕ null/undefined

const value3 = "";
const result3 = value3 ?? "default";
console.log(result3); // "" — НЕ null/undefined

const value4 = false;
const result4 = value4 ?? "default";
console.log(result4); // false — НЕ null/undefined

Ключевое отличие от || (логическое ИЛИ):

const value = 0;

// || — проверяет на falsy значение (0, "", false, null, undefined, NaN)
value || "default"; // "default" — 0 считается falsy

// ?? — проверяет ТОЛЬКО на null/undefined
value ?? "default"; // 0 — НЕ null/undefined, возвращается 0

const emptyString = "";
emptyString || "default"; // "default"
emptyString ?? "default"; // ""

const isFalse = false;
isFalse || "default"; // "default"
isFalse ?? "default"; // false

Сравнительная таблица:

Выражение||???:
Проверкаfalsynull/undefinedпроизвольное условие
0правыйлевыйзависит от условия
""правыйлевыйзависит от условия
falseправыйлевыйзависит от условия
nullправыйправыйзависит от условия
undefinedправыйправыйзависит от условия
NaNправыйлевыйзависит от условия

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

// ?? — значения по умолчанию для API-ответов
const user = {
name: response.name ?? "Anonymous",
age: response.age ?? 0, // 0 сохраняется
bio: response.bio ?? "", // пустая строка сохраняется
isActive: response.isActive ?? false, // false сохраняется
};

// || — когда нужно отсечь все falsy значения
const displayValue = input || "placeholder";

// ?: — условная логика
const message = isLoggedIn
? `Welcome back, ${name}!`
: "Please log in";

// Комбинация
const config = {
port: process.env.PORT ?? 3000, // ?? для числа 0
debug: process.env.DEBUG === "true", // ?: для boolean
host: process.env.HOST || "localhost", // || для строки
};

Optional chaining с нулевым слиянием:

const user = { name: "John" };

// Безопасный доступ с дефолтом
user?.address?.city ?? "Unknown"; // "Unknown"
user?.preferences?.theme ?? "light"; // "light"

// С тернарным оператором — более громоздко
user?.address?.city !== undefined && user?.address?.city !== null
? user.address.city
: "Unknown";

Оператор присваивания с нулевым слиянием (??=):

const config = { port: 3000 };

// Присваивает только если текущее значение null/undefined
config.port ??= 8080; // 3080 — port уже есть
config.host ??= "localhost"; // "localhost" — host не было

console.log(config); // { port: 3000, host: "localhost" }

Итог: Используйте ?? когда нужно задать значение по умолчанию для null/undefined, сохраняя 0, false, "". Используйте ?: для произвольных условий. Используйте || когда нужно отсечь все falsy значения.

Вопрос 31. Какие виды функций существуют в JavaScript и чем они отличаются?

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

Ответ собеседования: Правильный. Function Declaration (всплывают), Function Expression (не всплывают), стрелочные функции (нет собственного this, нет arguments, this определяется при создании).

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

JavaScript предоставляет несколько способов объявления функций, каждый со своими особенностями.

1. Function Declaration (объявление функции):

// Всплывает (hoisting) — можно вызвать до объявления
greet(); // "Hello!"

function greet() {
console.log("Hello!");
}

// Имеет собственное this
const obj = {
name: "John",
greet: function() {
console.log(this.name); // "John"
}
};

// Имеет псевдомассив arguments
function sum() {
console.log(arguments); // [1, 2, 3]
return [...arguments].reduce((a, b) => a + b, 0);
}
sum(1, 2, 3);

// Можно использовать как конструктор
function User(name) {
this.name = name;
}
const user = new User("John");

2. Function Expression (функциональное выражение):

// НЕ всплывает — нельзя вызвать до объявления
// greet(); // TypeError: greet is not a function

const greet = function() {
console.log("Hello!");
};

// Может быть именованным (для рекурсии и отладки)
const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1); // можно обратиться по имени
};

// Имеет собственное this и arguments — как Function Declaration

3. Arrow Function (стрелочная функция):

// Краткий синтаксис
const add = (a, b) => a + b;

// Без аргументов
const greet = () => "Hello!";

// Один аргумент — скобки необязательны
const double = n => n * 2;

// Тело с фигурными скобками — нужен return
const sum = (a, b) => {
const result = a + b;
return result;
};

// НЕ имеет собственного this
const obj = {
name: "John",
greet: () => {
console.log(this.name); // undefined — this из внешней области
}
};

// НЕ имеет arguments
const fn = () => {
// console.log(arguments); // ReferenceError
};

// НЕЛЬЗЯ использовать как конструктор
// const User = (name) => { this.name = name; };
// new User("John"); // TypeError: User is not a constructor

// this определяется в момент создания (лексически)

Сравнительная таблица:

ХарактеристикаFunction DeclarationFunction ExpressionArrow Function
HoistingДаНетНет
Собственный thisДаДаНет
argumentsДаДаНет
КонструкторДаДаНет
Имя в стеке вызововДаОпциональноНет*
Синтаксис newДаДаНет

Практические примеры различий в this:

const team = {
name: "Developers",
members: ["John", "Jane", "Bob"],

// Function Declaration — this указывает на team
showTeam: function() {
console.log(`Team: ${this.name}`);
},

// Проблема: обычная функция в колбэке теряет this
showMembersBroken: function() {
this.members.forEach(function(member) {
// this === undefined (strict mode) или window
// console.log(`${this.name}: ${member}`); // Не работает
});
},

// Решение 1: стрелочная функция сохраняет this
showMembersArrow: function() {
this.members.forEach((member) => {
console.log(`${this.name}: ${member}`); // Работает!
});
},

// Решение 2: bind
showMembersBind: function() {
this.members.forEach(function(member) {
console.log(`${this.name}: ${member}`);
}.bind(this));
},

// Решение 3: сохранить this в переменную
showMembersVar: function() {
const self = this;
this.members.forEach(function(member) {
console.log(`${self.name}: ${member}`);
});
}
};

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

// Function Declaration — основные функции, методы объектов, конструкторы
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}

// Function Expression — когда нужна условная инициализация
const handler = isProduction
? function() { /* production logic */ }
: function() { /* dev logic */ };

// Arrow Function — колбэки, короткие функции, когда нужно сохранить this
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);

const obj = {
items: [1, 2, 3],
print() {
this.items.forEach(item => {
console.log(item); // this сохраняет контекст obj
});
}
};

Итог: Используйте стрелочные функции для колбэков и коротких выражений, где нужно сохранить лексический this. Обычные функции — для методов объектов, конструкторов и когда нужен arguments. Стрелочные функции не могут быть конструкторами и не имеют собственного this и arguments.

Вопрос 32. Что такое функции-генераторы и каков их механизм работы?

Таймкод: 00:27:35

Ответ собеседования: Правильный. Генераторы — функции, которые можно приостанавливать, возвращая промежуточные значения через yield, и возобновлять через next(). Основаны на Symbol.iterator.

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

Генераторы — это особый тип функций, которые могут приостанавливать своё выполнение и возобновлять его позже, возвращая промежуточные значения по запросу (lazy evaluation).

Создание генератора:

// Звёздочка после function — признак генератора
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}

// Вызов генератора НЕ выполняет код сразу
// Возвращает объект-генератор (iterator)
const gen = numberGenerator();

// next() — выполняет код до следующего yield
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

Механизм работы:

console.log("Start");
yield 1; // пауза, возвращает 1
console.log("After 1");
yield 2; // пауза, возвращает 2
console.log("After 2");
yield 3; // пауза, возвращает 3
console.log("End");
// неявный return undefined
}

const gen = generator();

gen.next(); // "Start" → { value: 1, done: false }
gen.next(); // "After 1" → { value: 2, done: false }
gen.next(); // "After 2" → { value: 3, done: false }
gen.next(); // "End" → { value: undefined, done: true }

Передача значений в генератор:

const name = yield "What is your name?";
const age = yield `Hello, ${name}! How old are you?`;
return `${name} is ${age} years old`;
}

const gen = conversation();

console.log(gen.next()); // { value: "What is your name?", done: false }
console.log(gen.next("John")); // { value: "Hello, John! How old are you?", done: false }
console.log(gen.next(25)); // { value: "John is 25 years old", done: true }

Итерация по генератору:

yield 1;
yield 2;
yield 3;
}

// for...of автоматически вызывает next()
for (const value of generator()) {
console.log(value); // 1, 2, 3
}

// Spread оператор
const values = [...generator()]; // [1, 2, 3]

// Деструктуризация
const [a, b, c] = generator(); // a=1, b=2, c=3

Бесконечные генераторы:

let i = 0;
while (true) {
yield i++;
}
}

const infinite = infiniteCounter();
infinite.next(); // { value: 0, done: false }
infinite.next(); // { value: 1, done: false }
infinite.next(); // { value: 2, done: false }
// Можно продолжать бесконечно — память не переполнится

Генератор для обхода дерева:

if (node.children) {
for (const child of node.children) {
yield* traverse(child); // делегирование другому генератору
}
}
yield node.value;
}

const tree = {
value: "root",
children: [
{ value: "child1", children: [{ value: "grandchild1" }] },
{ value: "child2" }
]
};

for (const value of traverse(tree)) {
console.log(value);
}
// "grandchild1", "child1", "child2", "root"

yield — делегирование:*

yield* [1, 2, 3]; // делегирует массиву
yield* "hello"; // делегирует строке
yield* new Set([4, 5]); // делегирует Set
}

console.log([...delegator()]); // [1, 2, 3, "h", "e", "l", "l", "o", 4, 5]

throw и return в генераторах:

try {
yield 1;
yield 2;
} catch (e) {
console.log("Caught:", e.message);
yield "error handled";
}
}

const gen = errorGenerator();
gen.next(); // { value: 1, done: false }
gen.throw(new Error("Oops")); // "Caught: Oops" → { value: "error handled", done: false }

// return() — принудительное завершение
const gen2 = errorGenerator();
gen2.next(); // { value: 1, done: false }
gen2.return("done"); // { value: "done", done: true }

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

1.Ленивые вычисления:

let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}

const fib = fibonacci();
// Вычисляем только нужное количество элементов
const first10 = Array.from({ length: 10 }, () => fib.next().value);
console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

2. Управление асинхронными потоками (до async/await):

const users = yield fetch('/api/users');
const posts = yield fetch('/api/posts');
return { users, posts };
}

// С помощью библиотеки-раннера (co, bluebird)
run(generator);

3. Пагинация данных:

let page = 1;
while (true) {
const data = yield fetch(`/api/items?page=${page}`);
if (data.length === 0) return;
yield* data;
page++;
}
}

Итог: Генераторы — мощный инструмент для ленивых вычислений, управления потоками данных и создания итераторов. Они позволяют приостанавливать выполнение функции и возобновлять его, передавая значения в обе стороны. Основа для async/await.

Вопрос 33. Что такое замыкание?

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

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

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

Замыкание (closure) — это способность функции «запоминать» и иметь доступ к переменным из внешней области видимости, даже после того как внешняя функция завершила своё выполнение.

Механизм работы:

function outer() {
let count = 0; // переменная во внешней функции

function inner() {
// inner имеет доступ к count даже после завершения outer
count++;
return count;
}

return inner;
}

const counter = outer(); // outer завершилась, но count "жива"
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count сохраняется в замыкании

Почему переменная не удаляется:

Когда функция outer возвращает inner, движок JavaScript видит, что inner ссылается на переменную count. Поэтому count не удаляется сборщиком мусора, а сохраняется в специальной внутренней структуре (Environment Record), доступной через замыкание.

Классический пример — фабрика функций:

function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15
// Каждая функция сохраняет свой multiplier в замыкании

Замыкание в цикле (классическая ловушка):

// Проблема: var не имеет блочной области видимости
const functions = [];

for (var i = 0; i < 3; i++) {
functions.push(function() {
return i;
});
}

console.log(functions[0]()); // 3, не 0!
console.log(functions[1]()); // 3, не 1!
console.log(functions[2]()); // 3, не 2!
// Все функции ссылаются на одну и ту же переменную i

// Решение 1: let (создаёт новую переменную на каждой итерации)
const functions2 = [];
for (let i = 0; i < 3; i++) {
functions2.push(function() {
return i;
});
}
console.log(functions2[0]()); // 0 ✓

// Решение 2: IIFE для создания нового scope
const functions3 = [];
for (var i = 0; i < 3; i++) {
(function(j) {
functions3.push(function() {
return j;
});
})(i);
}
console.log(functions3[0]()); // 0 ✓

Приватные переменные через замыкание:

function createBankAccount(initialBalance) {
let balance = initialBalance; // приватная переменная

return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
throw new Error("Amount must be positive");
},
withdraw(amount) {
if (amount > balance) {
throw new Error("Insufficient funds");
}
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 100
console.log(account.deposit(50)); // 150
console.log(account.withdraw(30)); // 120
// console.log(account.balance); // undefined — нет прямого доступа!

Мемоизация с замыканием:

function memoize(fn) {
const cache = {}; // сохраняется в замыкании

return function(...args) {
const key = JSON.stringify(args);

if (cache[key] !== undefined) {
console.log("From cache");
return cache[key];
}

console.log("Computing");
const result = fn(...args);
cache[key] = result;
return result;
};
}

const factorial = memoize(function(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
});

console.log(factorial(5)); // Computing → 120
console.log(factorial(5)); // From cache → 120
console.log(factorial(6)); // Computing (uses cached 5!) → 720

Debounce через замыкание:

function debounce(fn, delay) {
let timerId; // сохраняется в замыкании

return function(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

const debouncedSearch = debounce(function(query) {
console.log("Searching for:", query);
}, 300);

// Быстрый вызов — выполнится только последний
debouncedSearch("a");
debouncedSearch("ap");
debouncedSearch("app");
debouncedSearch("appl");
debouncedSearch("apple");
// Через 300ms: "Searching for: apple"

Итог: Замыкание — один из фундаментальных механизмов JavaScript. Оно позволяет создавать приватные переменные, фабрики функций, мемоизацию, debounce/throttle и многое другое. Понимание замыканий критически важно для написания чистого и эффективного кода.

Вопрос 34. Как можно оптимизировать последовательный вызов map и filter для массива?

Таймкод: 00:29:08

Ответ собеседования: Правильный. Последовательные map и filter можно заменить на reduce, пройдясь по массиву один раз вместо нескольких.

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

Последовательные вызовы map и filter создают промежуточные массивы и проходят по данным несколько раз. Это можно оптимизировать.

Проблема — несколько проходов:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Три прохода по массиву + два промежуточных массива
const result = numbers
.filter(n => n % 2 === 0) // проход 1: [2, 4, 6, 8, 10]
.map(n => n * 2) // проход 2: [4, 8, 12, 16, 20]
.filter(n => n > 10); // проход 3: [12, 16, 20]

// Создано 2 промежуточных массива, которые потом попадают в GC

Решение 1 — reduce:

const result = numbers.reduce((acc, n) => {
if (n % 2 === 0) {
const doubled = n * 2;
if (doubled > 10) {
acc.push(doubled);
}
}
return acc;
}, []);

// Один проход, без промежуточных массивов

Решение 2 — for цикл:

const result = [];
for (let i = 0; i < numbers.length; i++) {
const n = numbers[i];
if (n % 2 === 0) {
const doubled = n * 2;
if (doubled > 10) {
result.push(doubled);
}
}
}

Решение 3 — flatMap (для filter + map):

// filter + map в один проход
const result = numbers.flatMap(n =>
n % 2 === 0 ? [n * 2] : []
);

// Но для сложных цепочек не подходит

Решение 4 — генераторы (ленивые вычисления):

function* filter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}

function* map(iterable, transform) {
for (const item of iterable) {
yield transform(item);
}
}

// Вычисления происходят только при итерации
const pipeline = filter(
map(
filter(numbers, n => n % 2 === 0),
n => n * 2
),
n => n > 10
);

console.log([...pipeline]); // [12, 16, 20]

Решение 5 — библиотека itertools:

// Lodash — ленивые вычисления
import _ from 'lodash';

const result = _(numbers)
.filter(n => n % 2 === 0)
.map(n => n * 2)
.filter(n => n > 10)
.value();
// Lodash оптимизирует цепочку — один проход

// RxJS — реактивные потоки
import { from } from 'rxjs';
import { filter, map } from 'rxjs/operators';

from(numbers).pipe(
filter(n => n % 2 === 0),
map(n => n * 2),
filter(n => n > 10)
).subscribe(result => console.log(result));

Решение 6 — Array.prototype с кастомным методом:

// Создание утилиты для цепочки операций
function chain(array) {
let result = array;

return {
filter(predicate) {
result = result.filter(predicate);
return this;
},
map(transform) {
result = result.map(transform);
return this;
},
value() {
return result;
}
};
}

// Или более эффективная версия — собираем операции и выполняем за один проход
function optimizedChain(array) {
const operations = [];

return {
filter(predicate) {
operations.push({ type: 'filter', fn: predicate });
return this;
},
map(transform) {
operations.push({ type: 'map', fn: transform });
return this;
},
value() {
return array.reduce((acc, item) => {
let current = item;
for (const op of operations) {
if (op.type === 'filter' && !op.fn(current)) {
return acc;
}
if (op.type === 'map') {
current = op.fn(current);
}
}
acc.push(current);
return acc;
}, []);
}
};
}

Сравнение производительности:

// Бенчмарк на большом массиве
const data = Array.from({ length: 1000000 }, (_, i) => i);

// Медленно: 3 прохода
console.time('chain');
data.filter(n => n % 2).map(n => n * 2).filter(n => n > 100);
console.timeEnd('chain');

// Быстро: 1 проход
console.time('reduce');
data.reduce((acc, n) => {
if (n % 2) {
const doubled = n * 2;
if (doubled > 100) acc.push(doubled);
}
return acc;
}, []);
console.timeEnd('reduce');

Когда оптимизация не нужна:

// Для небольших массивов (< 1000 элементов) — разница незаметна
// Читаемость важнее микрооптимизации
const activeUserNames = users
.filter(u => u.isActive)
.map(u => u.name);

// Лучше оставить так — читаемо и понятно

Итог: Для оптимизации цепочек map/filter используйте reduce, for цикл или ленивые вычисления (генераторы, Lodash). Но помните — для небольших массивов разница незаметна, а читаемость кода часто важнее микрооптимизации. Оптимизируйте только при наличии реальной проблемы с производительностью.

Вопрос 35. Расскажите о типах задач в Event Loop и порядке их выполнения.

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

Ответ собеседования: Правильный. В Event Loop есть синхронные задачи, микрозадачи (промисы, queueMicrotask, MutationObserver) и макрозадачи (setTimeout, setInterval, DOM-события). Сначала синхронный код, затем все микрозадачи, затем одна макрозадача. После проверяется перерисовка, цикл повторяется.

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

Event Loop — это механизм, обеспечивающий неблокирующую конкурентность в JavaScript. Он управляет порядком выполнения различных типов задач.

Архитектура Event Loop:

┌─────────────────────────────┐
│ Call Stack │ ← синхронный код
│ (стек вызовов) │
└──────────────┬──────────────┘
│ пусто?

┌─────────────────────────────┐
│ Microtask Queue │ ← микрозадачи
│ - Promise.then/catch/finally│
│ - queueMicrotask() │
│ - MutationObserver │
└──────────────┬──────────────┘
│ пусто?

┌─────────────────────────────┐
│ Macrotask Queue │ ← макрозадачи
│ - setTimeout │
│ - setInterval │
│ - setImmediate (Node.js) │
│ - I/O │
│ - UI rendering │
│ - requestAnimationFrame │
└──────────────┬──────────────┘


┌──────────────┐
│ Render │ ← перерисовка (если нужно)
│ (отрисовка) │
└──────────────┘

Порядок выполнения:

1. Синхронный код (Call Stack):

Выполняется полностью, блокирует всё остальное.

console.log("1");
console.log("2");
console.log("3");
// Выполнится первым, полностью

2. Микрозадачи (Microtask Queue):

Выполняются все, пока очередь не опустеет. Добавленные во время выполнения микрозадачи тоже выполняются в текущем цикле.

  • Promise.then/catch/finally
  • queueMicrotask()
  • MutationObserver
  • process.nextTick (Node.js, приоритетнее промисов)

3. Макрозадачи (Macrotask Queue):

Выполняется одна макрозадача, затем снова все микрозадачи.

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • I/O операции
  • UI rendering
  • requestAnimationFrame

Демонстрация порядка:

console.log("1 - синхронный");

setTimeout(() => {
console.log("2 - макрозадача (setTimeout)");
Promise.resolve().then(() => {
console.log("3 - микрозадача внутри макрозадачи");
});
}, 0);

Promise.resolve().then(() => {
console.log("4 - микрозадача (Promise)");
Promise.resolve().then(() => {
console.log("5 - вложенная микрозадача");
});
});

queueMicrotask(() => {
console.log("6 - микрозадача (queueMicrotask)");
});

console.log("7 - синхронный");

// Вывод:
// 1 - синхронный
// 7 - синхронный
// 4 - микрозадача (Promise)
// 6 - микрозадача (queueMicrotask)
// 5 - вложенная микрозадача
// 2 - макрозадача (setTimeout)
// 3 - микрозадача внутри макрозадачи

Важные нюансы:

1. Микрозадачи могут «голодать»:

function hungry() {
Promise.resolve().then(hungry);
}

hungry();
// Бесконечный цикл микрозадач — макрозадачи никогда не выполнятся!
// UI заблокирован

2. process.nextTick в Node.js:

// Node.js: nextTick имеет приоритет над промисами
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));

// Вывод:
// nextTick
// promise

3. Рендеринг между макрозадачами:

// Браузер рендерит кадр каждые ~16.6ms (60fps)
// Рендеринг происходит после макрозадачи и её микрозадач
// requestAnimationFrame вызывается перед рендерингом

Сложный пример:

console.log("script start");

setTimeout(() => {
console.log("setTimeout");
}, 0);

Promise.resolve()
.then(() => {
console.log("promise1");
})
.then(() => {
console.log("promise2");
});

queueMicrotask(() => {
console.log("microtask");
});

console.log("script end");

// Порядок вывода:
// script start
// script end
// promise1
// microtask
// promise2
// setTimeout

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

Цикл 1:
Call Stack: синхронный код → пуст
Microtask Queue: выполняем ВСЕ → пуст
Macrotask Queue: берём ОДНУ (setTimeout)
Render: если нужно

Цикл 2:
Call Stack: callback от setTimeout → пуст
Microtask Queue: микрозадачи, добавленные в callback → пуст
Macrotask Queue: следующая макрозадача
Render: если нужно

... и так далее

Итог: Event Loop гарантирует порядок: синхронный код → все микрозадачи → одна макрозадача → рендеринг → повтор. Микрозадачи выполняются полностью перед следующей макрозадачей. Это важно для предсказуемого поведения асинхронного кода и предотвращения блокировки UI.

Вопрос 36. Какие инструменты JavaScript можно использовать для выполнения ресурсоёмких задач на фронте без блокировки основного потока?

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

Ответ собеседования: Правильный. Основной инструмент — Web Workers: тяжёлый скрипт выносится в отдельный поток, не блокируя основной, и через postMessage происходит обмен данными. Второй вариант — разбиение задачи на части через setTimeout/setInterval.

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

JavaScript однопоточен, поэтому ресурсоёмкие задачи блокируют основной поток. Существует несколько инструментов для решения этой проблемы.

1. Web Workers:

Отдельный поток выполнения с собственным глобальным контекстом. Общение через postMessage.

// main.js
const worker = new Worker('worker.js');

worker.postMessage({ type: 'CALCULATE', data: [1, 2, 3, 4, 5] });

worker.onmessage = (event) => {
console.log('Result:', event.data);
};

worker.onerror = (error) => {
console.error('Worker error:', error);
};

// worker.js
self.onmessage = (event) => {
const { type, data } = event.data;

if (type === 'CALCULATE') {
// Ресурсоёмкая задача
const result = data.reduce((sum, n) => {
// Тяжёлые вычисления
for (let i = 0; i < 1000000; i++) {
n = Math.sqrt(n + i);
}
return sum + n;
}, 0);

self.postMessage(result);
}
};

Ограничения Web Workers:

  • Нет доступа к DOM
  • Нет доступа к window, document, parent
  • Общение только через postMessage (копирование данных)
  • Для больших данных — Transferable объекты (передача без копирования)
// Transferable — передача без копирования (zero-copy)
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB
worker.postMessage(buffer, [buffer]);
// buffer становится невалидным в основном потоке

2. SharedArrayBuffer + Atomics:

Разделяемая память между потоками:

// main.js
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);

const worker = new Worker('worker.js');
worker.postMessage({ sharedBuffer });

// worker.js
self.onmessage = (event) => {
const sharedArray = new Int32Array(event.data.sharedBuffer);
Atomics.add(sharedArray, 0, 1); // потокобезопасная операция
};

3. Service Worker:

Работает в отдельном потоке, перехватывает сетевые запросы, управляет кэшем.

// service-worker.js
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
}
});

// main.js
navigator.serviceWorker.register('/service-worker.js');

4. requestIdleCallback:

Выполнение задач в периоды простоя браузера:

function processChunk(deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
const task = tasks.shift();
processTask(task);
}

if (tasks.length > 0) {
requestIdleCallback(processChunk);
}
}

requestIdleCallback(processChunk);
// deadline.timeRemaining() — миллисекунды до следующего кадра

5. requestAnimationFrame:

Для анимаций и задач, связанных с отрисовкой:

function animate() {
// Обновление анимации
updatePositions();

requestAnimationFrame(animate);
}

requestAnimationFrame(animate);
// Вызывается ~60 раз в секунду перед рендерингом

6. Разбиение задачи на части (Time Slicing):

function processLargeArray(array, processItem) {
let index = 0;

function processChunk() {
const end = Math.min(index + 100, array.length); // по 100 элементов

for (; index < end; index++) {
processItem(array[index]);
}

if (index < array.length) {
setTimeout(processChunk, 0); // отдаём управление браузеру
}
}

processChunk();
}

// Или через async/await
async function processLargeArrayAsync(array, processItem) {
for (let i = 0; i < array.length; i++) {
processItem(array[i]);

// Каждые 100 элементов — пауза
if (i % 100 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}

7. WebAssembly (WASM):

Выполнение компилированного кода в отдельном потоке с производительностью, близкой к нативной:

// Использование WASM модуля
const wasmModule = await WebAssembly.instantiateStreaming(fetch('module.wasm'));
const result = wasmModule.instance.exports.heavyCalculation(data);

8. Scheduler API (экспериментальный):

// Планирование задач с приоритетами
if ('scheduler' in globalThis) {
scheduler.postTask(() => {
// Фоновая задача
}, { priority: 'background' });

scheduler.postTask(() => {
// Задача пользователя
}, { priority: 'user-visible' });
}

Сравнение инструментов:

ИнструментОтдельный потокДоступ к DOMСложностьПрименение
Web WorkerДаНетСредняяТяжёлые вычисления
SharedArrayBufferДаНетВысокаяРазделяемая память
Service WorkerДаНетСредняяКэширование, сеть
requestIdleCallbackНетДаНизкаяФоновые задачи
Time SlicingНетДаНизкаяРазбиение задач
WebAssemblyДа/НетНетВысокаяМаксимальная производительность

Итог: Для тяжёлых вычислений — Web Workers. Для фоновых задач — requestIdleCallback. Для задач с DOM — time slicing. Для максимальной производительности — WebAssembly. Выбор зависит от конкретной задачи и требований к взаимодействию с DOM.

Вопрос 37. В чём разница между интерфейсами (interface) и типами (type) в TypeScript?

Таймкод: 00:31:39

Ответ собеседования: Правильный. Типы могут типизировать любые данные, интерфейсы — только объекты и классы. Интерфейсы расширяются через extends и объединяются при одинаковых именах. Типы расширяются через амперсанд, при дублировании — ошибка.

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

interface и type в TypeScript во многом пересекаются, но имеют важные различия.

1. Declaration Merging (объединение объявлений):

Интерфейсы с одинаковым именем автоматически объединяются. Типы — нет.

// Интерфейсы — объединяются
interface User {
name: string;
}

interface User {
age: number;
}

// Результат: { name: string; age: number }
const user: User = { name: "John", age: 30 };

// Типы — ошибка
type User = { name: string };
type User = { age: number }; // Error: Duplicate identifier 'User'

Это свойство активно используется для расширения типов из сторонних библиотек:

// Расширение Window из библиотеки
declare global {
interface Window {
myCustomProperty: string;
}
}

2. Расширение (extends vs &):

// Интерфейсы — extends
interface Animal {
name: string;
}

interface Dog extends Animal {
breed: string;
}

// Типы — пересечение (&)
type Animal = {
name: string;
};

type Dog = Animal & {
breed: string;
};

// Можно комбинировать
type Cat = Animal & { whiskers: boolean } & { color: string };

3. Типы могут описывать больше:

// Примитивы
type ID = string | number;
type Status = "pending" | "active" | "inactive";

// Union типы
type StringOrNumber = string | number;

// Tuple
type Point = [number, number];

// Mapped types
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

// Conditional types
type NonNullable<T> = T extends null | undefined ? never : T;

// Интерфейсы НЕ могут этого делать
// interface ID = string | number; // Error

4. implements:

Классы могут реализовать и interface, и type:

interface Printable {
print(): void;
}

type Loggable = {
log(): string;
};

class Document implements Printable, Loggable {
print() { /* ... */ }
log() { return "log"; }
}

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

Интерфейсы немного быстрее обрабатываются компилятором TypeScript благодаря тому, что они всегда описывают объекты и кэшируются по имени. Разница обычно незаметна.

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

// Используйте interface для:
// - Публичных API (библиотеки, контракты)
// - Объектов, которые могут быть расширены через declaration merging
// - Имплементации классами

interface ApiResponse {
data: unknown;
status: number;
}

interface Repository<T> {
find(id: string): Promise<T>;
save(entity: T): Promise<void>;
}

// Используйте type для:
// - Union типов
// - Mapped types
// - Conditional types
// - Tuple
// - Примитивных алиасов

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Nullable<T> = T | null | undefined;
type AsyncFunction<T> = () => Promise<T>;

Итог: Используйте interface для описания объектов и контрактов, особенно если возможно расширение. Используйте type для union, mapped, conditional типов и примитивных алиасов. В большинстве случаев выбор — вопрос стиля и предпочтений команды.

Вопрос 38. Объсните назначение типов any и unknown. В чём их разница?

Таймкод: 00:32:38

Ответ собеседования: Правильный. any — небезопасный тип для значений с неизвестным типом, позволяет обращаться к любым свойствам без ошибок. unknown — безопасная альтернатива, не позволяет операции до определения типа.

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

any и unknown оба представляют «любой тип», но принципиально отличаются в плане безопасности типов.

any — отключает проверку типов:

let data: any;

// Всё разрешено — никаких ошибок компиляции
data.foo.bar.baz; // OK
data(); // OK
new data(); // OK
data[0][1][2]; // OK

// Можно присвоить куда угодно
const str: string = data; // OK
const num: number = data; // OK

// Ошибка только в runtime!
// data.nonExistentMethod() → TypeError

any полностью отключает TypeScript для этой переменной. Это «дыра» в системе типов.

unknown — требует проверки типа перед использованием:

let data: unknown;

// Ничего не разрешено без проверки типа
// data.foo; // Error: Object is of type 'unknown'
// data(); // Error: Object is of type 'unknown'
// data[0]; // Error: Object is of type 'unknown'

// Можно присвоить только в unknown или any
// const str: string = data; // Error: Type 'unknown' is not assignable to 'string'

// Нужна проверка типа (type narrowing)
if (typeof data === 'string') {
console.log(data.toUpperCase()); // OK — TypeScript знает, что это string
}

if (data && typeof data === 'object' && 'name' in data) {
console.log((data as { name: string }).name); // OK с type assertion
}

Практический пример — безопасная обработка API-ответа:

// Небезопасно с any
function processResponse(response: any) {
// TypeScript не поможет, если структура изменится
return response.data.user.name.toUpperCase();
// Если структура другая — ошибка только в runtime
}

// Безопасно с unknown
function processResponse(response: unknown): string {
// Нужно явно проверить структуру
if (
response &&
typeof response === 'object' &&
'data' in response &&
response.data &&
typeof response.data === 'object' &&
'user' in response.data &&
response.data.user &&
typeof response.data.user === 'object' &&
'name' in response.data.user &&
typeof response.data.user.name === 'string'
) {
return response.data.user.name.toUpperCase();
}

throw new Error('Invalid response structure');
}

Type guards для unknown:

// Создание type guard
function isUser(data: unknown): data is { name: string; age: number } {
return (
typeof data === 'object' &&
data !== null &&
'name' in data &&
typeof (data as Record<string, unknown>).name === 'string' &&
'age' in data &&
typeof (data as Record<string, unknown>).age === 'number'
);
}

function handleData(data: unknown) {
if (isUser(data)) {
// TypeScript знает: data is { name: string; age: number }
console.log(data.name, data.age);
} else {
console.log('Not a user');
}
}

Сравнение:

Характеристикаanyunknown
Присвоение в другие типыДаНет
Доступ к свойствамДаНет
Вызов как функцииДаНет
Арифметические операцииДаНет
БезопасностьНетДа
Присвоение из других типовДаДа

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

// unknown — когда тип действительно неизвестен
async function fetchData(url: string): Promise<unknown> {
const response = await fetch(url);
return response.json();
}

// any — для постепенной миграции с JavaScript
// или когда нужно быстро обойти систему типов
// Используйте как можно реже!

// Ещё один вариант — дженерики
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json() as T;
}

// Вызывающий код указывает тип
const user = await fetchData<User>('/api/user/1');

Итог: Всегда предпочитайте unknown вместо any. unknown заставляет явно проверять тип перед использованием, что предотвращает ошибки в runtime. any отключает проверку типов полностью и должен использоваться только как крайняя мера.

Вопрос 39. Объясните назначение типа never.

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

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

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

never — это тип, представляющий значения, которые никогда не возникают. Это «нижний тип» (bottom type) — подтип всех типов, но не имеет значений.

1. Функция, которая никогда не возвращает управление:

// Всегда выбрасывает ошибку
function throwError(message: string): never {
throw new Error(message);
}

// Бесконечный цикл
function infiniteLoop(): never {
while (true) {
// никогда не завершится
}
}

// Бесконечная рекурсия
function infiniteRecursion(): never {
return infiniteRecursion();
}

2. Проверка исчерпания (Exhaustiveness Checking):

Главное практическое применение — гарантия, что все варианты union типа обработаны:

type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };

function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// Если забыли обработать вариант — ошибка компиляции
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}

// Если добавим новый вариант в Shape:
// | { kind: "rectangle"; width: number; height: number }
// TypeScript покажет ошибку в default: shape не присваивается в never

3. never в union типах:

never игнорируется в union:

type A = string | never; // Упрощается до string
type B = number | never; // Упрощается до number

4. never в intersection типах:

never делает intersection невозможным:

type A = string & never; // never — невозможно быть одновременно string и never

5. Утилиты на основе never:

// Exclude — исключает типы из union
type Exclude<T, U> = T extends U ? never : T;
type Result = Exclude<"a" | "b" | "c", "a">; // "b" | "c"

// NonNullable — исключает null и undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type Result2 = NonNullable<string | null | undefined>; // string

// Extract — извлекает типы из union
type Extract<T, U> = T extends U ? T : never;
type Result3 = Extract<"a" | "b" | "c" | 1 | 2, string>; // "a" | "b" | "c"

6. Практический пример — обработка событий:

type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "scroll"; position: number };

function handleEvent(event: Event): void {
switch (event.type) {
case "click":
console.log(`Click at (${event.x}, ${event.y})`);
break;
case "keypress":
console.log(`Key pressed: ${event.key}`);
break;
case "scroll":
console.log(`Scrolled to ${event.position}`);
break;
default:
// Если добавим новый тип события — ошибка компиляции
const _exhaustive: never = event;
throw new Error(`Unknown event: ${_exhaustive}`);
}
}

7. never в массивах:

const arr: never[] = []; // Массив, в который нельзя добавить ничего
// arr.push(1); // Error
// arr.push("hello"); // Error

Итог: never — мощный инструмент для:

  • Обозначения функций, которые никогда не возвращают управление
  • Проверки исчерпания — гарантии обработки всех вариантов union типа
  • Построения утилит типов (Exclude, NonNullable, Extract)

Использование never в default-ветке switch — лучшая практика для надёжного кода.

Вопрос 40. Что такое утверждение типа (type assertion) в TypeScript?

Таймкод: 00:34:19

Ответ собеседования: Правильный. Утверждение типа (as) — принудительное присвоение переменной определённого типа. Позволяет работать с переменной более безопасно.

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

Type assertion — это способ сказать компилятору: «Я знаю тип лучше тебя, поверь мне». Это не приведение типа в runtime — только подсказка компилятору.

Синтаксис:

// Два эквивалентных синтаксиса
let value: unknown = "hello";

// Синтаксис «angle brackets» (не используется в JSX/TSX)
let str1 = <string>value;

// Синтаксис «as» (предпочтительный)
let str2 = value as string;

// Оба дают одинаковый результат
console.log(str2.toUpperCase()); // TypeScript знает, что это string

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

1. Работа с unknown или более широким типом:

function processResponse(response: unknown): string {
// Мы знаем структуру, но TypeScript — нет
const data = response as { message: string };
return data.message;
}

2. DOM-элементы:

// TypeScript возвращает HTMLElement | null
const input = document.getElementById("myInput");

// Мы знаем, что это HTMLInputElement
const value = (input as HTMLInputElement).value;

// Или с проверкой null
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d");

3. Работа с API, где TypeScript не знает тип:

interface User {
id: number;
name: string;
email: string;
}

async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data as User; // Мы уверены в структуре
}

4. Double assertion (двойное утверждение):

Когда нужно привести к типу, который несовместим напрямую:

let value: unknown = "hello";

// Напрямую нельзя: string не присваивается в number
// let num = value as number; // Error

// Через промежуточный any или unknown
let num = value as unknown as number;
// Или
let num2 = value as any as number;

// ⚠️ Опасно! В runtime value всё ещё строка

Type assertion vs Type guard:

// Type assertion — вы говорите TypeScript, какой тип
function process(data: unknown) {
// Просто утверждаем — без проверки
const user = data as { name: string };
console.log(user.name); // Может упасть в runtime!
}

// Type guard — проверяете тип перед использованием (безопаснее)
function isUser(data: unknown): data is { name: string } {
return (
typeof data === 'object' &&
data !== null &&
'name' in data &&
typeof (data as Record<string, unknown>).name === 'string'
);
}

function processSafe(data: unknown) {
if (isUser(data)) {
console.log(data.name); // Безопасно — TypeScript знает тип
} else {
console.log('Not a user');
}
}

Const assertion (as const):

Запрещает расширение типа и делает все свойства readonly:

// Без as const
const colors = {
red: "#ff0000",
blue: "#0000ff"
};
// colors.red = "#00ff00"; // OK — свойство изменяемо
// Тип: { red: string; blue: string }

// С as const
const colors2 = {
red: "#ff0000",
blue: "#0000ff"
} as const;
// colors2.red = "#00ff00"; // Error — readonly
// Тип: { readonly red: "#ff0000"; readonly blue: "#0000ff" }

// Полезно для литеральных типов
const config = {
host: "localhost",
port: 3000,
debug: true
} as const;

type Config = typeof config;
// { readonly host: "localhost"; readonly port: 3000; readonly debug: true }

Предупреждение:

Type assertion не выполняет никаких проверок в runtime:

const data: unknown = "hello";
const num = data as number;

// TypeScript думает, что это number
console.log(num.toFixed(2)); // Runtime error: toFixed is not a number

// Type assertion — это не приведение типа в runtime!
// Это только подсказка компилятору

Итог: Type assertion (as) говорит компилятору доверить вам знание типа. Не выполняет преобразований в runtime. Используйте с осторожностью — предпочитайте type guards для безопасной работы с unknown. as const — полезный паттерн для литеральных типов и readonly объектов.

Вопрос 41. Какие существуют кейсы, когда утверждение типа (type assertion) является приемлемой практикой?

Таймкод: 00:35:27

Ответ собеседования: Правильный. Приемлемо при работе с NPM-библиотеками без TypeScript, когда разработчик точно знает тип. Также после type guard для присвоения конкретного типа.

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

Type assertion — легитимный инструмент, но должен использоваться осознанно. Вот случаи, когда это оправдано.

1. Библиотеки без типизации:

// Библиотека без типов
// @types/legacy-lib не существует
import legacyLib from 'legacy-lib';

// Вы знаете структуру API
const result = legacyLib.getData() as {
id: number;
name: string;
items: Array<{ value: number }>;
};

// Лучше создать свои типы
interface LegacyResult {
id: number;
name: string;
items: Array<{ value: number }>;
}

const result2 = legacyLib.getData() as LegacyResult;

2. После type guard:

function isUser(data: unknown): data is User {
return /* ... */;
}

function process(data: unknown) {
if (!isUser(data)) {
throw new Error('Invalid data');
}

// Type guard уже сузил тип, но иногда нужно явное утверждение
const user = data as User;
console.log(user.name);
}

3. DOM-элементы:

// TypeScript не знает точный тип элемента
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;

// Input элемент
const input = document.querySelector('#email') as HTMLInputElement;
const email = input.value;

4. JSON.parse и подобные функции:

interface Config {
apiUrl: string;
timeout: number;
}

// JSON.parse возвращает any
const config: Config = JSON.parse(localStorage.getItem('config')!) as Config;

// Безопаснее с валидацией
function parseConfig(json: string): Config {
const parsed = JSON.parse(json);
// Здесь можно добавить валидацию
return parsed as Config;
}

5. Переопределение слишком строгих типов:

// Тип из библиотеки слишком узкий
declare function getItems(): ReadonlyArray<string>;

// Внутри вашего кода вы знаете, что массив не будет изменяться
const items = getItems() as string[];
items.push('new item'); // Вы уверены, что это безопасно

6. Тестирование и моки:

// В тестах удобно создавать частичные моки
const mockUser = {
id: 1,
name: 'Test User'
} as User; // User имеет больше полей, но для теста достаточно этих

// Или с частичным типом
const partialUser = {
id: 1
} as unknown as User; // Для теста допустимо

7. API-ответы с известной структурой:

interface ApiResponse {
data: {
users: Array<{ id: number; name: string }>;
total: number;
};
status: 'success' | 'error';
}

async function fetchUsers(): Promise<ApiResponse> {
const response = await fetch('/api/users');
return await response.json() as ApiResponse;
}

Когда type assertion НЕ допустим:

// НЕПРАВИЛЬНО: утверждение без понимания типа
function process(data: unknown) {
const result = data as string; // А вдруг data — число?
result.toUpperCase(); // Runtime error!
}

// ПРАВИЛЬНО: проверка перед использованием
function process(data: unknown) {
if (typeof data !== 'string') {
throw new Error('Expected string');
}
data.toUpperCase(); // Безопасно
}

Безопасные альтернативы type assertion:

// 1. Type guards
function isString(value: unknown): value is string {
return typeof value === 'string';
}

// 2. Валидация с Zod
import { z } from 'zod';

const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

function parseUser(data: unknown): User {
return UserSchema.parse(data); // Безопасная валидация
}

// 3. satisfies оператор (TS 4.9+)
const config = {
host: 'localhost',
port: 3000,
} satisfies Config; // Проверяет соответствие, но сохраняет литеральный тип

Итог: Type assertion допустим при работе с нетипизированными библиотеками, DOM-элементами, после type guard, в тестах и когда вы точно знаете тип. Избегайте утверждений без проверки — используйте type guards и валидацию (Zod, io-ts) для безопасной работы с неизвестными данными.

Вопрос 42. Расскажите про утилитарные типы TypeScript, их особенности и для чего они нужны.

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

Ответ собеседования: Правильный. Утилитарные типы позволяют писать меньше рутинного кода, избегая дублирования. Сравнимы с переиспользуемыми функциями-утилитами для типов. Основные: Record, Pick, Required, Partial, Omit.

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

Утилитарные типы (utility types) — встроенные обобщённые типы для трансформирования других типов. Они решают типичные задачи без дублирования кода.

Основные утилитарные типы:

1. Partial<T> — все свойства опциональны:

interface User {
id: number;
name: string;
email: string;
}

// Все свойства становятся опциональными
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string }

// Применение: обновление сущности
function updateUser(id: number, updates: Partial<User>) {
// Можно передать только изменённые поля
}

updateUser(1, { name: "John" }); // OK
updateUser(1, { email: "john@example.com" }); // OK

2. Required<T> — все свойства обязательны:

interface Config {
host?: string;
port?: number;
debug?: boolean;
}

// Все свойства становятся обязательными
type RequiredConfig = Required<Config>;
// { host: string; port: number; debug: boolean }

function init(config: RequiredConfig) {
// Все поля гарантированно присутствуют
}

3. Readonly<T> — все свойства readonly:

interface User {
id: number;
name: string;
}

type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string }

const user: ReadonlyUser = { id: 1, name: "John" };
// user.name = "Jane"; // Error: Cannot assign to 'name'

4. Pick<T, K> — выбрать указанные свойства:

interface User {
id: number;
name: string;
email: string;
password: string;
}

// Выбираем только нужные поля
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }

// Применение: публичный профиль без password
function getPublicProfile(user: User): PublicUser {
return {
id: user.id,
name: user.name,
email: user.email,
};
}

5. Omit<T, K> — исключить указанные свойства:

interface User {
id: number;
name: string;
email: string;
password: string;
}

// Исключаем ненужные поля
type CreateUserDto = Omit<User, "id">;
// { name: string; email: string; password: string }

type PublicUser = Omit<User, "password">;
// { id: number; name: string; email: string }

function createUser(dto: CreateUserDto): User {
return {
id: generateId(),
...dto,
};
}

6. Record<K, T> — объект с ключами K и значениями T:

// Объект с ключами-строками и значениями-числами
type Scores = Record<string, number>;
// { [key: string]: number }

const scores: Scores = {
math: 95,
english: 87,
history: 92,
};

// С конкретными ключами
type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;

const permissions: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};

7. Exclude<T, U> — исключить типы из union:

type AllColors = "red" | "green" | "blue" | "yellow";
type PrimaryColors = "red" | "blue";

type NonPrimaryColors = Exclude<AllColors, PrimaryColors>;
// "green" | "yellow"

// Exclude для типов
type AllTypes = string | number | boolean | null;
type NonNullableTypes = Exclude<AllTypes, null | undefined>;
// string | number | boolean

8. Extract<T, U> — извлечь типы из union:

type AllTypes = string | number | boolean | null;
type StringOrNumber = Extract<AllTypes, string | number>;
// string | number

9. NonNullable<T> — исключить null и undefined:

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string

10. ReturnType<T> — тип возвращаемого значения функции:

function getUser() {
return { id: 1, name: "John", email: "john@example.com" };
}

type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string }

// Практическое применение
async function fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}

type FetchUserResult = ReturnType<typeof fetchUser>;
// Promise<any> — можно уточнить

11. Parameters<T> — типы параметров функции:

function createUser(name: string, age: number, role: string) {
// ...
}

type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number, role: string]

// Применение: обёртка с теми же параметрами
async function createUserWithValidation(...args: CreateUserParams) {
// Валидация args
return createUser(...args);
}

12. Awaited<T> — тип из Promise:

type PromiseType = Promise<string>;
type Resolved = Awaited<PromiseType>;
// string

// Вложенные промисы
type Nested = Promise<Promise<number>>;
type ResolvedNested = Awaited<Nested>;
// number

Комбинирование утилитарных типов:

interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}

// DTO для создания: без id и дат, все поля обязательны
type CreateUserDto = Required<Omit<User, "id" | "createdAt" | "updatedAt">>;

// DTO для обновления: без id и дат, все поля опциональны
type UpdateUserDto = Partial<Omit<User, "id" | "createdAt" | "updatedAt">>;

// Публичный профиль: только id, name, email
type PublicProfile = Readonly<Pick<User, "id" | "name" | "email">>;

Кастомные утилитарные типы:

// DeepPartial — рекурсивный Partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
}

type PartialConfig = DeepPartial<Config>;
// Все свойства на всех уровнях опциональны

// Nullable — все свойства могут быть null
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};

// Values — union значений объекта
type Values<T> = T[keyof T];
type UserValues = Values<User>; // number | string | Date

Итог: Утилитарные типы — мощный инструмент для трансформирования типов. Основные: Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, ReturnType, Parameters, Awaited. Комбинируя их, можно создавать точные типы без дублирования кода.

Вопрос 43. Что такое declaration merging (слияние объявлений) в TypeScript? Плюсы и минусы.

Таймкод: 00:37:45

Ответ собеседования: Правильный. Declaration merging — механизм, при котором два интерфейса с одинаковыми именами сливаются в один. Минус — непредсказуемое поведение. Плюс — возможность дополнять интерфейсы в разных местах.

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

Declaration merging — механизм TypeScript, при котором несколько объявлений с одинаковым именем объединяются в одно.

Как работает:

// Объявление 1
interface User {
name: string;
age: number;
}

// Объявление 2 — сливается с первым
interface User {
email: string;
role: string;
}

// Результат: объединение всех свойств
const user: User = {
name: "John",
age: 30,
email: "john@example.com",
role: "admin",
};
// Все свойства обязательны!

Правила слияния:

1. Интерфейсы — сливаются:

interface Config {
host: string;
}

interface Config {
port: number;
}

// Config: { host: string; port: number }

2. Типы (type) — ошибка дублирования:

type Config = { host: string };
type Config = { port: number }; // Error: Duplicate identifier 'Config'

3. Конфликт свойств с одинаковым именем:

interface User {
name: string;
}

interface User {
name: string; // OK — одинаковый тип
// name: number; // Error — разные типы
}

4. Слияние функций (overloads):

function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
return String(value);
}

// TypeScript видит три перегрузки
format("hello"); // OK
format(42); // OK

5. Слияние пространств имён:

namespace App {
export interface Config {
apiUrl: string;
}
}

namespace App {
export interface Config {
timeout: number;
}
}

// App.Config: { apiUrl: string; timeout: number }

Плюсы declaration merging:

1. Расширение типов сторонних библиотек:

// Расширение глобального интерфейса Window
declare global {
interface Window {
myApp: {
version: string;
init: () => void;
};
}
}

// Теперь window.myApp типизирован
window.myApp.init();

2. Паттерн Module Augmentation:

// В библиотеке express
declare namespace Express {
interface Request {
user?: { id: number; name: string };
}
}

// В вашем коде — расширяем
declare namespace Express {
interface Request {
requestId: string;
}
}

// Express.Request теперь содержит user и requestId

3. Разделение типов по файлам:

// types/user.ts
interface User {
id: number;
name: string;
}

// types/user-permissions.ts
interface User {
permissions: string[];
}

// Везде доступен User со всеми свойствами

Минусы declaration merging:

1. Непредсказуемость:

// Файл A
interface Config {
apiUrl: string;
}

// Файл B — другой разработчик, не зная о файле A
interface Config {
apiUrl: string; // Сливается — может быть неожиданностью
timeout: number;
}

// Если забыли apiUrl — ошибка везде, где используется Config

2. Сложность отладки:

// Где определено это свойство?
interface User {
name: string;
}

// ... 500 строк и 10 файлов спустя ...
interface User {
// Откуда взялось это поле?
lastLogin: Date;
}

3. Конфликты имён:

// Библиотека A
interface Response {
data: unknown;
}

// Библиотека B
interface Response {
body: unknown;
}

// Конфликт — оба поля обязательны

4. Невозможность использовать с type:

// Хотите слияние? Используйте interface
type User = { name: string };
type User = { age: number }; // Error!

// Но type даёт большую гибкость
type User = { name: string } & { age: number }; // OK

Рекомендации:

// Используйте declaration merging для:
// 1. Расширения глобальных типов (Window, Express)
// 2. Module augmentation библиотек
// 3. Чётко документированных расширений в пределах проекта

// Избегайте:
// 1. Слияния в больших командах без согласования
// 2. Слияния типов из разных доменов
// 3. Слияния без документации

// Альтернатива — явное расширение через extends
interface BaseUser {
name: string;
}

interface UserWithPermissions extends BaseUser {
permissions: string[];
}

// Или через intersection
type UserWithPermissions = BaseUser & { permissions: string[] };

Итог: Declaration merging — мощный механизм для расширения типов, но требует дисциплины. Используйте для module augmentation и расширения глобальных типов. В остальных случаях предпочитайте явное наследование через extends или &.

Вопрос 44. Что такое mapped types (сопоставленные типы) в TypeScript и каков их механизм работы?

Таймкод: 00:39:31

Ответ собеседования: Правильный. Mapped types используются для перебора ключей типа или объекта с целью создания нового типа. Работают через синтаксис [key in keyof ...], позволяя получить значение типа по каждому ключу и вывести новый тип.

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

Mapped types — это механизм создания новых типов путём итерации по ключам существующего типа и трансформации каждого свойства.

Базовый синтаксис:

type MappedType<T> = {
[Key in keyof T]: T[Key];
};

interface User {
name: string;
age: number;
email: string;
}

// Итерация по каждому ключу User
type UserCopy = MappedType<User>;
// { name: string; age: number; email: string }

Модификаторы свойств:

// Сделать все свойства опциональными
type Optional<T> = {
[Key in keyof T]?: T[Key];
};

// Сделать все свойства readonly
type Readonly<T> = {
readonly [Key in keyof T]: T[Key];
};

// Сделать все свойства обязательными (убрать ?)
type Required<T> = {
[Key in keyof T]-?: T[Key];
// Минус перед ? убирает опциональность
};

// Сделать все свойства изменяемыми (убрать readonly)
type Mutable<T> = {
-readonly [Key in keyof T]: T[Key];
// Минус перед readonly убирает его
};

Трансформация типов свойств:

interface User {
name: string;
age: number;
email: string;
}

// Все свойства становятся string
type Stringified<T> = {
[Key in keyof T]: string;
};

type StringifiedUser = Stringified<User>;
// { name: string; age: string; email: string }

// Все свойства становятся Promise
type Promisified<T> = {
[Key in keyof T]: Promise<T[Key]>;
};

type AsyncUser = Promisified<User>;
// { name: Promise<string>; age: Promise<number>; email: Promise<string> }

// Практическое применение: асинхронная загрузка
async function loadUser(id: number): Promise<AsyncUser> {
const user = await fetchUser(id);
return {
name: Promise.resolve(user.name),
age: Promise.resolve(user.age),
email: Promise.resolve(user.email),
};
}

Переименование ключей (key remapping):

// Добавить префикс ко всем ключам
type Getters<T> = {
[Key in keyof T as `get${Capitalize<string & Key>}`]: () => T[Key];
};

type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; getEmail: () => string }

// Фильтрация ключей
type StringKeysOnly<T> = {
[Key in keyof T as T[Key] extends string ? Key : never]: T[Key];
};

type UserStringKeys = StringKeysOnly<User>;
// { name: string; email: string }
// age исключён, потому что number

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

1. Дженерик-обёртка для API-ответа:

type ApiResponse<T> = {
[Key in keyof T as `data${Capitalize<string & Key>}`]: {
value: T[Key];
loading: boolean;
error: string | null;
};
};

type UserState = ApiResponse<User>;
// {
// dataName: { value: string; loading: boolean; error: string | null };
// dataAge: { value: number; loading: boolean; error: string | null };
// dataEmail: { value: string; loading: boolean; error: string | null };
// }

2. Event handlers:

type EventHandlers<T> = {
[Key in keyof T as `on${Capitalize<string & Key>}Change`]: (
value: T[Key]
) => void;
};

type UserHandlers = EventHandlers<User>;
// {
// onNameChange: (value: string) => void;
// onAgeChange: (value: number) => void;
// onEmailChange: (value: string) => void;
// }

3. DeepPartial — рекурсивный Partial:

type DeepPartial<T> = {
[Key in keyof T]?: T[Key] extends object
? DeepPartial<T[Key]>
: T[Key];
};

interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
}

type PartialConfig = DeepPartial<Config>;
// Все свойства на всех уровнях опциональны

4. Реализация встроенных утилит:

// Partial
type MyPartial<T> = {
[Key in keyof T]?: T[Key];
};

// Pick
type MyPick<T, K extends keyof T> = {
[Key in K]: T[Key];
};

// Omit
type MyOmit<T, K extends keyof T> = {
[Key in keyof T as Key extends K ? never : Key]: T[Key];
};

// Record
type MyRecord<K extends keyof any, T> = {
[Key in K]: T;
};

5. Условные mapped types:

// Обернуть в Promise только свойства-функции
type PromisifyFunctions<T> = {
[Key in keyof T]: T[Key] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[Key];
};

interface UserService {
getUser(id: number): User;
isAdmin: boolean;
}

type AsyncUserService = PromisifyFunctions<UserService>;
// {
// getUser: (id: number) => Promise<User>;
// isAdmin: boolean;
// }

Итог: Mapped types — мощный инструмент для трансформирования типов. Ключевые возможности: итерация по ключам (Key in keyof T), модификаторы (?, readonly, -?, -readonly), переименование ключей (as), условные типы. На их основе построены все встроенные утилитарные типы TypeScript.

Вопрос 45. Как и какие состояния жизненного цикла можно отловить с помощью useEffect?

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

Ответ собеседования: Правильный. useEffect позволяет отловить: монтирование (пустой массив зависимостей), размонтирование (возврат cleanup-функции), обновление (указание зависимостей). Cleanup вызывается перед каждым новым эффектом.

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

useEffect — основный хук для работы с побочными эффектами и жизненным циклом компонента в React.

Синтаксис:

useEffect(() => {
// эффект

return () => {
// cleanup (очистка)
};
}, [зависимости]);

1. Монтирование (componentDidMount):

useEffect(() => {
console.log('Компонент смонтирован');

// Запрос данных, подписки, таймеры
const data = fetchData();

}, []); // Пустой массив — выполнится один раз при монтировании

2. Размонтирование (componentWillUnmount):

useEffect(() => {
const subscription = eventBus.subscribe('update', handler);
const timerId = setInterval(() => {}, 1000);

return () => {
// Cleanup — выполнится при размонтировании
subscription.unsubscribe();
clearInterval(timerId);
};
}, []);

3. Обновление (componentDidUpdate):

// При изменении конкретной зависимости
useEffect(() => {
console.log('userId изменился:', userId);
fetchUser(userId);
}, [userId]);

// При изменении любой из зависимостей
useEffect(() => {
console.log('filters или sort изменились');
applyFilters(filters, sort);
}, [filters, sort]);

4. Выполнение при каждом рендере:

useEffect(() => {
// Нет второго аргумента — выполняется при каждом рендере
console.log('Компонент отрендерился');
});
// ⚠️ Осторожно — может вызвать проблемы с производительностью

Порядок выполнения:

function Component({ userId }) {
console.log('1. Рендер');

useEffect(() => {
console.log('2. Эффект (после монтирования)');

return () => {
console.log('3. Cleanup (перед следующим эффектом или размонтированием)');
};
}, [userId]);

return <div>User: {userId}</div>;
}

// Первый рендер (userId = 1):
// 1. Рендер
// 2. Эффект (после монтирования)

// Обновление (userId = 2):
// 1. Рендер
// 3. Cleanup (userId = 1)
// 2. Эффект (userId = 2)

// Размонтирование:
// 3. Cleanup (userId = 2)

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

1. Запрос данных:

useEffect(() => {
let cancelled = false;

async function loadData() {
setLoading(true);
try {
const data = await fetchUser(userId);
if (!cancelled) {
setUser(data);
}
} catch (error) {
if (!cancelled) {
setError(error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}

loadData();

return () => {
cancelled = true; // Отменяем обновление при размонтировании
};
}, [userId]);

2. Подписка на WebSocket:

useEffect(() => {
const ws = new WebSocket('ws://localhost:8080');

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
setMessages(prev => [...prev, data]);
};

return () => {
ws.close();
};
}, []);

3. Document title:

useEffect(() => {
document.title = `User: ${user.name}`;

return () => {
document.title = 'My App';
};
}, [user.name]);

4. Таймер:

useEffect(() => {
const interval = setInterval(() => {
setTime(prev => prev + 1);
}, 1000);

return () => clearInterval(interval);
}, []);

5. Слушатель событий:

useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}

window.addEventListener('resize', handleResize);
handleResize(); // Начальное значение

return () => window.removeEventListener('resize', handleResize);
}, []);

Важные нюансы:

1. Cleanup вызывается перед каждым новым эффектом:

useEffect(() => {
console.log('Effect for', count);

return () => {
console.log('Cleanup for', count);
};
}, [count]);

// count = 1: Effect for 1
// count = 2: Cleanup for 1, Effect for 2
// count = 3: Cleanup for 2, Effect for 3
// Размонтирование: Cleanup for 3

2. Замыкание и актуальные значения:

function Timer() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
console.log(count); // Всегда 0! Замыкание на первом значении
setCount(count + 1); // Всегда setCount(1)
}, 1000);

return () => clearInterval(id);
}, []); // Пустые зависимости

return <div>{count}</div>;
}

// Решение 1: Функциональное обновление
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // Используем актуальное значение
}, 1000);
return () => clearInterval(id);
}, []);

// Решение 2: Добавить зависимость
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]); // Пересоздаётся при каждом изменении count

Итог: useEffect покрывает все этапы жизненного цикла: монтирование ([]), обновление ([deps]), размонтирование (cleanup). Cleanup вызывается перед каждым новым эффектом и при размонтировании. Важно учитывать замыкания и использовать функциональные обновления при необходимости.

Вопрос 46. Как можно отловить ошибку, возникшую в React-компоненте?

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

Ответ собеседования: Правильный. Ошибки в React-компонентах можно отловить с помощью Error Boundary и метода componentDidCatch.

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

React предоставляет несколько механизмов для отлова и обработки ошибок в компонентах.

1. Error Boundaries — componentDidCatch и getDerivedStateFromError:

Error Boundary — это компонент-класс, который перехватывает JavaScript-ошибки в дереве дочерних компонентов.

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error) {
// Обновляем state для показа fallback UI
return { hasError: true, error };
}

componentDidCatch(error, errorInfo) {
// Логируем ошибку
console.error('Error caught:', error);
console.error('Component stack:', errorInfo.componentStack);

// Отправляем в сервис мониторинга
logErrorToService(error, errorInfo);
}

render() {
if (this.state.hasError) {
// Fallback UI
return (
<div>
<h2>Что-то пошло не так</h2>
<details>
<summary>Подробности</summary>
<pre>{this.state.error?.message}</pre>
</details>
<button onClick={() => this.setState({ hasError: false })}>
Попробовать снова
</button>
</div>
);
}

return this.props.children;
}
}

// Использование
function App() {
return (
<ErrorBoundary>
<Header />
<main>
<ErrorBoundary>
<UserProfile />
</ErrorBoundary>
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
</main>
</ErrorBoundary>
);
}

2. react-error-boundary — библиотека для функциональных компонентов:

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<h2>Ошибка:</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Попробовать снова</button>
</div>
);
}

function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
logErrorToService(error, info);
}}
onReset={() => {
// Сброс состояния приложения
}}
>
<UserProfile />
</ErrorBoundary>
);
}

3. Отлов ошибок в обработчиках событий:

Error Boundary НЕ ловят ошибки в обработчиках событий. Для этого нужен try/catch:

function Component() {
const [error, setError] = useState(null);

const handleClick = async () => {
try {
await riskyOperation();
} catch (err) {
setError(err);
}
};

if (error) {
return <div>Ошибка: {error.message}</div>;
}

return <button onClick={handleClick}>Выполнить</button>;
}

4. Отлов асинхронных ошибок:

function Component() {
const [error, setError] = useState(null);

useEffect(() => {
let cancelled = false;

async function loadData() {
try {
const data = await fetchData();
if (!cancelled) {
setData(data);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
}
}

loadData();
return () => { cancelled = true; };
}, []);

if (error) {
return <div>Ошибка загрузки: {error.message}</div>;
}
}

5. Глобальный отлов ошибок:

// window.onerror — для синхронных ошибок
window.onerror = (message, source, line, col, error) => {
logErrorToService({ message, source, line, col, error });
return true; // Предотвращает вывод в консоль
};

// window.onunhandledrejection — для необработанных промисов
window.onunhandledrejection = (event) => {
logErrorToService({
type: 'unhandledrejection',
reason: event.reason,
});
};

6. Интеграция с сервисами мониторинга:

import * as Sentry from '@sentry/react';

// Sentry Error Boundary
const SentryErrorBoundary = Sentry.withErrorBoundary(
function ErrorFallback({ error }) {
return <div>Произошла ошибка: {error.message}</div>;
},
{
fallback: ErrorFallback,
onError: (error, componentStack) => {
Sentry.captureException(error, { extra: { componentStack } });
},
}
);

// Или ручная отправка
function logErrorToService(error, errorInfo) {
Sentry.captureException(error, {
contexts: {
react: { componentStack: errorInfo.componentStack },
},
});
}

Что Error Boundary НЕ ловит:

  • Ошибки в обработчиках событий (нужно try/catch)
  • Асинхронный код (setTimeout, requestAnimationFrame)
  • Ошибки на серверном рендеринге (SSR)
  • Ошибки в самом Error Boundary
// ❌ Error Boundary не поймает это
function Component() {
useEffect(() => {
setTimeout(() => {
throw new Error('Async error');
}, 1000);
}, []);

// ❌ Error Boundary не поймает это
const handleClick = () => {
throw new Error('Event error');
};

return <button onClick={handleClick}>Click</button>;
}

Использование нескольких Error Boundary:

function App() {
return (
<ErrorBoundary fallback={<AppCrash />}>
<Header />
<main>
{/* Каждая секция изолирована */}
<ErrorBoundary fallback={<FeedError />}>
<Feed />
</ErrorBoundary>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
</main>
</ErrorBoundary>
);
}
// Если Feed упадёт — только он покажет ошибку, Sidebar продолжит работать

Итог: Error Boundary (componentDidCatch + getDerivedStateFromError) — основной механизм для отлова ошибок в рендере. Для обработчиков событий и асинхронного кода — try/catch. Для глобального отлова — window.onerror и window.onunhandledrejection. Используйте react-error-boundary для функциональных компонентов.

Вопрос 47. Как бороться с повторными рендерами в React?

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

Ответ собеседования: Правильный. Для борьбы с повторными рендерами используется мемоизация: useMemo, useCallback и React.memo.

Правильный 回答:

Повторные рендеры — одна из основных проблем производительности в React. Существует несколько стратегий оптимизации.

1. React.memo — мемоизация компонента:

Предотвращает повторный рендер компонента, если его props не изменились.

// Без мемоизации — рендерится при каждом рендере родителя
function Child({ name, age }) {
console.log('Child rendered');
return <div>{name}: {age}</div>;
}

// С мемоизацией — рендерится только при изменении props
const MemoizedChild = React.memo(function Child({ name, age }) {
console.log('Child rendered');
return <div>{name}: {age}</div>;
});

// Кастомная функция сравнения
const MemoizedChild2 = React.memo(
function Child({ name, age }) {
return <div>{name}: {age}</div>;
},
(prevProps, nextProps) => {
// Вернуть true — пропустить рендер
// Вернуть false — перерендерить
return (
prevProps.name === nextProps.name &&
prevProps.age === nextProps.age
);
}
);

Когда React.memo НЕ помогает:

// Объекты и массивы — новые ссылки при каждом рендере
function Parent() {
const config = { theme: 'dark' }; // Новая ссылка каждый раз
return <MemoizedChild config={config} />;
// Child будет перерендериваться каждый раз!
}

// Функции — новые ссылки при каждом рендере
function Parent() {
const handleClick = () => {}; // Новая ссылка каждый раз
return <MemoizedChild onClick={handleClick} />;
// Child будет перерендериваться каждый раз!
}

// Дети — новые ссылки
function Parent() {
return <MemoizedChild>{<SomeComponent />}</MemoizedChild>;
}

2. useMemo — мемоизация вычислений:

Кеширует результат вычисления между рендерами.

function Component({ items, filter }) {
// Без мемоизации — вычисляется при каждом рендере
const filtered = items.filter(item => item.category === filter);

// С мемоизацией — вычисляется только при изменении items или filter
const filtered = useMemo(() => {
console.log('Filtering items...');
return items.filter(item => item.category === filter);
}, [items, filter]);

// Мемоизация объекта — для передачи в мемоизированные компоненты
const config = useMemo(() => ({
theme: 'dark',
locale: 'ru',
}), []); // Зависимости не меняются — объект создаётся один раз

return <MemoizedChild config={config} items={filtered} />;
}

3. useCallback — мемоизация функций:

Кеширует функцию между рендерами.

function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');

// Без мемоизации — новая функция при каждом рендере
const handleClick = () => {
console.log('clicked');
};

// С мемоизацией — та же функция, если зависимости не изменились
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // Нет зависимостей — функция создаётся один раз

// Функция с зависимостями
const handleSubmit = useCallback(() => {
submitForm({ name });
}, [name]); // Новая функция только при изменении name

return (
<>
<MemoizedChild onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
</>
);
}

4. Разделение состояния:

// Плохо: одно состояние на всё
function Form() {
const [state, setState] = useState({
name: '',
email: '',
isSubmitting: false,
});

// Изменение isSubmitting вызывает пересчёт name и email
const handleSubmit = () => {
setState(prev => ({ ...prev, isSubmitting: true }));
};
}

// Хорошо: разделённое состояние
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);

// Изменение isSubmitting не влияет на name и email
const handleSubmit = () => {
setIsSubmitting(true);
};
}

5. Вынос контента через children:

// Плохо: ExpensiveComponent рендерится при каждом изменении count
function Parent() {
const [count, setCount] = useState(0);

return (
<div>
<ExpensiveComponent />
<button onClick={() => setCount(count + 1)}>
{count}
</button>
</div>
);
}

// Хорошо: children не перерендерится при изменении count
function Parent({ children }) {
const [count, setCount] = useState(0);

return (
<div>
{children}
<button onClick={() => setCount(count + 1)}>
{count}
</button>
</div>
);
}

// Использование
function App() {
return (
<Parent>
<ExpensiveComponent /> {/* Не перерендерится */}
</Parent>
);
}

6. Context — селекторы и разделение:

// Плохо: все потребители перерендерятся при любом изменении контекста
const AppContext = createContext();

function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');

const value = { user, setUser, theme, setTheme };
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}

// Хорошо: разделение контекста на части
const UserContext = createContext();
const ThemeContext = createContext();

function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');

return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
);
}

// Компонент подписан только на user — не перерендерится при изменении theme
function UserProfile() {
const { user } = useContext(UserContext);
return <div>{user?.name}</div>;
}

7. Виртуализация списков:

import { FixedSizeList } from 'react-window';

// Без виртуализации — рендерятся все 10000 элементов
function List({ items }) {
return (
<div>
{items.map(item => (
<ListItem key={item.id} item={item} />
))}
</div>
);
}

// С виртуализацией — рендерятся только видимые элементы
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
<ListItem item={items[index]} />
</div>
);

return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}

Когда НЕ нужно оптимизировать:

// ❌ Избыточная оптимизация
const MemoizedText = React.memo(function Text({ value }) {
return <span>{value}</span>;
});

const memoizedValue = useMemo(() => value, [value]);
const memoizedCallback = useCallback(() => {}, []);

// ✅ Оптимизируйте только когда есть реальная проблема
// 1. Компонент рендерится часто
// 2. Рендер компонента дорогой
// 3. Props не меняются между рендерами

Итог: Используйте React.memo для компонентов, useMemo для вычислений, useCallback для функций. Разделяйте состояние, используйте children для изоляции рендеров, виртуализируйте длинные списки. Но не оптимизируйте преждевременно — сначала профилируйте и находите реальные узкие места.

Вопрос 48. В чём разница между использованием обычной переменной и useState внутри компонента?

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

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

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

Ключевое отличие: useState триггерит перерендер компонента, обычная переменная — нет.

1. useState — вызывает перерендер:

function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(count + 1); // Триггерит перерендер
// Компонент перерендерится с новым значением count
};

return <button onClick={handleClick}>Count: {count}</button>;
}
// При каждом клике — новый рендер, кнопка показывает актуальное значение

2. Обычная переменная — не вызывает перерендер:

function Counter() {
let count = 0;

const handleClick = () => {
count = count + 1; // Значение изменилось, но...
console.log(count); // 1, 2, 3... — значение реально меняется
// НО компонент НЕ перерендерится!
// UI продолжает показывать "Count: 0"
};

return <button onClick={handleClick}>Count: {count}</button>;
}
// Клики меняют переменную, но UI не обновляется

3. useRef — изменение без перерендера:

function Component() {
const renderCount = useRef(0);

useEffect(() => {
renderCount.current += 1; // Изменяем без перерендера
});

// renderCount.current обновляется, но не вызывает рендер
return <div>Рендеров: {renderCount.current}</div>;
// ⚠️ Покажет предыдущее значение — текущий рендер уже отработал
}

4. Сравнение в одном компоненте:

function Comparison() {
const [stateValue, setStateValue] = useState(0);
let regularValue = 0;
const refValue = useRef(0);

const handleClick = () => {
setStateValue(stateValue + 1); // Вызовет перерендер
regularValue += 1; // Изменится, но без перерендера
refValue.current += 1; // Изменится, но без перерендера

console.log('regularValue:', regularValue); // 1, 2, 3...
console.log('refValue:', refValue.current); // 1, 2, 3...
console.log('stateValue:', stateValue); // 0, 0, 0... (замыкание!)
};

return (
<div>
<p>State: {stateValue}</p> {/* Обновляется */}
<p>Regular: {regularValue}</p> {/* Всегда 0 */}
<p>Ref: {refValue.current}</p> {/* Всегда 0 */}
<button onClick={handleClick}>Increment</button>
</div>
);
}

5. Почему обычная переменная не работает:

// При каждом рендере функция вызывается заново
// Локальные переменные создаются заново
function Component() {
let count = 0; // ← Создаётся заново при каждом рендере!

const handleClick = () => {
count += 1;
// count изменится только в рамках этого вызова
// При следующем рендере count снова будет 0
};

return <button>{count}</button>; // Всегда 0
}

6. Когда использовать что:

function Component() {
// useState — когда значение должно отображаться в UI
const [name, setName] = useState('');

// useRef — когда нужно хранить значение между рендерами без перерендера
const previousName = useRef('');
const inputRef = useRef(null);

// Обычная переменная — для вычислений внутри рендера
const upperName = name.toUpperCase(); // Вычисляется при каждом рендере

// useRef для хранения предыдущего значения
useEffect(() => {
previousName.current = name;
}, [name]);

return (
<div>
<input
ref={inputRef}
value={name}
onChange={e => setName(e.target.value)}
/>
<p>Upper: {upperName}</p>
<p>Previous: {previousName.current}</p>
</div>
);
}

Сравнительная таблица:

ХарактеристикаuseStateuseRefОбычная переменная
Перерендер при измененииДаНетНет
Сохраняет значение между рендерамиДаДаНет (создаётся заново)
Доступно в UIДаДа (.current)Да
ИспользованиеUI-состояниеСсылка/значение без рендераВременные вычисления

Итог: Используйте useState для данных, которые должны отображаться в UI и обновляться при изменении. useRef — для хранения значений между рендерами без перерендера. Обычные переменные — для временных вычислений внутри одного рендера. Изменение обычной переменной не вызовет перерендер — компонент отрисуется с начальным значением.

Вопрос 49. Для чего нужен useContext?

Таймкод: 00:42:54

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

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

useContext — хук для получения значения из React Context без необходимости использовать компонент-потребитель.

Проблема — Prop Drilling:

// Без контекста — данные пробрасываются через множество компонентов
function App() {
const user = { name: 'John', role: 'admin' };

return <Page user={user} />;
}

function Page({ user }) {
return <Layout user={user} />;
}

function Layout({ user }) {
return <Sidebar user={user} />;
}

function Sidebar({ user }) {
return <UserMenu user={user} />;
}

function UserMenu({ user }) {
return <div>{user.name}</div>;
}
// Page, Layout, Sidebar — просто пробрасывают user дальше
// Они не используют user, но обязаны его принимать

Решение — Context + useContext:

// 1. Создаём контекст
const UserContext = createContext(null);

// 2. Провайдер оборачивает дерево компонентов
function App() {
const user = { name: 'John', role: 'admin' };

return (
<UserContext.Provider value={user}>
<Page />
</UserContext.Provider>
);
}

// 3. Промежуточные компоненты НЕ пробрасывают props
function Page() {
return <Layout />;
}

function Layout() {
return <Sidebar />;
}

function Sidebar() {
return <UserMenu />;
}

// 4. Компонент получает значение напрямую
function UserMenu() {
const user = useContext(UserContext);
return <div>{user.name}</div>;
}

Полный пример с несколькими контекстами:

// Контексты
const ThemeContext = createContext('light');
const AuthContext = createContext(null);

// Провайдеры
function App() {
const [theme, setTheme] = useState('light');
const [user, setUser] = useState(null);

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<AuthContext.Provider value={{ user, setUser }}>
<Page />
</AuthContext.Provider>
</ThemeContext.Provider>
);
}

// Использование в компоненте
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
const { user } = useContext(AuthContext);

return (
<header className={theme}>
<span>{user?.name}</span>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle theme
</button>
</header>
);
}

Создание кастомного хука:

// Для удобства и типобезопасности
function useUser() {
const context = useContext(UserContext);
if (context === null) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}

// Использование
function UserMenu() {
const user = useUser(); // Не нужно импортировать контекст
return <div>{user.name}</div>;
}

Когда использовать, а когда нет:

// ✅ Подходит для:
// 1. Глобальных данных (тема, авторизация, язык)
// 2. Данных, используемых во многих компонентах на разных уровнях
// 3. Конфигурации приложения

// ❌ Не подходит для:
// 1. Данных, используемых только в двух соседних компонентах
// 2. Часто меняющихся данных (каждый потребитель перерендерится)
// 3. Сложного состояния (лучше Redux, Zustand)

// Проблема производительности:
function App() {
const [count, setCount] = useState(0);

// Каждый потребитель контекста перерендерится при изменении count
const value = { count, setCount };

return (
<MyContext.Provider value={value}>
<ExpensiveComponent /> {/* Перерендерится при каждом изменении count */}
</MyContext.Provider>
);
}

Оптимизация контекста:

// Разделение на несколько контекстов
const CountContext = createContext();
const SetCountContext = createContext();

function OptimizedProvider({ children }) {
const [count, setCount] = useState(0);

// Мемоизация значений
const countValue = useMemo(() => ({ count }), [count]);
const setCountValue = useMemo(() => ({ setCount }), []);

return (
<CountContext.Provider value={countValue}>
<SetCountContext.Provider value={setCountValue}>
{children}
</SetCountContext.Provider>
</CountContext.Provider>
);
}

// Компонент, использующий только setCount, не перерендерится при изменении count
function IncrementButton() {
const { setCount } = useContext(SetCountContext);
return <button onClick={() => setCount(c => c + 1)}>+</button>;
}

Итог: useContext решает проблему prop drilling, позволяя передавать данные глубоко в дерево компонентов. Подходит для глобальных данных (тема, авторизация, локализация). Избегайте для часто меняющихся данных — каждый потребитель будет перерендериваться.

Вопрос 50. Почему не стоит мемоизировать каждый компонент?

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

Ответ собеседования: Правильный. Мемоизация — это тоже вычисления, потребляющие память. Мемоизация мелких операций может ухудшить производительность. Два основных кейса: передача функции в компонент с React.memo и передача функции в зависимости хуков.

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

Мемоизация — это не бесплатная оптимизация. У неё есть своя цена, и неправильное использование может навредить производительности.

1. Стоимость мемоизации:

// React.memo выполняет поверхностное сравнение props
const MemoizedComponent = React.memo(function Component({ name, age }) {
return <div>{name}: {age}</div>;
});

// Что происходит при каждом рендере:
// 1. Вызов shallowCompare(prevProps, nextProps)
// 2. Для каждого prop: prevProp === nextProp
// 3. Если все равны — пропустить рендер

// Поверхностное сравнение объекта:
// { name: 'John', age: 30 } !== { name: 'John', age: 30 } // true — разные ссылки!
// Сравнение двух объектов по ключам — это тоже работа

2. Когда мемоизация вредит:

// ❌ Мемоизация дешёвого компонента
const MemoizedText = React.memo(function Text({ value }) {
return <span>{value}</span>; // Простой JSX — рендер почти бесплатный
});

// Стоимость сравнения props > стоимости рендера
// React.memo тратит время на сравнение, а рендер был бы быстрее

// ❌ Мемоизация с часто меняющимися props
const MemoizedItem = React.memo(function Item({ item }) {
return <div>{item.name}</div>;
});

function List({ items }) {
return items.map(item => (
// При каждом рендере items — новый массив
// Каждый item — новая ссылка (если создаётся в render)
<MemoizedItem key={item.id} item={item} />
));
// Сравнение props для каждого элемента — напрасная работа
}

3. Стоимость useMemo и useCallback:

// useMemo — сохраняет значение + проверяет зависимости
const memoized = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);

// Что происходит:
// 1. Проверка: a === prevA && b === prevB
// 2. Если равны — вернуть закешированное значение
// 3. Если нет — пересчитать

// Для простых вычислений — оверхед:
const doubled = useMemo(() => count * 2, [count]);
// Сравнение count + возврат значения > просто count * 2

// useCallback — сохраняет ссылку на функцию
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);

// Что происходит:
// 1. Проверка зависимостей
// 2. Если изменились — создать НОВУЮ функцию
// 3. Если нет — вернуть старую ссылку

4. Правильные кейсы для мемоизации:

// ✅ Дорогие вычисления
const sortedItems = useMemo(() => {
console.log('Sorting...');
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

// ✅ Передача в мемоизированный компонент
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);

return <MemoizedChild onClick={handleClick} />;

// ✅ Зависимость в других хуках
useEffect(() => {
const subscription = subscribe(filter);
return () => subscription.unsubscribe();
}, [filter]); // filter должен быть мемоизирован, если это объект

const filter = useMemo(() => ({
category: 'electronics',
price: { min: 0, max: 1000 },
}), []);

// ✅ Рендер большого список
const MemoizedRow = React.memo(function Row({ item }) {
return (
<tr>
<td>{item.name}</td>
<td>{item.price}</td>
{/* Много вложенных компонентов */}
</tr>
);
});

5. Когда НЕ нужно мемоизировать:

// ❌ Простые компоненты
const MemoizedSpan = React.memo(({ text }) => <span>{text}</span>);
// Рендер <span> дешевле, чем сравнение props

// ❌ Простые вычисления
const doubled = useMemo(() => count * 2, [count]);
// count * 2 — тривиальная операция

// ❌ Компоненты, которые всегда получают новые props
const MemoizedChild = React.memo(({ data }) => <div>{data}</div>);

function Parent() {
// data — всегда новый объект
return <MemoizedChild data={{ value: count }} />;
// React.memo бесполезен — props всегда новые
}

// ❌ Чрезмерная мемоизация зависимостей
const a = useMemo(() => computeA(), []);
const b = useMemo(() => computeB(a), [a]);
const c = useMemo(() => computeC(b), [b]);
// Если computeA, computeB, computeC — простые, оверхед > выгоды

6. Правило большого пальца:

Мемоируйте, когда:
1. Рендер компонента дорогой (> 1ms)
2. Props не меняются между рендерами
3. Передаёте в зависимости хуков (useEffect, useMemo)
4. Компонент рендерится часто с теми же props

НЕ мемоизируйте, когда:
1. Рендер дешёвый
2. Props меняются каждый рендер
3. Вычисления тривиальные
4. Нет измеримого улучшения

Итог: Мемоизация — это не бесплатная оптимизация. React.memo выполняет сравнение props, useMemo/useCallback — проверяют зависимости. Для дешёвых операций эта проверка может стоить дороже, чем сам рендер. Оптимизируйте только при наличии реальной проблемы с производительностью, измеренной профайлером.

Вопрос 50. Почему не стоит мемоизировать каждый компонент?

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

Ответ собеседования: Правильный. Мемоизация — это тоже вычисления, потребляющие память. Мемоизация мелких операций может ухудшить производительность. Два основных кейса: передача функции в компонент с React.memo и передача функции в зависимости хуков.

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

Мемоизация — это не бесплатная оптимизация. У неё есть своя цена, и неправильное использование может навредить производительности.

1. Стоимость мемоизации:

// React.memo выполняет поверхностное сравнение props
const MemoizedComponent = React.memo(function Component({ name, age }) {
return <div>{name}: {age}</div>;
});

// Что происходит при каждом рендере:
// 1. Вызов shallowCompare(prevProps, nextProps)
// 2. Для каждого prop: prevProp === nextProp
// 3. Если все равны — пропустить рендер

// Поверхностное сравнение объекта:
// { name: 'John', age: 30 } !== { name: 'John', age: 30 } // true — разные ссылки!
// Сравнение двух объектов по ключам — это тоже работа

2. Когда мемоизация вредит:

// ❌ Мемоизация дешёвого компонента
const MemoizedText = React.memo(function Text({ value }) {
return <span>{value}</span>; // Простой JSX — рендер почти бесплатный
});

// Стоимость сравнения props > стоимости рендера
// React.memo тратит время на сравнение, а рендер был бы быстрее

// ❌ Мемоизация с часто меняющимися props
const MemoizedItem = React.memo(function Item({ item }) {
return <div>{item.name}</div>;
});

function List({ items }) {
return items.map(item => (
// При каждом рендере items — новый массив
// Каждый item — новая ссылка (если создаётся в render)
<MemoizedItem key={item.id} item={item} />
));
// Сравнение props для каждого элемента — напрасная работа
}

3. Стоимость useMemo и useCallback:

// useMemo — сохраняет значение + проверяет зависимости
const memoized = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);

// Что происходит:
// 1. Проверка: a === prevA && b === prevB
// 2. Если равны — вернуть закешированное значение
// 3. Если нет — пересчитать

// Для простых вычислений — оверхед:
const doubled = useMemo(() => count * 2, [count]);
// Сравнение count + возврат значения > просто count * 2

// useCallback — сохраняет ссылку на функцию
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);

// Что происходит:
// 1. Проверка зависимостей
// 2. Если изменились — создать НОВУЮ функцию
// 3. Если нет — вернуть старую ссылку

4. Правильные кейсы для мемоизации:

// ✅ Дорогие вычисления
const sortedItems = useMemo(() => {
console.log('Sorting...');
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

// ✅ Передача в мемоизированный компонент
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);

return <MemoizedChild onClick={handleClick} />;

// ✅ Зависимость в других хуках
useEffect(() => {
const subscription = subscribe(filter);
return () => subscription.unsubscribe();
}, [filter]); // filter должен быть мемоизирован, если это объект

const filter = useMemo(() => ({
category: 'electronics',
price: { min: 0, max: 1000 },
}), []);

// ✅ Рендер большого списка
const MemoizedRow = React.memo(function Row({ item }) {
return (
<tr>
<td>{item.name}</td>
<td>{item.price}</td>
{/* Много вложенных компонентов */}
</tr>
);
});

5. Когда НЕ нужно мемоизировать:

// ❌ Простые компоненты
const MemoizedSpan = React.memo(({ text }) => <span>{text}</span>);
// Рендер <span> дешевле, чем сравнение props

// ❌ Простые вычисления
const doubled = useMemo(() => count * 2, [count]);
// count * 2 — тривиальная операция

// ❌ Компоненты, которые всегда получают новые props
const MemoizedChild = React.memo(({ data }) => <div>{data}</div>);

function Parent() {
// data — всегда новый объект
return <MemoizedChild data={{ value: count }} />;
// React.memo бесполезен — props всегда новые
}

// ❌ Чрезмерная мемоизация зависимостей
const a = useMemo(() => computeA(), []);
const b = useMemo(() => computeB(a), [a]);
const c = useMemo(() => computeC(b), [b]);
// Если computeA, computeB, computeC — простые, оверхед > выгоды

6. Правило большого пальца:

Мемоируйте, когда:
1. Рендер компонента дорогой (> 1ms)
2. Props не меняются между рендерами
3. Передаёте в зависимости хуков (useEffect, useMemo)
4. Компонент рендерится часто с теми же props

НЕ мемоизируйте, когда:
1. Рендер дешёвый
2. Props меняются каждый рендер
3. Вычисления тривиальные
4. Нет измеримого улучшения

Итог: Мемоизация — это не бесплатная оптимизация. React.memo выполняет сравнение props, useMemo/useCallback — проверяют зависимости. Для дешёвых операций эта проверка может стоить дороже, чем сам рендер. Оптимизируйте только при наличии реальной проблемы с производительностью, измеренной профайлером.

Вопрос 51. Что такое reconciliation в контексте React?

Таймкод: 00:44:22

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

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

Reconciliation — это алгоритм, по которому React определяет, как обновить DOM при изменении состояния или props.

1. Общий принцип работы:

1. Компонент рендерится → создаётся дерево Virtual DOM
2. Состояние/props изменились → создаётся НОВОЕ дерево Virtual DOM
3. React сравнивает два деревия (old vs new)
4. Вычисляется минимальный набор изменений (diff)
5. Только эти изменения применяются к реальному DOM

2. Эвристические правила сравнения:

// Правило 1: Разные типы элементов → полная пересборка
// Старое дерево
<div>
<Counter />
</div>

// Новое дерево — другой корневой элемент
<span>
<Counter />
</span>
// React уничтожает div (и всё внутри) и создаёт span заново
// Counter размонтируется и монтируется заново — состояние теряется!

// Правило 2: Одинаковые типы → обновление атрибутов
// Старое
<div className="header" title="old" />

// Новое
<div className="footer" title="new" />
// React обновит только className и title в существующем DOM-узле

3. Проблема со списками и ключи:

// ❌ Без ключей — React не понимает, что изменилось
function List({ items }) {
return (
<ul>
{items.map(item => (
<li>{item.name}</li> {/* Нет key! */}
))}
</ul>
);
}

// Было: [A, B, C] → Стало: [A, B, C, D]
// React видит: 3 элемента → 4 элемента
// Обновит A, B, C и добавит D
// Но если вставить X в начало: [X, A, B, C]
// React обновит A→X, B→A, C→B и добавит C
// Все элементы обновятся, хотя достаточно было добавить X в начало!

// ✅ С ключами — React понимает порядок
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

// Было: [{id:1,A}, {id:2,B}, {id:3,C}]
// Стало: [{id:0,X}, {id:1,A}, {id:2,B}, {id:3,C}]
// React видит: новый элемент id=0 в начале
// Добавляет <li>X</li> в начало, остальные не трогает

4. Почему нельзя использовать index как key:

// ❌ Индекс как key — проблемы при изменении порядка
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'First', completed: false },
{ id: 2, text: 'Second', completed: false },
{ id: 3, text: 'Third', completed: false },
]);

const addTodoAtStart = () => {
setTodos([
{ id: Date.now(), text: 'New', completed: false },
...todos,
]);
};

return (
<ul>
{todos.map((todo, index) => (
<TodoItem
key={index} {/* Проблема! */}
todo={todo}
/>
))}
</ul>
);
}

// Что произойдёт при addTodoAtStart:
// Было: key=0 (id:1), key=1 (id:2), key=2 (id:3)
// Стало: key=0 (new), key=1 (id:1), key=2 (id:2), key=3 (id:3)
// React: key=0 был id:1, теперь new → обновить
// key=1 был id:2, теперь id:1 → обновить
// key=2 был id:3, теперь id:2 → обновить
// key=3 новый → добавить
// Все TodoItem обновятся + новый добавится
// Состояние чекбоксов может перепутаться!

// ✅ Уникальный id как key
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
// React поймёт: новый элемент добавился в начало
// Остальные не изменились — просто сдвинулись

5. Пример reconciliation с компонентами:

function App() {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(c => c + 1)}>+</button>
<Display count={count} />
</div>
);
}

function Display({ count }) {
return (
<div>
<h1>Count: {count}</h1>
<ChildComponent />
</div>
);
}

function ChildComponent() {
const [text, setText] = useState('');
return <input value={text} onChange={e => setText(e.target.value)} />;
}

// При клике на кнопку:
// 1. App рендерится → count изменился
// 2. Display рендерится → count изменился в props
// 3. ChildComponent НЕ перерендерится — его props не изменились
// 4. Input не обновляется → текст сохраняется ✓

6. Оптимизация reconciliation:

// React.memo — пропускает рендер, если props не изменились
const ExpensiveChild = React.memo(function Child({ data }) {
// Дорогой рендер
return <div>{/* много элементов */}</div>;
});

// Родитель перерендерится, но Child — нет
function Parent() {
const [count, setCount] = useState(0);

// Мемоизируем объект, чтобы ссылка не менялась
const data = useMemo(() => ({ value: 'static' }), []);

return (
<div>
<button onClick={() => setCount(c => c + 1)}>+</button>
<ExpensiveChild data={data} />
</div>
);
}

Итог: Reconciliation — процесс сравнения старого и нового дерева Virtual DOM для вычисления минимальных изменений. React использует эвристики: разные типы → пересборка, одинаковые → обновление атрибутов. Ключи критически важны для списков — они помогают React понимать, какие элементы переместились, добавились или удалились.

Вопрос 52. Зачем нужен useCallback и в каких случаях его необходимо применять?

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

Ответ собеседования: Правильный. useCallback нужен для мемоизации функций, чтобы ссылка оставалась одинаковой. Два основных случая: передача в компонент с React.memo и передача в зависимости других хуков.

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

useCallback — хук для мемоизации функций, который возвращает ту же ссылку на функцию между рендерами, пока не изменятся зависимости.

1. Проблема — новые ссылки при каждом рендере:

function Parent() {
const [count, setCount] = useState(0);

// При каждом рендере создаётся НОВАЯ функция
const handleClick = () => {
console.log('clicked');
};

return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild onClick={handleClick} />
{/* handleClick — новая ссылка при каждом клике */}
{/* MemoizedChild перерендерится, хотя props не изменились */}
</div>
);
}

2. Решение — useCallback:

function Parent() {
const [count, setCount] = useState(0);

// Функция пересоздаётся только при изменении зависимостей
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // Пустой массив — функция создаётся один раз

return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild onClick={handleClick} />
{/* handleClick — та же ссылка → MemoizedChild не перерендерится */}
</div>
);
}

3. Кейс 1: Передача в мемоизированный компонент:

// Без useCallback — React.memo бесполезен
const Child = React.memo(({ onUpdate }) => {
console.log('Child render');
return <button onClick={onUpdate}>Update</button>;
});

function Parent() {
const [count, setCount] = useState(0);

// Новая функция при каждом рендере
const handleUpdate = () => {
setCount(c => c + 1);
};

return (
<div>
<button onClick={() => setCount(c => c + 10)}>+10</button>
<Child onUpdate={handleUpdate} />
{/* Child перерендерится при каждом +10 */}
</div>
);
}

// С useCallback — React.memo работает
function Parent() {
const [count, setCount] = useState(0);

const handleUpdate = useCallback(() => {
setCount(c => c + 1);
}, []); // Та же ссылка

return (
<div>
<button onClick={() => setCount(c => c + 10)}>+10</button>
<Child onUpdate={handleUpdate} />
{/* Child НЕ перерендерится при +10 */}
</div>
);
}

4. Кейс 2: Зависимость в useEffect:

// Без useCallback — бесконечный цикл
function Search({ query }) {
const [results, setResults] = useState([]);

const fetchResults = () => {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(setResults);
};

useEffect(() => {
fetchResults();
}, [fetchResults]); // fetchResults — новая ссылка каждый рендер!
// Эффект запускается снова → новый fetchResults → снова эффект...

return <div>{/* результаты */}</div>;
}

// С useCallback — работает корректно
function Search({ query }) {
const [results, setResults] = useState([]);

const fetchResults = useCallback(() => {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(setResults);
}, [query]); // Пересоздаётся только при изменении query

useEffect(() => {
fetchResults();
}, [fetchResults]); // Эффект запускается только при изменении query

return <div>{/* результаты */}</div>;
}

5. Кейс 3: Оптимизация обработчиков событий:

function TodoList({ todos }) {
const [filter, setFilter] = useState('all');

// Без useCallback — каждый TodoItem перерендерится при смене фильтра
return (
<div>
<FilterButtons onChange={setFilter} />
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={() => deleteTodo(todo.id)} // Новая функция!
onToggle={() => toggleTodo(todo.id)} // Новая функция!
/>
))}
</div>
);
}

// С useCallback — обработчики стабильны
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');

const createDeleteHandler = useCallback((id) => {
return () => deleteTodo(id);
}, []);

const createToggleHandler = useCallback((id) => {
return () => toggleTodo(id);
}, []);

return (
<div>
<FilterButtons onChange={setFilter} />
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={createDeleteHandler(todo.id)}
onToggle={createToggleHandler(todo.id)}
/>
))}
</div>
);
}

6. Когда useCallback НЕ нужен:

// ❌ Функция не передаётся никуда
function Component() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);

return <button onClick={handleClick}>Click</button>;
// useCallback бесполезен — кнопка не мемоизирована
}

// ❌ Функция используется только в текущем компоненте
function Component() {
const [items, setItems] = useState([]);

const addItem = useCallback((item) => {
setItems(prev => [...prev, item]);
}, []);

return (
<div>
<button onClick={() => addItem({ id: Date.now() })}>Add</button>
{items.map(item => <div key={item.id}>{item.id}</div>)}
</div>
);
// useCallback бесполезен — нет дочерних мемоизированных компонентов
}

// ❌ Передаётся в обычный (не мемоизированный) компонент
function Parent() {
const handleClick = useCallback(() => {}, []);

return <Child onClick={handleClick} />;
// useCallback бесполезен — Child всё равно перерендерится
}

7. useCallback с зависимостями:

function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);

// Функция пересоздаётся при изменении step
const increment = useCallback(() => {
setCount(c => c + step);
}, [step]); // step — зависимость

return (
<div>
<button onClick={increment}>+{step}</button>
<input
value={step}
onChange={e => setStep(Number(e.target.value))}
/>
<MemoizedDisplay count={count} onReset={() => setCount(0)} />
</div>
);
}

Итог: useCallback необходим в двух случаях: 1) передача функции в компонент, обёрнутый в React.memo, и 2) передача функции в массив зависимостей хуков (useEffect, useMemo, useCallback). В остальных случаях использование useCallback добавляет оверхед без пользы.

Вопрос 53. Что происходит, когда рендер уже запустился и ещё не завершился, но произошло обновление стейта?

Таймкод: 00:46:56

Ответ собеседования: Неправильный. Было предположено, что Fiber прервёт текущий рендер, но на самом деле запущенный рендер не прерывается — он завершится полностью, после чего запустится следующий.

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

В React есть два режима работы: синхронный (legacy) и конкурентный (concurrent, React 18+). Поведение при обновлении стейта во время рендера зависит от режима.

1. Синхронный режим (React 17 и ранее, React 18 без concurrent features):

function Component() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(1); // Запускает синхренный рендер

// Во время рендера:
// 1. React начинает рендерить Component
// 2. Если в процессе рендера вызывается setCount(2)
// 3. React НЕ прерывает текущий рендер
// 4. Текущий рендер завершится полностью
// 5. После завершения запустится новый рендер с setCount(2)
};

// Пример обновления стейта ВО ВРЕМЯ рендера
if (count === 1) {
setCount(2); // Обновление во время рендера
// React пометит это обновление
// Текущий рендер продолжится со старым значением
// После завершения — новый рендер с count=2
}

return <div>{count}</div>;
}

Поведение в синхронном режиме:

Рендер 1: count=0 → начало рендера

setCount(1) вызван → React планирует обновление

Рендер 1 ЗАВЕРШАЕТСЯ (не прерывается!)

Батчинг: собирает все обновления стейта

Рендер 2: count=1 → начало нового рендера

setCount(2) вызван во время рендера

Рендер 2 ЗАВЕРШАЕТСЯ

Рендер 3: count=2 → финальный рендер

2. Конкурентный режим (React 18+ с concurrent features):

function Component() {
const [count, setCount] = useState(0);
const [isPending, startTransition] = useTransition();

const handleClick = () => {
// Срочное обновление — может прервать рендер
setCount(c => c + 1);

// Несрочное обновление — не прерывает текущий рендер
startTransition(() => {
setSomeHeavyState(newValue);
});
};

return <div>{count}</div>;
}

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

Рендер 1 (count=0) запущен

Срочное обновление (setCount) → React ПРИОСТАНАВЛИВАЕТ текущий рендер

React проверяет: новое обновление более приоритетное?

Если да → отбрасывает текущий рендер, начинает новый с count=1

Если нет → продолжает текущий рендер

3. Fiber архитектура и прерывание:

// Fiber — это единица работы в React
// Каждый компонент — отдельный Fiber-узел

function App() {
return (
<div> {/* Fiber: div */}
<Header /> {/* Fiber: Header — можно прервать */}
<Main /> {/* Fiber: Main — можно прервать */}
<Footer /> {/* Fiber: Footer — можно прервать */}
</div>
);
}

// React рендерит по одному Fiber за раз
// После каждого Fiber проверяет: есть ли более приоритетная задача?
// Если да → приостанавливает текущую работу
// Если нет → продолжает следующий Fiber

4. Пример: прерывание рендера в конкурентном режиме:

function SlowComponent({ filter }) {
// Тяжёлые вычисления
const items = useMemo(() => {
return expensiveFiltering(data, filter);
}, [filter]);

return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}

function SearchApp() {
const [query, setQuery] = useState('');
const [filter, setFilter] = useState('');

return (
<div>
<input
value={query}
onChange={e => {
setQuery(e.target.value); // Срочное — обновить input сразу
startTransition(() => {
setFilter(e.target.value); // Несрочное — обновить фильтр
});
}}
/>
<SlowComponent filter={filter} />
</div>
);
}

// Что происходит при быстром вводе:
// 1. Пользователь вводит "a" → setQuery("a") — срочное
// 2. React прерывает текущий рендер SlowComponent
// 3. Рендерит input с query="a" — пользователь видит мгновенный отклик
// 4. После завершения срочного обновления → возобновляет рендер SlowComponent
// 5. Пользователь вводит "ab" → setQuery("ab") — снова прерывает
// 6. Предыдущий рендер SlowComponent с filter="a" отбрасывается
// 7. Начинается новый рендер с filter="ab"

5. Ключевые отличия режимов:

┌─────────────────────┬──────────────────────┬──────────────────────┐
│ │ Синхронный режим │ Конкурентный режим │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ Прерывание рендера │ Нет │ Да (для срочных │
│ │ │ обновлений) │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ Приоритеты │ Нет │ Есть (срочные vs │
│ │ │ несрочные) │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ Батчинг │ Только в обработчиках│ Везде (автоматически)│
│ │ событий │ │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ Отбрасывание │ Нет │ Да (устаревшие │
│ рендеров │ │ рендеры отбрасываются│
└─────────────────────┴──────────────────────┴──────────────────────┘

6. Что происходит при setState во время рендера:

function Component() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);

// Синхронный режим:
const handleClick = () => {
setA(1); // Запускает синхронный рендер

// Во время этого рендера:
setB(2); // Это обновление добавляется в очередь

// React завершит текущий рендер с a=1, b=0
// Затем запустит новый рендер с a=1, b=2
};

// Конкурентный режим с startTransition:
const handleClickConcurrent = () => {
setA(1); // Срочное — может прервать текущий рендер

startTransition(() => {
setB(2); // Несрочное — не прерывает
});

// React может прервать текущий рендер
// Выполнить setA(1) сразу
// setB(2) выполнится после
};

return <div>a: {a}, b: {b}</div>;
}

Итог: В синхронном режиме запущенный рендер не прерывается — он завершится полностью, а все обновления стейта будут обработаны в следующем батчинге. В конкурентном режиме (React 18+) срочные обновления могут прервать текущий рендер, если они более приоритетные. Несрочные обновления (через startTransition) не прерывают текущий рендер и выполняются после его завершения.

Вопрос 54. Что такое микроразметка Schema.org и как она влияет на SEO?

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

Ответ собеседования: Неправильный. Кандидат перепутал микроразметку с мета-тегами Open Graph, не знал Schema.org.

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

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

1. Что такое Schema.org:

Schema.org — это совместная инициатива Google, Yandex, Bing и Yahoo!
по созданию единого словаря структурированных данных для веб-страниц.

Цель: помочь поисковикам понимать смысл контента, а не просто текст.

2. Форматы разметки:

<!-- JSON-LD (рекомендуемый Google формат) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Заголовок статьи",
"author": {
"@type": "Person",
"name": "Иван Иванов"
},
"datePublished": "2024-01-15",
"image": "https://example.com/image.jpg"
}
</script>

<!-- Microdata -->
<div itemscope itemtype="https://schema.org/Article">
<h1 itemprop="headline">Заголовок статьи</h1>
<span itemprop="author">Иван Иванов</span>
<time itemprop="datePublished" datetime="2024-01-15">15 января 2024</time>
</div>

<!-- RDFa -->
<div vocab="https://schema.org/" typeof="Article">
<h1 property="headline">Заголовок статьи</h1>
<span property="author">Иван Иванов</span>
</div>

3. Основные типы разметки:

// Разметка организации
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Компания",
"url": "https://example.com",
"logo": "https://example.com/logo.png",
"contactPoint": {
"@type": "ContactPoint",
"telephone": "+7-123-456-7890",
"contactType": "customer service"
}
}
</script>

// Разметка продукта
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Ноутбук",
"image": "https://example.com/laptop.jpg",
"description": "Мощный ноутбук для работы",
"brand": {
"@type": "Brand",
"name": "BrandName"
},
"offers": {
"@type": "Offer",
"price": "50000",
"priceCurrency": "RUB",
"availability": "https://schema.org/InStock"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": "120"
}
}
</script>

// Разметка хлебных крошек
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Главная",
"item": "https://example.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Каталог",
"item": "https://example.com/catalog"
},
{
"@type": "ListItem",
"position": 3,
"name": "Ноутбуки",
"item": "https://example.com/catalog/laptops"
}
]
}
</script>

// FAQ разметка
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Как оформить заказ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Добавьте товар в корзину и перейдите к оформлению."
}
},
{
"@type": "Question",
"name": "Какие способы оплаты?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Мы принимаем карты Visa, Mastercard и наличные."
}
}
]
}
</script>

4. Влияние на SEO — Rich Results:

Обычный результат поиска:
─────────────────────────
Ноутбук BrandName - Купить
https://example.com/laptop
Мощный ноутбук для работы и игр. Цена от 50000 руб.

Rich Result (с разметкой):
─────────────────────────
Ноутбук BrandName ⭐ 4.5 (120 отзывов)
https://example.com/laptop
💰 50000 ₽ | ✅ В наличии
Мощный ноутбук для работы и игр.

5. Типы Rich Results:

┌─────────────────────┬──────────────────────────────────────┐
│ Тип разметки │ Rich Result в поиске │
├─────────────────────┼──────────────────────────────────────┤
│ Product │ Цена, рейтинг, наличие │
├─────────────────────┼──────────────────────────────────────┤
│ Article │ Дата, автор, изображение │
├─────────────────────┼──────────────────────────────────────┤
│ FAQ │ Раскрывающиеся вопросы-ответы │
├─────────────────────┼──────────────────────────────────────┤
│ HowTo │ Пошаговая инструкция с картинками │
├─────────────────────┼──────────────────────────────────────┤
│ Recipe │ Время приготовления, калории, рейтинг│
├─────────────────────┼──────────────────────────────────────┤
│ Event │ Дата, место, цена билетов │
├─────────────────────┼──────────────────────────────────────┤
│ LocalBusiness │ Карта, адрес, телефон, часы работы │
├─────────────────────┼──────────────────────────────────────┤
│ BreadcrumbList │ Хлебные крошки в URL │
└─────────────────────┴──────────────────────────────────────┘

6. Валидация разметки:

// Инструменты проверки:
// 1. Google Rich Results Test: https://search.google.com/test/rich-results
// 2. Schema.org Validator: https://validator.schema.org/
// 3. Yandex Webmaster: https://webmaster.yandex.ru/

// Пример проверки через Google Search Console:
// 1. Перейти в раздел "Улучшения" (Enhancements)
// 2. Проверить статус разметки
// 3. Исправить ошибки и предупреждения

7. Разница Schema.org и Open Graph:

<!-- Schema.org — для поисковых систем -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Заголовок",
"author": "Иван Иванов"
}
</script>

<!-- Open Graph — для социальных сетей (Facebook, VK, Telegram) -->
<meta property="og:title" content="Заголовок" />
<meta property="og:description" content="Описание" />
<meta property="og:image" content="https://example.com/image.jpg" />
<meta property="og:url" content="https://example.com/page" />
<meta property="og:type" content="article" />

<!-- Twitter Cards — для Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Заголовок" />
<meta name="twitter:image" content="https://example.com/image.jpg" />

8. Пример полной разметки для React-приложения:

// Компонент для Schema.org разметки
function JsonLd({ data }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data)
}}
/>
);
}

// Использование на странице продукта
function ProductPage({ product }) {
const schemaData = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
image: product.images,
description: product.description,
brand: {
'@type': 'Brand',
name: product.brand
},
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'RUB',
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock'
},
aggregateRating: product.reviews && {
'@type': 'AggregateRating',
ratingValue: product.rating,
reviewCount: product.reviews.length
}
};

return (
<>
<JsonLd data={schemaData} />
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>{product.price}</span>
</div>
</>
);
}

Итог: Schema.org — это стандарт структурированных данных, который помогает поисковым системам понимать контент страницы. Правильная разметка позволяет получить Rich Results (расширенные сниппеты) в поиске, что увеличивает CTR и видимость сайта. В отличие от Open Graph (для соцсетей), Schema.org предназначена именно для SEO.

Вопрос 55. Как используются robots.txt и sitemap.xml для управления индексацией?

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

Ответ собеседования: Неполный. Кандидат упомянул, что в robots.txt прописываются ключевые слова для роботов, файл лежит в корне проекта и вставляется в HTML-разметку. Ответ неполный и содержит неточности — robots.txt содержит директивы для краулеров, а не ключевые слова, и он не вставляется в HTML.

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

robots.txt и sitemap.xml — это два ключевых файла для управления тем, как поисковые роботы индексируют сайт. Они решают разные задачи, но работают вместе.

1. robots.txt — файл ограничений для краулеров:

robots.txt — это текстовый файл в корне сайта (https://example.com/robots.txt),
который указывает поисковым роботам, какие страницы и разделы
можно/нельзя посещать и индексировать.

Важно: robots.txt не запрещает показ страницы в поиске —
он только ограничивает краулинг. Если страница запрещена в robots.txt,
но на неё есть внешние ссылки — поисковик может показать её URL без сниппета.
# robots.txt — базовый пример
User-agent: *
Disallow: /admin/
Disallow: /private/
Disallow: /api/
Disallow: /*?sort=
Allow: /

Sitemap: https://example.com/sitemap.xml
# robots.txt — продвинутый пример
# Разные правила для разных роботов
User-agent: Googlebot
Disallow: /search/
Disallow: /tmp/
Allow: /search/featured/

User-agent: Yandex
Disallow: /search/
Allow: /search/featured/
Clean-param: utm_source&utm_medium&utm_campaign

User-agent: *
Disallow: /admin/
Disallow: /private/

# Задержка краулинга (не все роботы поддерживают)
Crawl-delay: 1

# Карта сайта
Sitemap: https://example.com/sitemap.xml
Sitemap: https://example.com/sitemap-images.xml
┌─────────────────────┬──────────────────────────────────────────────┐
│ Директива │ Описание │
├─────────────────────┼──────────────────────────────────────────────┤
│ User-agent │ Имя поискового робота (* — для всех) │
├─────────────────────┼──────────────────────────────────────────────┤
│ Disallow │ Запрет на посещение пути │
├─────────────────────┼──────────────────────────────────────────────┤
│ Allow │ Разрешение (имеет приоритет над Disallow) │
├─────────────────────┼──────────────────────────────────────────────┤
│ Sitemap │ Ссылка на карту сайта │
├─────────────────────┼──────────────────────────────────────────────┤
│ Crawl-delay │ Задержка между запросами (секунды) │
├─────────────────────┼──────────────────────────────────────────────┤
│ Clean-param │ Указание параметров URL, не влияющих на │
│ │ контент (только Яндекс) │
└─────────────────────┴──────────────────────────────────────────────┘

2. sitemap.xml — карта сайта для краулеров:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
<lastmod>2024-01-15</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://example.com/products</loc>
<lastmod>2024-01-14</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://example.com/about</loc>
<lastmod>2024-01-10</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
┌─────────────────────┬──────────────────────────────────────────────┐
│ Тег │ Описание │
├─────────────────────┼──────────────────────────────────────────────┤
│ loc │ URL страницы (обязательно) │
├─────────────────────┼──────────────────────────────────────────────┤
│ lastmod │ Дата последнего изменения │
├─────────────────────┼──────────────────────────────────────────────┤
│ changefreq │ Частота изменения: always, hourly, daily, │
│ │ weekly, monthly, yearly, never │
├─────────────────────┼──────────────────────────────────────────────┤
│ priority │ Приоритет от 0.0 до 1.0 (по умолчанию 0.5) │
└─────────────────────┴──────────────────────────────────────────────┘

3. Индексный файл sitemap для больших сайтов:

<!-- sitemap-index.xml — для сайтов с >50 000 URL -->
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://example.com/sitemap-pages.xml</loc>
<lastmod>2024-01-15</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-products.xml</loc>
<lastmod>2024-01-15</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-categories.xml</loc>
<lastmod>2024-01-14</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-images.xml</loc>
<lastmod>2024-01-13</lastmod>
</sitemap>
</sitemapindex>

4. Видео и изображения в sitemap:

<!-- Изображения -->
<url>
<loc>https://example.com/product/123</loc>
<image:image>
<image:loc>https://example.com/images/product-123.jpg</image:loc>
<image:title>Ноутбук BrandName</image:title>
<image:caption>Вид спереди</image:caption>
</image:image>
</url>

<!-- Видео -->
<url>
<loc>https://example.com/video/456</loc>
<video:video>
<video:title>Обзор ноутбука</video:title>
<video:description>Подробный обзор характеристик</video:description>
<video:thumbnail_loc>https://example.com/thumb.jpg</video:thumbnail_loc>
<video:content_loc>https://example.com/video.mp4</video:content_loc>
<video:duration>300</video:duration>
</video:video>
</url>

5. Как они работают вместе:

Пользователь → https://example.com/page

Поисковый робот → Проверяет /robots.txt

┌───────┴───────┐
│ Разрешено? │
└───────┬───────┘
Да │ Нет
↓ │ ↓
Читает sitemap │ Пропускает страницу
↓ │
Находит URL в │
sitemap.xml │
↓ │
Определяет │
приоритет │
↓ │
Индексирует │
страницу │

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

❌ Ошибки в robots.txt:
- Заблокировать важные страницы случайно
- Использовать robots.txt для скрытия страниц (используйте noindex)
- Не указать sitemap
- Синтаксические ошибки

❌ Ошибки в sitemap.xml:
- Указать несуществующие URL (404)
- Указать URL, заблокированные в robots.txt
- Не обновлять lastmod
- Превысить лимит 50 000 URL или 50 МБ на файл
- Указать URL с редиректами

7. Дополнительные способы управления индексацией:

<!-- Мета-тег noindex в HTML (надёжнее, чем robots.txt) -->
<meta name="robots" content="noindex, nofollow" />

<!-- Заголовок X-Robots-Tag в HTTP-ответе -->
<!-- Для не-HTML файлов (PDF, изображения) -->
X-Robots-Tag: noindex, nofollow

<!-- Canonical ссылка -->
<link rel="canonical" href="https://example.com/original-page" />
┌─────────────────────┬──────────────────────┬──────────────────────┐
│ Метод │ Что делает │ Когда использовать │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ robots.txt │ Запрещает краулинг │ Большие разделы, │
│ │ │ служебные страницы │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ meta noindex │ Запрещает индексацию│ Отдельные страницы, │
│ │ │ которые уже в поиске │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ X-Robots-Tag │ Запрещает индексацию│ Не-HTML файлы │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ sitemap.xml │ Указывает URL для │ Все важные страницы, │
│ │ индексации │ новые страницы │
├─────────────────────┼──────────────────────┼──────────────────────┤
│ canonical │ Указывает │ Дубли страниц, │
│ │ оригинальную страницу│ UTL с параметрами │
└─────────────────────┴──────────────────────┴──────────────────────┘

8. Генерация sitemap для React/Next.js приложения:

// next-sitemap.config.js (для Next.js)
module.exports = {
siteUrl: 'https://example.com',
generateRobotsTxt: true,
robotsTxtOptions: {
policies: [
{ userAgent: '*', allow: '/' },
{ userAgent: '*', disallow: ['/admin', '/api', '/private'] },
],
additionalSitemaps: [
'https://example.com/sitemap-products.xml',
],
},
};

// Или динамическая генерация через API Route (Next.js)
// app/sitemap.xml/route.js
export async function GET() {
const products = await db.products.findMany({
select: { id: true, updatedAt: true }
});

const urls = products.map(p => `
<url>
<loc>https://example.com/products/${p.id}</loc>
<lastmod>${p.updatedAt.toISOString()}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
`).join('');

const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com</loc>
<priority>1.0</priority>
</url>
${urls}
</urlset>`;

return new Response(xml, {
headers: { 'Content-Type': 'application/xml' }
});
}

Итог: robots.txt управляет тем, какие страницы робот может посещать, а sitemap.xml указывает, какие страницы важны для индексации. Они дополняют друг друга: robots.txt защищает от нежелательного краулинга, sitemap.xml помогает поисковикам находить нужные страницы. Для удаления страниц из поисковой выдачи лучше использовать meta noindex, а не robots.txt — последний только ограничивает краулинг, но не удаляет URL из индекса.

Вопрос 56. Какие две основные задачи выполняет технология SSR (Server-Side Rendering)?

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

Ответ собеседования: Правильный. Кандидат верно назвал две основные задачи — улучшение SEO и ускорение отрисовки страницы на клиенте.

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

SSR (Server-Side Rendering) решает две фундаментальные проблемы клиентских приложений: поисковую оптимизацию и производительность начальной загрузки.

1. Улучшение SEO (Search Engine Optimization):

Проблема CSR (Client-Side Rendering):
─────────────────────────────────────
Поисковый робот загружает HTML → видит пустой <div id="root">
→ Ждёт выполнения JavaScript → Индексирует (или не успевает)

С SSR:
───────
Поисковый робот загружает HTML → получает полностью отрендеренный контент
→ Сразу индексирует все тексты, заголовки, ссылки
// CSR — что видит робот (до выполнения JS)
<!DOCTYPE html>
<html>
<head>
<title>App</title>
</head>
<body>
<div id="root"></div> <!-- Пустой контейнер! -->
<script src="/bundle.js"></script>
</body>
</html>

// SSR — что видит робот (полный HTML)
<!DOCTYPE html>
<html>
<head>
<title>Каталог товаров</title>
<meta name="description" content="Более 1000 товаров">
</head>
<body>
<div id="root">
<header><nav>...</nav></header>
<main>
<h1>Каталог товаров</h1>
<div class="products">
<div class="product">
<h2>Ноутбук BrandName</h2>
<p>Цена: 50000 руб.</p>
</div>
<!-- ... все товары -->
</div>
</main>
</div>
<script src="/bundle.js"></script>
</body>
</html>

2. Ускорение начальной отрисовки (Performance):

Метрики производительности:
────────────────────────────

CSR без SSR:
FCP (First Contentful Paint): ~2-3 сек ← Плохо
TTI (Time to Interactive): ~3-5 сек
LCP (Largest Contentful Paint): ~3-4 сек

С SSR:
FCP (First Contentful Paint): ~0.5-1 сек ← Хорошо
TTI (Time to Interactive): ~2-3 сек
LCP (Largest Contentful Paint): ~1-2 сек
Временная шкала загрузки:

CSR:
├── HTML (пустой) ──├── Загрузка JS ──├── Выполнение JS ──├── Рендеринг ──├── Готово
0с 0.5с 1.5с 2.5с 3.5с

SSR:
├── HTML (с контентом) ──├── Загрузка JS ──├── Hydration ──├── Готово
0с 0.5с 1.0с 1.5с

FCP здесь! Пользователь видит контент сразу

3. Как работает SSR — процесс:

Клиент Сервер База данных
│ │ │
├── GET /products ────────→│ │
│ ├── Запрос данных ────────→│
│ │←── Данные ───────────────┤
│ │ │
│ ├── Рендеринг React │
│ │ (renderToString) │
│ │ │
│←── Полный HTML ──────────┤ │
│ │ │
├── Отображение HTML │ │
│ (пользователь видит │ │
│ контент сразу) │ │
│ │ │
├── Загрузка JS ──────────→│ │
│ │ │
├── Hydration │ │
│ (React "оживляет" │ │
│ HTML, добавляет │ │
│ обработчики событий) │ │
│ │ │
└── Страница интерактивна │ │

4. Пример SSR на Next.js:

// pages/products/[id].js — Next.js автоматически делает SSR
import { useRouter } from 'next/router';

// Эта функция выполняется на сервере при каждом запросе
export async function getServerSideProps(context) {
const { id } = context.params;

// Данные загружаются на сервере
const product = await fetch(`https://api.example.com/products/${id}`)
.then(res => res.json());

if (!product) {
return { notFound: true };
}

return {
props: {
product, // Передаётся в компонент
},
};
}

// Компонент получает данные как пропсы
export default function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>{product.price}</span>
</div>
);
}

5. Сравнение подходов:

┌─────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ Характеристика │ CSR │ SSR │ SSG │
│ │ (Client-Side) │ (Server-Side) │ (Static Gen) │
├─────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ SEO │ Плохое │ Отличное │ Отличное │
├─────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ FCP │ Медленный │ Быстрый │ Самый быстрый │
├─────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Нагрузка на │ Минимальная │ Высокая │ Минимальная │
│ сервер │ │ │ │
├─────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Данные │ Устаревшие │ Актуальные │ На момент сборки │
├─────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Примеры │ Create React App │ Next.js, Nuxt │ Next.js, Gatsby │
└─────────────────┴──────────────────┴──────────────────┴──────────────────┘

6. Когда использовать SSR:

✅ Использовать SSR:
- Страницы с динамическим контентом (каталог, профили пользователей)
- Страницы, критичные для SEO (лендинги, блог, новости)
- Страницы с персонализированным контентом
- Приложения, где важна первая загрузка

❌ Не использовать SSR:
- Админ-панели (не нужен SEO)
- Дашборды с реальным временем (лучше CSR + WebSocket)
- Простые лендинги (лучше SSG — быстрее и дешевле)

Итог: SSR выполняет две основные задачи — улучшает SEO (поисковые роботы получают готовый HTML с контентом) и ускоряет начальную отрисовку (пользователь видит контент сразу, не дожидаясь загрузки и выполнения JavaScript). Это достигается за счёт рендеринга на сервере, но ценой повышенной нагрузки на сервер и более сложной архитектуры.

Вопрос 57. Что такое гидратация (hydration) в контексте SSR-приложений?

Таймкод: 00:52:45

Ответ собеседования: Правильный. Кандидат верно описал гидратацию как процесс "оживления" HTML от сервера с помощью JavaScript.

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

Гидратация — это процесс, при котором JavaScript-фреймворк на клиенте «привязывает» обработчики событий и создаёт реактивное дерево компонентов к уже существующему HTML, который был отрендерен на сервере.

1. Полный цикл SSR + Гидратация:

Сервер Клиент
────── ──────
1. Получает запрос GET /products
2. Загружает данные из БД
3. Рендерит React-компоненты
в HTML-строку (renderToString)
4. Отправляет полный HTML
──────────────→ 5. Браузер отображает HTML
(пользователь видит контент)
──────────────→ 6. Загружается JavaScript bundle
──────────────→ 7. ГИДРАТАЦИЯ:
- React проходит по DOM
- Сравнивает с виртуальным DOM
- Привязывает обработчики событий
- Создаёт внутреннее состояние
──────────────→ 8. Страница интерактивна

2. Что происходит во время гидрации — детально:

// Сервер отправляет такой HTML:
<div id="root">
<div class="product-card" data-reactroot="">
<h1>Ноутбук BrandName</h1>
<p class="price">50000 ₽</p>
<button class="buy-btn">Купить</button>
</div>
</div>

// Клиентский React выполняет гидратацию:
import { hydrateRoot } from 'react-dom/client';
import App from './App';

// hydrateRoot НЕ перерисовывает DOM — он привязывается к существующему!
const root = hydrateRoot(
document.getElementById('root'),
<App />
);

// React делает следующее:
// 1. Создаёт виртуальный DOM из <App />
// 2. Сравнивает с существующим DOM от сервера
// 3. Если совпадает — привязывает обработчики событий
// 4. Если не совпадает — выдаёт предупреждение (hydration mismatch)

3. Проблема Hydration Mismatch (несоответствие):

Hydration mismatch возникает, когда HTML от сервера
НЕ совпадает с тем, что ожидает клиентский React.

Причины:
────────
- Разные данные на сервере и клиенте
- Зависимость от браузерных API (window, localStorage)
- Случайные значения (Math.random(), Date.now())
- Разные часовые пояса
// ❌ Пример проблемы: hydration mismatch
function TimeDisplay() {
// На сервере: "10:30"
// На клиенте: "10:31" (другое время!)
const now = new Date().toLocaleTimeString();
return <span>{now}</span>;
}

// ✅ Решение: используем useEffect для клиентского кода
function TimeDisplay() {
const [time, setTime] = useState(
() => new Date().toLocaleTimeString() // Начальное значение
);

useEffect(() => {
// Обновляем только на клиенте
setTime(new Date().toLocaleTimeString());
const interval = setInterval(() => {
setTime(new Date().toLocaleTimeString());
}, 1000);
return () => clearInterval(interval);
}, []);

return <span>{time}</span>;
}

// ✅ Решение: условный рендеринг для браузерного API
function WindowSize() {
const [width, setWidth] = useState(null);

useEffect(() => {
setWidth(window.innerWidth);
const handler = () => setWidth(window.innerWidth);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);

if (width === null) {
return null; // Не рендерим на сервере
}

return <span>Ширина окна: {width}px</span>;
}

4. Визуализация проблемы:

Сервер рендерит: Клиент ожидает:
<div> <div>
<p>Случайное: 42</p> <p>Случайное: 87</p> ← Другое число!
</div> </div>

React при гидратации:
⚠️ Text content did not match.
Server: "Случайное: 42"
Client: "Случайное: 87"

5. Сравнение render vs hydrate:

// createRoot — для чистого CSR (Client-Side Rendering)
// Полностью пересоздаёт DOM
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />); // Заменяет всё содержимое

// hydrateRoot — для SSR + гидратации
// Привязывается к существующему DOM
import { hydrateRoot } from 'react-dom/client';
const root = hydrateRoot(
document.getElementById('root'),
<App /> // Не пересоздаёт, а "оживляет"
);

6. Производительность гидратации:

Проблемы производительности:
─────────────────────────────

1. Блокирующая гидратация (по умолчанию):
HTML отображается → JS загружается → Гидрация → Интерактивно
↑ Пользователь видит кнопку, но клик не работает!

2. Selective Hydration (React 18+):
Гидрация происходит по частям, приоритет — видимым элементам

3. Streaming SSR + Suspense:
Сервер отправляет HTML частями, клиент гидрирует по мере поступления
// React 18: Streaming SSR с Suspense
// Серверная сторона
import { renderToPipeableStream } from 'react-dom/server';

// Клиентская сторона
import { hydrateRoot } from 'react-dom/client';

function App() {
return (
<Layout>
<Suspense fallback={<Skeleton />}>
{/* Этот компонент загружается отдельно */}
<ProductList />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Comments />
</Suspense>
</Layout>
);
}

Итог: Гидратация — это процесс «оживления» HTML, полученного с сервера, путём привязки JavaScript-обработчиков событий и создания реактивного состояния. Главная проблема — hydration mismatch, когда контент на сервере и клиенте различается. Для предотвращения нужно избегать зависимостей от браузерных API и недетерминированных значений в серверном рендеринге. React 18 улучшил процесс через Streaming SSR и Selective Hydration.

Вопрос 58. В чём различия между SSR и ISR (Incremental Static Regeneration)?

Таймкод: 00:53:34

Ответ собеседования: Правильный. Кандидат верно описал ISR как промежуточный подход между SSR и SSG с ревалидацией.

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

ISR (Incremental Static Regeneration) — это гибридный подход, при котором страницы генерируются статически один раз, а затем автоматически обновляются по расписанию без необходимости полной пересборки всего сайта.

1. Сравнение трёх подходов:

SSG (Static Site Generation):
──────────────────────────────
Время сборки: ████████████████████ (долго, все страницы)
Время запроса: ░░░░ (мгновенно, отдаём готовый файл)
Актуальность: Данные на момент сборки
Пример: getStaticProps без revalidate

SSR (Server-Side Rendering):
────────────────────────────
Время сборки: ░░░░ (нет)
Время запроса: ████████ (каждый раз рендерим)
Актуальность: Всегда актуальные данные
Пример: getServerSideProps

ISR (Incremental Static Regeneration):
──────────────────────────────────────
Время сборки: ████████ (один раз при сборке)
Время запроса: ░░░░ (мгновенно после генерации)
Актуальность: Обновляется по расписанию
Пример: getStaticProps с revalidate: 60

2. Как работа ISR — пошагово:

Временная шкала ISR:
────────────────────

t=0с Первый запрос → страницы нет в кэше
→ Генерируем на лету (как SSR)
→ Сохраняем в кэш
→ Отдаём пользователю
→ Статус: STALE (скоро устареет)

t=10с Второй запрос → отдаём из кэша (быстро!)
→ Проверяем: прошло ли revalidate время?
→ Нет (прошло только 10 сек из 60)
→ Отдаём старый кэш

t=60с Третий запрос → время ревалидации пришло!
→ Отдаём СТАРЫЙ кэш пользователю (не ждём!)
→ Фоново запускаем регенерацию
→ Пользователь не ждёт генерацию

t=65с Страница сгенерирована → обновляем кэш
→ Теперь все запросы получают свежую версию

t=120с Следующая ревалидация...

3. Реализация ISR в Next.js:

// pages/products/[id].js
export async function getStaticProps({ params }) {
const product = await fetch(
`https://api.example.com/products/${params.id}`
).then(res => res.json());

return {
props: { product },
// Ключевое отличие от SSG — параметр revalidate
revalidate: 60, // Ревалидация каждые 60 секунд
};
}

// Также нужен getStaticPaths для динамических маршрутов
export async function getStaticPaths() {
const products = await fetch(
'https://api.example.com/products?limit=100'
).then(res => res.json());

const paths = products.map(product => ({
params: { id: product.id.toString() },
}));

return {
paths,
// 'blocking' — новые страницы генерируются при первом запросе
// 'false' — новые страницы вернут 404
fallback: 'blocking',
};
}

export default function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>{product.price}</span>
<small>
Страница обновляется каждые 60 секунд
</small>
</div>
);
}

4. На уровне HTTP — как это выглядит:

SSG ответ:
──────────
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable

Никогда не меняется

SSR ответ:
──────────
HTTP/1.1 200 OK
Cache-Control: no-store, must-revalidate

Всегда генерируем заново

ISR ответ (свежая версия):
──────────────────────────
HTTP/1.1 200 OK
Cache-Control: public, s-maxage=60, stale-while-revalidate=30
↑ ↑
Актуальна 60 сек Отдаём старое + обновляем 30 сек

5. On-Demand ISR — ручная ревалидация:

// pages/api/revalidate.js
// Эндпоинт для принудительной ревалидации
export default async function handler(req, res) {
const { secret, path } = req.query;

// Проверяем секрет для безопасности
if (secret !== process.env.REVALIDATION_SECRET) {
return res.status(401).json({ message: 'Invalid token' });
}

try {
// Ревалидируем конкретный путь
await res.revalidate(path);
return res.json({ revalidated: true, path });
} catch (err) {
return res.status(500).json({ message: 'Error revalidating' });
}
}

// Использование:
// POST /api/revalidate?secret=MY_SECRET&path=/products/123
// Пример: ревалидация при обновлении данных
// pages/api/products/[id].js — webhook от CMS
export default async function handler(req, res) {
if (req.method === 'POST') {
const { id } = req.body;

// Обновляем данные в БД
await db.products.update(id, req.body);

// Запускаем ревалидацию страницы
await fetch(
`${process.env.SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_SECRET}&path=/products/${id}`
);

return res.json({ success: true });
}
}

6. Fallback стратегии:

// fallback: 'false'
// ─────────────────
// Только страницы из getStaticPaths доступны
// Остальные → 404
export async function getStaticPaths() {
return { paths: [{ params: { id: '1' } }], fallback: 'false' };
}

// fallback: 'true'
// ────────────────
// Все страницы доступны
// При первом запросе показываем loading, генерируем на лету
export async function getStaticPaths() {
return { paths: [], fallback: 'true' };
}

function ProductPage({ product }) {
const router = useRouter();

// Показываем загрузку во время генерации
if (router.isFallback) {
return <div>Загрузка...</div>;
}

return <h1>{product.name}</h1>;
}

// fallback: 'blocking'
// ─────────────────────
// Все страницы доступны
// При первом запросе ждём генерацию (как SSR)
// Пользователь не видит loading-состояние
export async function getStaticPaths() {
return { paths: [], fallback: 'blocking' };
}

7. Когда использовать каждый подход:

SSG — когда данные редко меняются:
─────────────────────────────────────
✅ Блог, документация, лендинги
✅ Страницы «О компании», контакты
✅ Маркетинговые страницы

SSR — когда данные устаревают мгновенно:
──────────────────────────────────────────
✅ Персонализированные страницы (профиль пользователя)
✅ Данные в реальном времени (котировки, аукционы)
✅ Страницы, зависящие от cookies/авторизации

ISR — когда данные обновляются периодически:
──────────────────────────────────────────────
✅ Каталог товаров (цена меняется раз в час)
✅ Новостная лента (обновление каждые 5 минут)
✅ Страницы с рейтингами/статистикой
✅ Страницы с комментариями/отзывами

8. Сравнительная таблица:

┌──────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ │ SSG │ SSR │ ISR │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Генерация │ При сборке │ При запросе │ При сборке + │
│ │ │ │ по расписанию │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ TTFB │ Минимальный │ Высокий │ Минимальный │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Актуальность │ Устаревшие │ Всегда свежие │ Свежие в рамках │
│ │ │ │ окна revalidate │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Нагрузка на │ Нет │ Каждый запрос │ Только при │
│ сервер │ │ │ ревалидации │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ CDN-кэширование │ Отличное │ Невозможно │ Хорошее │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Сложность │ Низкая │ Средняя │ Средняя │
├──────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Примеры │ Gatsby, Hugo │ Next.js SSR │ Next.js ISR │
└──────────────────┴──────────────────┴──────────────────┴──────────────────┘

Итог: ISR — это компромисс между скоростью SSG и актуальностью SSR. Страница генерируется один раз при сборке (или при первом запросе), кэшируется, и автоматически регенерируется через заданный интервал revalidate. Пользователь никогда не ждёт генерацию — он всегда получает кэшированную версию (даже если она немного устарела), а обновление происходит в фоне. Это идеально подходит для контента, который обновляется периодически, но не требует мгновенной актуальности.

Вопрос 59. Практическая задача: определить порядок выполнения console.log в коде с синхронными задачами, микрозадачами (Promise) и макрозадачами (setTimeout).

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

Ответ собеседования: Правильный. Кандидат правильно определил порядок выполнения: сначала весь синхронный код, затем микрозадачи (Promise.then) в порядке их появления, затем макрозадачи (setTimeout).

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

Разберём типовую задачу на понимание Event Loop в JavaScript с примером кода.

1. Пример задачи:

console.log('1');

setTimeout(() => {
console.log('2');
}, 0);

Promise.resolve().then(() => {
console.log('3');
});

console.log('4');

setTimeout(() => {
console.log('5');
}, 0);

Promise.resolve().then(() => {
console.log('6');
});

Правильный ответ: 1, 4, 3, 6, 2, 5

2. Пошаговый разбор выполнения:

Шаг 1: Синхронный код (Call Stack)
────────────────────────────────────
Call Stack: console.log('1')
Output: 1

Шаг 2: setTimeout → в очередь макрозадач
─────────────────────────────────────────
Call Stack: setTimeout(callback, 0)
→ callback попадает в Macrotask Queue
Macrotask Queue: [callback(2)]

Шаг 3: Promise.then → в очередь микрозадач
───────────────────────────────────────────
Call Stack: Promise.resolve().then(callback)
→ callback попадает в Microtask Queue
Microtask Queue: [callback(3)]

Шаг 4: Синхронный код
──────────────────────
Call Stack: console.log('4')
Output: 1, 4

Шаг 5: Второй setTimeout
────────────────────────
Macrotask Queue: [callback(2), callback(5)]

Шаг 6: Второй Promise.then
───────────────────────────
Microtask Queue: [callback(3), callback(6)]

Шаг 7: Call Stack пуст → выполняем ВСЕ микрозадачи
──────────────────────────────────────────────────
Call Stack: console.log('3')
Output: 1, 4, 3

Call Stack: console.log('6')
Output: 1, 4, 3, 6

Microtask Queue: [] (пусто)

Шаг 8: Микрозадачи выполнены → берём ОДНУ макрозадачу
──────────────────────────────────────────────────────
Call Stack: console.log('2')
Output: 1, 4, 3, 6, 2

Шаг 9: Следующая итерация Event Loop → ещё одна макрозадача
───────────────────────────────────────────────────────────
Call Stack: console.log('5')
Output: 1, 4, 3, 6, 2, 5

3. Визуализация Event Loop:

┌─────────────────────────────────────────┐
│ CALL STACK │
│ (выполняется синхронный код) │
│ │
│ console.log('1') → выводит "1" │
│ console.log('4') → выводит "4" │
└──────────────┬──────────────────────────┘
│ стек пуст

┌─────────────────────────────────────────┐
│ MICROTASK QUEUE │
│ (Promise.then, queueMicrotask, │
│ MutationObserver) │
│ │
│ → callback(3) выводит "3" │
│ → callback(6) выводит "6" │
│ │
│ ⚠️ ВЫПОЛНЯЕТСЯ ВСЁ ПОДРЯД │
│ пока очередь не опустеет │
└──────────────┬──────────────────────────┘
│ микрозадачи выполнены

┌─────────────────────────────────────────┐
│ MACROTASK QUEUE │
│ (setTimeout, setInterval, │
│ setImmediate, I/O) │
│ │
│ → callback(2) выводит "2" │
│ → callback(5) выводит "5" │
│ │
│ ⚠️ ПО ОДНОЙ ЗАДАЧЕ ЗА ИТЕРАЦИЮ │
└─────────────────────────────────────────┘

4. Более сложный пример с вложенностью:

console.log('start');

setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise in timeout1');
});
}, 0);

Promise.resolve().then(() => {
console.log('promise1');
setTimeout(() => {
console.log('timeout in promise1');
}, 0);
});

console.log('end');

Ответ: start, end, promise1, timeout1, promise in timeout1, timeout in promise1

Разбор:
───────
1. console.log('start') → "start"
2. setTimeout → Macrotask Queue [timeout1]
3. Promise.then → Microtask Queue [promise1]
4. console.log('end') → "end"

Микрозадачи:
5. console.log('promise1') → "promise1"
6. setTimeout внутри promise → Macrotask Queue [timeout1, timeout in promise1]

Макрозадачи (по одной):
7. console.log('timeout1') → "timeout1"
8. Promise внутри timeout → Microtask Queue [promise in timeout1]
(микрозадача выполнится ДО следующей макрозадачи!)
9. console.log('promise in timeout1') → "promise in timeout1"
10. console.log('timeout in promise1') → "timeout in promise1"

5. Ключевые правила Event Loop:

Правило 1: Синхронный код выполняется первым
─────────────────────────────────────────────
Весь код в Call Stack выполняется до конца,
прежде чем что-либо из очередей начнёт выполняться

Правило 2: Микрозадачи имеют приоритет над макрозадачами
─────────────────────────────────────────────────────────
После опустошения Call Stack ВСЕ микрозадачи
выполняются до первой макрозадачи

Правило 3: Микрозадачи выполняются все подряд
──────────────────────────────────────────────
Если в процессе выполнения микрозадач появляются
новые микрозадачи — они тоже выполняются до макрозадач

Правило 4: Макрозадачи выполняются по одной
────────────────────────────────────────────
После каждой макрозадачи Event Loop проверяет
микрозадачи и выполняет их, если есть

Правило 5: Новая микрозадача внутри макрозадачи
────────────────────────────────────────────────
Выполнится сразу после текущей макрозадачи,
ДО следующей макрозадачи

6. Сравнение типов задач:

┌─────────────────────┬──────────────────────────┬──────────────────────┐
│ Тип │ Когда выполняется │ Примеры │
├─────────────────────┼──────────────────────────┼──────────────────────┤
│ Синхронный код │ Сразу │ Обычные вызовы │
│ │ │ функций │
├─────────────────────┼──────────────────────────┼──────────────────────┤
│ Микрозадача │ После синхронного кода, │ Promise.then/catch │
│ (Microtask) │ до макрозадач │ queueMicrotask() │
│ │ │ MutationObserver │
├─────────────────────┼──────────────────────────┼──────────────────────┤
│ Макрозадача │ После всех микрозадач │ setTimeout() │
│ (Macrotask) │ │ setInterval() │
│ │ │ setImmediate (Node) │
│ │ │ requestAnimationFrame│
│ │ │ I/O операции │
└─────────────────────┴──────────────────────────┴──────────────────────┘

7. Типичная ловушка:

// ❌ Распространённое заблуждение:
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));

// Многие думают: "setTimeout с 0мс выполнится сразу"
// На самом деле: ВСЕГДА сначала promise, потом timeout
// Потому что микрозадачи имеют приоритет над макрозадачами

// Вывод:
// promise
// timeout

Итог: Event Loop в JavaScript работает по принципу: весь синхронный код → все микрозадачи → одна макрозадача → проверка микрозадач → следующая макрозадача. Микрозадачи (Promise, queueMicrotask) всегда выполняются до макрозадач (setTimeout, setInterval). Ключевой нюанс: новая микрозадача, созданная внутри макрозадачи, выполнится сразу после текущей макрозадачи, но до следующей.

Вопрос 60. Практическая задача: найти максимальный доход от покупки и продажи доллара на основе массива курсов.

Таймкод: 00:57:44

Ответ собеседования: Правильный. Кандидат верно определил, что нужно купить за 80 (минимальный курс) и продать за 125 (максимальный курс после покупки), получив доход 45. Реализовал алгоритм с линейной сложностью O(n).

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

Это классическая задача «Best Time to Buy and Sell Stock» — одна из самых популярных задач на собеседованиях.

1. Условие задачи:

Дан массив курсов доллара по дням:
prices = [100, 80, 90, 70, 125, 60, 110]

Нужно выбрать один день для покупки и один день для продажи
(продажа ТОЛЬКО после покупки) так, чтобы максимизировать прибыль.

Ответ: купить за 70 (день 4), продать за 125 (день 5) → прибыль = 55

2. Решение за O(n) — один проход:

function maxProfit(prices) {
if (prices.length < 2) return 0;

let minPrice = prices[0]; // минимальная цена покупки
let maxProfit = 0; // максимальная прибыль

for (let i = 1; i < prices.length; i++) {
// Вариант 1: продать сегодня — какая прибыль?
const potentialProfit = prices[i] - minPrice;

// Обновляем максимальную прибыль
maxProfit = Math.max(maxProfit, potentialProfit);

// Вариант 2: может, выгоднее купить сегодня?
minPrice = Math.min(minPrice, prices[i]);
}

return maxProfit;
}

// Пример:
console.log(maxProfit([100, 80, 90, 70, 125, 60, 110]));
// Вывод: 55

3. Пошаговый разбор алгоритма:

prices = [100, 80, 90, 70, 125, 60, 110]

День 0: price=100
minPrice = 100
maxProfit = 0
┌──────────────────────────────────┐
│ Покупаем за 100 │
│ Продать нечего — это первый день │
└──────────────────────────────────┘

День 1: price=80
potentialProfit = 80 - 100 = -20 (убыток, не продаём)
maxProfit = max(0, -20) = 0
minPrice = min(100, 80) = 80 ← новая лучшая цена покупки!
┌──────────────────────────────────┐
│ Выгоднее купить за 80, чем за 100│
└──────────────────────────────────┘

День 2: price=90
potentialProfit = 90 - 80 = 10
maxProfit = max(0, 10) = 10 ← новая лучшая прибыль!
minPrice = min(80, 90) = 80
┌──────────────────────────────────┐
│ Купили за 80, продали за 90 │
│ Прибыль: +10 │
└──────────────────────────────────┘

День 3: price=70
potentialProfit = 70 - 80 = -10 (убыток)
maxProfit = max(10, -10) = 10
minPrice = min(80, 70) = 70 ← ещё выгоднее!
┌──────────────────────────────────┐
│ Нашли дно! Лучше купить за 70 │
└──────────────────────────────────┘

День 4: price=125
potentialProfit = 125 - 70 = 55
maxProfit = max(10, 55) = 55 ← ОТВЕТ!
minPrice = min(70, 125) = 70
┌──────────────────────────────────┐
│ Купили за 70, продали за 125 │
│ Прибыль: +55 ✓ │
└──────────────────────────────────┘

День 5: price=60
potentialProfit = 60 - 70 = -10
maxProfit = 55 (не изменилась)
minPrice = min(70, 60) = 60
┌──────────────────────────────────┐
│ Новое дно за 60, но продавать │
│ некуда — после 60 только 110 │
│ 110-60=50 < 55 │
└──────────────────────────────────┘

День 6: price=110
potentialProfit = 110 - 60 = 50
maxProfit = max(55, 50) = 55 (не изменилась)

ИТОГО: maxProfit = 55
Купить за 70 (день 3), продать за 125 (день 4)

4. Наивное решение за O(n²) — для сравнения:

// ❌ Медленное решение — перебор всех пар
function maxProfitBruteforce(prices) {
let maxProfit = 0;

for (let buy = 0; buy < prices.length; buy++) {
for (let sell = buy + 1; sell < prices.length; sell++) {
const profit = prices[sell] - prices[buy];
maxProfit = Math.max(maxProfit, profit);
}
}

return maxProfit;
}

// Сложность: O(n²) — не проходит на больших массивах
// Для массива из 10⁵ элементов — 10¹⁰ операций!

5. Расширенная версия — вернуть дни покупки и продажи:

function maxProfitWithDays(prices) {
if (prices.length < 2) return { profit: 0, buyDay: -1, sellDay: -1 };

let minPrice = prices[0];
let minDay = 0;
let maxProfit = 0;
let buyDay = 0;
let sellDay = 0;

for (let i = 1; i < prices.length; i++) {
const profit = prices[i] - minPrice;

if (profit > maxProfit) {
maxProfit = profit;
buyDay = minDay;
sellDay = i;
}

if (prices[i] < minPrice) {
minPrice = prices[i];
minDay = i;
}
}

return {
profit: maxProfit,
buyDay,
sellDay,
buyPrice: prices[buyDay],
sellPrice: prices[sellDay],
};
}

console.log(maxProfitWithDays([100, 80, 90, 70, 125, 60, 110]));
// {
// profit: 55,
// buyDay: 3,
// sellDay: 4,
// buyPrice: 70,
// sellPrice: 125
// }

6. Вариация задачи — несколько сделок:

// Разрешено совершить сколько угодно сделок
// (купил → продал → купил → продал)
// Идея: ловим каждый рост цены

function maxProfitUnlimited(prices) {
let totalProfit = 0;

for (let i = 1; i < prices.length; i++) {
// Если цена выросла — берём прибыль
if (prices[i] > prices[i - 1]) {
totalProfit += prices[i] - prices[i - 1];
}
}

return totalProfit;
}

// Пример: [1, 5, 3, 8]
// Купили за 1, продали за 5 → +4
// Купили за 3, продали за 8 → +5
// Итого: 9
console.log(maxProfitUnlimited([1, 5, 3, 8])); // 9

7. Графическое представление:

Курс доллара по дням:
─────────────────────

125 │ ★ ← продаём здесь (maxProfit = 55)
│ /│
110 │ / │ ●
│ / │ /│
100 │● / │ / │
│ \ / │ / │
90 │ \ ● │ / │
│ \ / │ / │
80 │ ● │ / │
│ │ / │
70 │ ● ← покупаем │
│ │ \ │
60 │ │ ● │
│ │ │
└──────────────────────────────
Д0 Д1 Д2 Д3 Д4 Д5 Д6

8. Сравнение подходов:

┌─────────────────────┬────────────┬───────────┬──────────────────────┐
│ Подход │ Сложность │ Память │ Когда использовать │
├─────────────────────┼────────────┼───────────┼──────────────────────┤
│ Наивный (все пары) │ O(n²) │ O(1) │ Никогда на проде │
├─────────────────────┼────────────┼───────────┼──────────────────────┤
│ Один проход (min) │ O(n) │ O(1) │ Одна сделка ✓ │
├─────────────────────┼────────────┼───────────┼──────────────────────┤
│ Суммирование ростов │ O(n) │ O(1) │ Много сделок ✓ │
├─────────────────────┼────────────┼───────────┼──────────────────────┤
│ DP (k сделок) │ O(n·k) │ O(n·k) │ Ограничено k сделок │
└─────────────────────┴────────────┴───────────┴──────────────────────┘

Итог: Оптимальное решение за O(n) — один проход по массиву, на каждом шаге отслеживаем минимальную цену покупки и максимальную прибыль. Ключевая идея: на каждом шаге решаем — выгоднее ли продать сегодня (используя минимальную цену из прошлого) или обновить минимальную цену для будущих продаж.

Вопрос 61. Практическая задача: найти и устранить утечку памяти в коде.

Таймкод: 01:05:24

Ответ собеседования: Правильный. Кандидат обнаружил утечку памяти: при добавлении элементов навешивались обработчики событий (addEventListener), но при удалении элементов слушатели не удалялись. Также предложил обнулять массив элементов после удаления, чтобы избежать накопления ссылок в памяти.

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

Утечки памяти в JavaScript — одна из самых частых проблем в веб-приложениях. Разберём основные причины и способы их устранения.

1. Проблемный код с утечкой:

// ❌ Код с утечкой памяти
class Component {
constructor() {
this.items = [];
this.handlers = [];
}

addItem(data) {
const element = document.createElement('div');
element.textContent = data;
document.body.appendChild(element);

// Утечка: создаём новый обработчик каждый раз
const handler = () => this.handleClick(data);
element.addEventListener('click', handler);

this.items.push(element);
this.handlers.push(handler); // даже если храним — не удаляем!
}

removeItems() {
// ❌ Утечка: удаляем элементы из DOM,
// но обработчики событий остаются в памяти!
this.items.forEach(item => {
item.remove(); // элемент удалён из DOM, но ссылка осталась
});
// ❌ this.items всё ещё содержит ссылки на DOM-элементы
// ❌ this.handlers содержит ссылки на замыкания
}

handleClick(data) {
console.log('Clicked:', data);
}
}

// Каждый вызов addItem() без правильной очистки = утечка
const component = new Component();
for (let i = 0; i < 10000; i++) {
component.addItem(`Item ${i}`);
}
component.removeItems(); // Элементы из DOM удалены, но память не освобождена!

2. Исправленный код:

// ✅ Код без утечки памяти
class Component {
constructor() {
this.items = new Map(); // Map для связи элемента и обработчика
}

addItem(data) {
const element = document.createElement('div');
element.textContent = data;
document.body.appendChild(element);

// Создаём обработчик и сохраняем связь
const handler = () => this.handleClick(data);
element.addEventListener('click', handler);

this.items.set(element, { handler, data });
}

removeItems() {
this.items.forEach(({ handler }, element) => {
// ✅ Удаляем обработчик события
element.removeEventListener('click', handler);
// ✅ Удаляем элемент из DOM
element.remove();
});
// ✅ Очищаем Map — удаляем все ссылки
this.items.clear();
}

handleClick(data) {
console.log('Clicked:', data);
}
}

3. Основные причины утечек памяти в JavaScript:

┌─────────────────────────────────────────────────────────────────┐
│ ПРИЧИНЫ УТЕЧЕК ПАМЯТИ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Забытые обработчики событий (addEventListener) │
│ → Элемент удалён из DOM, но обработчик держит ссылку │
│ │
│ 2. Замыкания (closures) │
│ → Функция захватывает переменные внешней области видимости │
│ → Эти переменные не могут быть собраны GC │
│ │
│ 3. Неочищенные таймеры (setTimeout, setInterval) │
│ → Таймер продолжает работать даже после "удаления" объекта │
│ │
│ 4. Ссылка на DOM-элементы из JavaScript-объектов │
│ → DOM удалён, но JS-объект хранит ссылку │
│ │
│ 5. Накопление данных в массивах/объектах │
│ → Бесконечный рост коллекции без очистки │
│ │
│ 6. Циклические ссылки (менее актуально в современных движках) │
│ → Два объекта ссылаются друг на друга │
│ │
└─────────────────────────────────────────────────────────────────┘

4. Примеры каждой утечки и их исправления:

А. Забытые обработчики событий:

// ❌ Утечка
class Widget {
constructor() {
this.button = document.querySelector('#myButton');
this.button.addEventListener('click', this.onClick.bind(this));
}
// При "удалении" виджета обработчик остаётся
}

// ✅ Исправление
class Widget {
constructor() {
this.button = document.querySelector('#myButton');
this.boundClick = this.onClick.bind(this);
this.button.addEventListener('click', this.boundClick);
}

destroy() {
this.button.removeEventListener('click', this.boundClick);
this.button = null; // удаляем ссылку на DOM
}

onClick() {
console.log('clicked');
}
}

Б. Утечка через замыкание:

// ❌ Утечка: замыкание хранит ссылку на огромный массив
function createHandler() {
const hugeData = new Array(1000000).fill('x'); // 8MB данных

return function(event) {
// hugeData захвачено замыканием,
// даже если не используется в этой функции!
console.log('event', event.type);
};
}

// Каждый вызов создаёт замыкание с 8MB данных
for (let i = 0; i < 100; i++) {
element.addEventListener('click', createHandler()); // 800MB утечки!
}

// ✅ Исправление: не захватываем лишнее
function createHandler() {
// hugeData не передаём в замыкание
return function(event) {
console.log('event', event.type);
};
}

// Или передаём только нужные данные
function createHandler(id) {
const hugeData = loadData(id); // загружаем по необходимости
return function(event) {
console.log('event', event.type, id);
// hugeData используется — но только когда нужно
};
}

В. Неочищенные таймеры:

// ❌ Утечка: таймер продолжает работать
class PollingService {
start() {
this.intervalId = setInterval(() => {
this.fetchData();
}, 1000);
}

// Нет метода stop() — таймер работает вечно!
}

// ✅ Исправление
class PollingService {
start() {
this.intervalId = setInterval(() => {
this.fetchData();
}, 1000);
}

stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}

fetchData() {
// ...
}
}

// То же самое с setTimeout
// ❌ Утечка
setTimeout(() => {
this.updateUI(); // ссылка на this сохраняется
}, 60000);

// ✅ Исправление
this.timeoutId = setTimeout(() => {
this.updateUI();
}, 60000);

// При уничтожении:
clearTimeout(this.timeoutId);
this.timeoutId = null;

Г. Ссылка на удалённые DOM-элементы:

// ❌ Утечка
class Cache {
constructor() {
this.elements = [];
}

cache(selector) {
const el = document.querySelector(selector);
this.elements.push(el); // храним ссылку на DOM
}

remove(selector) {
const el = document.querySelector(selector);
el.remove(); // удалили из DOM
// но this.elements всё ещё содержит ссылку!
}
}

// ✅ Исправление
class Cache {
constructor() {
this.elements = new Map();
}

cache(key, selector) {
const el = document.querySelector(selector);
this.elements.set(key, el);
}

remove(key) {
const el = this.elements.get(key);
if (el) {
el.remove();
this.elements.delete(key); // ✅ удаляем ссылку
}
}

clear() {
this.elements.forEach(el => el.remove());
this.elements.clear();
}
}

5. Использование WeakMap и WeakRef для предотвращения утечек:

// ✅ WeakMap — ключи собираются GC автоматически
class EventManager {
constructor() {
// WeakMap: если DOM-элемент удалён — запись удалится автоматически
this.listeners = new WeakMap();
}

addListener(element, event, handler) {
element.addEventListener(event, handler);

if (!this.listeners.has(element)) {
this.listeners.set(element, []);
}
this.listeners.get(element).push({ event, handler });
}

removeAllFor(element) {
const listeners = this.listeners.get(element);
if (listeners) {
listeners.forEach(({ event, handler }) => {
element.removeEventListener(event, handler);
});
// WeakMap удалит запись автоматически,
// когда элемент будет собран GC
}
}
}

// ✅ WeakRef — слабая ссылка на объект
class Cache {
constructor() {
this.cache = new Map();
}

set(key, value) {
// WeakRef не мешает GC собрать объект
this.cache.set(key, new WeakRef(value));
}

get(key) {
const ref = this.cache.get(key);
if (!ref) return undefined;

const value = ref.deref();
if (value === undefined) {
// Объект уже собран GC
this.cache.delete(key);
return undefined;
}
return value;
}
}

6. Инструменты для обнаружения утечек:

┌─────────────────────────────────────────────────────────────┐
│ ИНСТРУМЕНТЫ ДИАГНОСТИКИ │
├─────────────────────────────────────────────────────────────┤
│ │
│ Chrome DevTools: │
│ ───────────────── │
│ • Memory → Heap Snapshot │
│ → Снимок памяти, поиск detached DOM-элементов │
│ │
│ • Memory → Allocation Timeline │
│ → Запись аллокаций в реальном времени │
│ → Видно, какие объекты не освобождаются │
│ │
│ • Performance → Memory checkbox │
│ → График использования памяти │
│ → Если линия идёт вверх — утечка! │
│ │
│ • console.memory (non-standard) │
│ → Текущее использование памяти │
│ │
│ Метрики для отслеживания: │
│ ───────────────────────── │
│ • usedJSHeapSize — используемая память │
│ • totalJSHeapSize — общий размер кучи │
│ • jsHeapSizeLimit — лимит памяти │
│ │
└─────────────────────────────────────────────────────────────┘

7. Пример мониторинга памяти:

// Мониторинг утечек в продакшене
class MemoryMonitor {
constructor(thresholdMB = 100) {
this.threshold = thresholdMB * 1024 * 1024;
this.measurements = [];
}

measure() {
if (performance.memory) {
const used = performance.memory.usedJSHeapSize;
this.measurements.push({
time: Date.now(),
usedMB: Math.round(used / 1024 / 1024),
});

if (used > this.threshold) {
console.warn(`⚠️ Memory threshold exceeded: ${Math.round(used / 1024 / 1024)}MB`);
this.reportLeak();
}
}
}

reportLeak() {
const recent = this.measurements.slice(-10);
const growing = recent.every(
(m, i) => i === 0 || m.usedMB >= recent[i - 1].usedMB
);

if (growing) {
console.error('🔴 Memory leak detected! Continuously growing:', recent);
}
}

start(intervalMs = 5000) {
this.intervalId = setInterval(() => this.measure(), intervalMs);
}

stop() {
clearInterval(this.intervalId);
}
}

// Использование:
const monitor = new MemoryMonitor(200); // алерт при 200MB
monitor.start();

Итог: Основные принципы предотвращения утеков памяти: всегда удаляйте обработчики событий при удалении элементов, очищайте таймеры, обнуляйте ссылки на удалённые объекты, используйте WeakMap/WeakRef для кеширования. Для диагностики используйте Chrome DevTools → Memory → Heap Snapshot ищите detached DOM-элементы — это главный индикатор утечек.

Вопрос 62. В чём различие между тегами article и section?

Таймкод: 01:10:15

Ответ собеседования: Правильный. Section — это контейнер для крупных смысловых блоков страницы (список товаров, раздел контактов, раздел сотрудников). Article — для мелких самостоятельных блоков (пост, комментарий).

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

Разница между <article> и <section> — один из самых частых вопросов на собеседованиях по HTML-семантике. Ключевое отличие — самостоятельность контента.

1. Ключевое правило:

┌─────────────────────────────────────────────────────────────────┐
│ │
│ <article> — контент САМОСТОЯТЕЛЕН │
│ Если вырезать его со страницы — смысл сохранится │
│ Пример: пост в блоге, комментарий, карточка товара │
│ │
│ <section> — тематическая ГРУППИРОВКА │
│ Если вырезать со страницы — смысл теряется │
│ Пример: "О компании", "Наши услуги", "Контакты" │
│ │
└─────────────────────────────────────────────────────────────────┘

2. Тест на самостоятельность:

Вопрос: "Можно ли этот блок вырвать из контекста страницы,
и он всё ещё будет иметь смысл?"

ДА → <article>
НЕТ → <section>

3. Примеры использования:

<!-- Страница блога -->
<body>

<!-- Section: группирует все посты -->
<section class="blog-posts">
<h2>Последние статьи</h2>

<!-- Article: каждый пост самостоятелен -->
<article>
<h3>Как изучить Go за 30 дней</h3>
<p>Содержание статьи...</p>
<footer>
<time datetime="2024-01-15">15 января 2024</time>
</footer>
</article>

<!-- Article: другой пост, тоже самостоятелен -->
<article>
<h3>Паттерны проектирования в Go</h3>
<p>Содержание статьи...</p>
</article>
</section>

<!-- Section: контактная информация -->
<section class="contacts">
<h2>Контакты</h2>
<p>Телефон: +7 (999) 123-45-67</p>
<p>Email: info@example.com</p>
</section>

<!-- Section: список сотрудников -->
<section class="team">
<h2>Наша команда</h2>

<!-- Каждый сотрудник — самостоятельная единица -->
<article class="employee-card">
<h3>Иван Петров</h3>
<p>Go-разработчик</p>
</article>

<article class="employee-card">
<h3>Мария Сидорова</h3>
<p>Frontend-разработчик</p>
</article>
</section>

</body>

4. Вложенность — article внутри section и наоборот:

<!-- ✅ Article внутри section — когда группируем самостоятельные блоки -->
<section class="news-feed">
<h2>Новости</h2>

<article>
<h3>Запуск нового продукта</h3>
<p>Текст новости...</p>
</article>

<article>
<h3>Открытие офиса в Берлине</h3>
<p>Текст новости...</p>
</article>
</section>

<!-- ✅ Section внутри article — когда статья имеет логические разделы -->
<article class="long-read">
<h2>Полное руководство по Docker</h2>

<section>
<h3>Установка Docker</h3>
<p>Шаги установки...</p>
</section>

<section>
<h3>Основные команды</h3>
<p>docker run, docker build...</p>
</section>

<section>
<h3>Docker Compose</h3>
<p>Оркестрация контейнеров...</p>
</section>
</article>

5. Сравнительная таблица:

┌──────────────────────┬──────────────────────┬──────────────────────┐
│ Критерий │ <article> │ <section> │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ Самостоятельность │ Да, имеет смысл │ Нет, часть общего │
│ │ сам по себе │ контекста │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ Можно распечатать │ Да │ Нет смысла отдельно │
│ отдельно │ │ │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ RSS/Atom ленты │ Да, включается │ Нет │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ Оглавление (TOC) │ Нет │ Да, может иметь │
│ │ │ заголовок в TOC │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ Типичный контент │ Пост, комментарий, │ "О нас", "Услуги", │
│ │ карточка товара, │ "Команда", "Отзывы" │
│ │ виджет │ │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ Может содержать │ section │ article и section │
│ внутри │ │ │
├──────────────────────┼──────────────────────┼──────────────────────┤
│ Обязательный │ Желательно │ Желательно │
│ заголовок │ (для семантики) │ (для семантики) │
└──────────────────────┴──────────────────────┴──────────────────────┘

6. Пример интернет-магазина:

<body>

<!-- Section: каталог товаров -->
<section class="catalog">
<h2>Каталог</h2>

<!-- Article: каждая карточка — самостоятельна -->
<article class="product-card">
<h3>MacBook Pro 14"</h3>
<img src="macbook.jpg" alt="MacBook Pro">
<p class="price">199 990 ₽</p>
<button>В корзину</button>
</article>

<article class="product-card">
<h3>iPhone 15 Pro</h3>
<img src="iphone.jpg" alt="iPhone 15 Pro">
<p class="price">129 990 ₽</p>
<button>В корзину</button>
</article>
</section>

<!-- Section: информация о доставке -->
<section class="delivery-info">
<h2>Доставка</h2>
<p>Бесплатная доставка от 5000 ₽</p>
<p>Срок доставки: 1-3 дня</p>
</section>

<!-- Section: отзывы -->
<section class="reviews">
<h2>Отзывы</h2>

<!-- Article: каждый отзыв самостоятелен -->
<article class="review">
<h4>Алексей К.</h4>
<p class="rating">★★★★★</p>
<p>Отличный магазин, быстрая доставка!</p>
<time datetime="2024-03-10">10 марта 2024</time>
</article>

<article class="review">
<h4>Ольга М.</h4>
<p class="rating">★★★★☆</p>
<p>Хороший товар, но упаковка помялась.</p>
<time datetime="2024-03-08">8 марта 2024</time>
</article>
</section>

</body>

7. Когда использовать просто div:

<!-- div — когда нет семантического смысла -->
<!-- Только для стилизации/группировки -->

<!-- ✅ div как стилевой контейнер -->
<div class="grid-container">
<article>...</article>
<article>...</article>
</div>

<!-- ✅ div как обёртка для скриптов/стилей -->
<div class="modal-overlay" id="modal">
<article class="modal-content">
<h2>Подписка на рассылку</h2>
<form>...</form>
</article>
</div>

<!-- ❌ Неправильно: использовать div вместо article/section -->
<div class="post"> <!-- Лучше: <article> -->
<h3>Заголовок поста</h3>
<p>Текст...</p>
</div>

Итог: Запомните простое правило — <article> для контента, который самостоятелен (пост, комментарий, карточка товара, виджет), а <section> для тематической группировки контента ("О компании", "Услуги", "Контакты"). Если сомневаетесь — задайте вопрос: «Будет ли этот блок иметь смысл, если его вырвать из контекста страницы?». Да → <article>, нет → <section>.