Mock Собеседование для Junior Frontend разработчика
Сегодня мы разберём моковое техническое собеседование с кандидатом на позицию фронтенд-разработчика: инвьюер проверяет знания JavaScript, React и навыки решения алгоритмических задач. В ходе беседы кандидат демонстрирует базовое понимание ключевых концепций, но испытывает трудности с лайфкодингом и глубоким объяснением деталей. В финале инвьюер даёт обратную связь, рекомендуя кандидату усиленно прокачивать практические навыки и систематизировать подготовку к собеседованиям.
Вопрос 1. Что такое REST и какие HTTP-методы ты знаешь?
Таймкод: 00:00:44
Ответ собеседника: Неполный. Кандидат описал REST как работу с методами GET, POST, PUT, PATCH, DELETE, но не смог назвать принципы REST-архитектуры (их шесть). Про методы рассказал: GET — получение данных, POST — отправка данных, PUT — полная замена объекта, PATCH — изменение отдельного свойства объекта, DELETE — удаление. Разницу между PUT и PATCH объяснил верно: PUT перезаписывает весь объект, PATCH меняет только указанное свойство.
Правильный ответ:
Что такое REST
REST (Representational State Transfer) — это архитектурный стиль взаимодействия компонентов распределённой системы, описанный Роем Филдингом в 2000 году в его диссертации. Это не протокол и не стандарт, а набор ограничений и принципов, которые при соблюдении позволяют создавать масштабируемые, надёжные и простые в поддержке веб-сервисы.
Шесть принципов REST-архитектуры
1. Клиент-серверная архитектура (Client-Server) Разделение ответственности между клиентом (пользовательский интерфейс, хранение состояния сессии) и сервером (хранение данных, бизнес-логика). Это позволяет компонентам эволюционировать независимо друг от друга.
2. Отсутствие состояния (Stateless) Каждый запрос от клиента к серверу должен содержать всю информацию, необходимую для его обработки. Сервер не хранит состояние клиента между запросами. Сессионное состояние целиком хранится на стороне клиента. Это упрощает масштабирование, потому что любой сервер в кластере может обработать любой запрос.
3. Кэшируемость (Cacheable) Ответы сервера должны явно или неявно маркироваться как кэшируемые или некэшируемые. Если ответ кэшируемый, клиент получает право повторно использовать эти данные для последующих эквивалентных запросов. Это снижает нагрузку на сервер и улучшает производительность.
4. Единообразный интерфейс (Uniform Interface) Это ключевое ограничение, которое упрощает архитектуру всей системы. Оно включает четыре подограничения:
- Идентификация ресурсов — каждый ресурс идентифицируется через URI (например,
/api/users/42) - Манипуляция ресурсами через представления — клиент работает с представлениями ресурсов (JSON, XML), отправляя их серверу
- Самоописывающие сообщения — каждое сообщение содержит достаточно информации для его обработки (Content-Type, HTTP-методы, статус-коды)
- Гипермедиа как движок состояния приложения (HATEOAS) — ответы сервера содержат ссылки на связанные ресурсы и доступные действия
5. Многоуровневая система (Layered System) Архитектура может состоять из иерархических слоёв, при этом каждый компонент видит только непосредственно примыкающий слой. Это позволяет размещать балансировщики, кэши, шлюзы, системы безопасности между клиентом и сервером без изменения клиентского кода.
6. Код по требованию (Code on Demand) — необязательное ограничение Сервер может расширять функциональность клиента, передавая исполняемый код (например, JavaScript). Это единственное необязательное ограничение в REST.
Основные HTTP-методы в REST API
GET — получение ресурса или списка ресурсов. Идемпотентный, безопасный метод, не изменяющий состояние сервера. Примеры:
GET /api/users— список пользователейGET /api/users/42— конкретный пользовательGET /api/users?page=2&limit=10— пагинация
POST — создание нового ресурса или выполнение операции, не попадающей под другие методы. Не идемпотентный (повторный вызов создаст дубликат). Пример:
POST /api/users— создание нового пользователя
PUT — полная замена (обновление) ресурса по указанному URI. Идемпотентный метод. Если ресурса не существует, может быть создан. Клиент отправляет полное представление ресурса. Пример:
PUT /api/users/42— полная замена всех полей пользователя
// Пример PUT-обработчика в Go
func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := vars["id"]
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
user.ID = userID
// Валидация: все обязательные поля должны быть заполнены
if err := h.validateFullUser(user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := h.repo.Save(user); err != nil {
http.Error(w, "Failed to save user", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(user)
}
PATCH — частичное обновление ресурса. Клиент отправляет только те поля, которые нужно изменить. Формат может быть разным: JSON Merge Patch (RFC 7396) или JSON Patch (RFC 6902). Пример:
PATCH /api/users/42— изменение отдельных полей пользователя
// Пример PATCH-обработчика с JSON Merge Patch
func (h *UserHandler) PatchUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := vars["id"]
existingUser, err := h.repo.FindByID(userID)
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
var patch map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Применяем только переданные поля
if name, ok := patch["name"]; ok {
existingUser.Name = name.(string)
}
if email, ok := patch["email"]; ok {
existingUser.Email = email.(string)
}
if err := h.repo.Save(existingUser); err != nil {
http.Error(w, "Failed to save user", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(existingUser)
}
DELETE — удаление ресурса. Идемпотентный метод (повторное удаление того же ресурса возвращает тот же результат — ресурс не найден). Пример:
DELETE /api/users/42— удаление пользователя
Дополнительные HTTP-методы
HEAD — аналогичен GET, но возвращает только заголовки без тела ответа. Используется для проверки существования ресурса, получения метаданных, проверки кэширования.
OPTIONS — возвращает список поддерживаемых HTTP-методов для ресурса. Используется в механизме CORS (preflight-запросы). Пример:
OPTIONS /api/users→ ответ с заголовкомAllow: GET, POST, HEAD, OPTIONS
TRACE — выполняет эхо-тест обратного пути по пути к целевому ресурсу. Используется для диагностики. В продакшене часто отключается из соображений безопасности.
CONNECT — устанавливает туннель к серверу, идентифицированному целевым ресурсом. Используется для HTTPS через прокси.
Важные свойства HTTP-методов
Идемпотентность — метод идемпотентный, если повторные одинаковые запросы дают тот же результат, что и один запрос. Идемпотентные: GET, PUT, DELETE, HEAD, OPTIONS. Не идемпотентный: POST. PATCH формально не гарантируется идемпотентным, хотя на практике часто реализуется как идемпотентный.
Безопасность — безопасный метод не изменяет состояние ресурса на сервере. Безопасные: GET, HEAD, OPTIONS. Небезопасные: POST, PUT, PATCH, DELETE.
HTTP-статус-коды в REST
В REST API корректное использование HTTP-статус-кодов — важная часть единого интерфейса:
200 OK— успешный GET, PUT, PATCH, DELETE201 Created— успешный POST, ресурс создан204 No Content— успешный запрос без тела ответа (часто для DELETE)301/302— перенаправление400 Bad Request— некорректный запрос от клиента401 Unauthorized— требуется аутентификация403 Forbidden— доступ запрещён404 Not Found— ресурс не найден405 Method Not Allowed— метод не поддерживается для данного ресурса409 Conflict— конфликт с текущим состоянием ресурса422 Unprocessable Entity— семантическая ошибка в данных429 Too Many Requests— превышен лимит запросов500 Internal Server Error— внутренняя ошибка сервера
Пример RESTful API на Go
package main
import (
"encoding/json"
"log"
"net/http"
"strings"
"github.com/gorilla/mux"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type UserHandler struct {
store map[string]User
}
func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
users := make([]User, 0, len(h.store))
for _, u := range h.store {
users = append(users, u)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
userID := mux.Vars(r)["id"]
user, exists := h.store[userID]
if !exists {
http.Error(w, `{"error": "user not found"}`, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, `{"error": "invalid body"}`, http.StatusBadRequest)
return
}
user.ID = generateID()
h.store[user.ID] = user
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
userID := mux.Vars(r)["id"]
if _, exists := h.store[userID]; !exists {
http.Error(w, `{"error": "user not found"}`, http.StatusNotFound)
return
}
delete(h.store, userID)
w.WriteHeader(http.StatusNoContent)
}
func generateID() string {
return "usr_" + strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
}
func main() {
handler := &UserHandler{store: make(map[string]User)}
r := mux.NewRouter()
r.HandleFunc("/api/users", handler.ListUsers).Methods("GET")
r.HandleFunc("/api/users/{id}", handler.GetUser).Methods("GET")
r.HandleFunc("/api/users", handler.CreateUser).Methods("POST")
r.HandleFunc("/api/users/{id}", handler.DeleteUser).Methods("DELETE")
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
Чем REST отличается от RESTful
Строго говоря, REST — это архитектурный стиль с шестью ограничениями. RESTful — это API, которое следует всем принципам REST. На практике большинство так называемых REST-API являются RESTful лишь частично (например, не реализуют HATEOAS). Такие API иногда называют REST-like или HTTP API.
Вопрос 2. Какие ещё HTTP-методы существуют, например OPTIONS и HEAD?
Таймкод: 00:02:13
Ответ собеседника: Неполный. Про OPTIONS кандидат не смог ничего сказать. HEAD описал как заголовок, куда можно писать формат данных, что неверно — HEAD возвращает только заголовки ответа без тела.
Правильный ответ:
HTTP-метод HEAD
HEAD — это HTTP-метод, аналогичный GET, но сервер возвращает только заголовки ответа без тела (body). Тело ответа при HEAD-запросе никогда не отправляется, даже если заголовок Content-Length указывает на его размер.
Практические применения HEAD:
- Проверка существования ресурса — можно узнать, существует ли ресурс, не загружая его содержимое
- Проверка изменений — через заголовки Last-Modified, ETag можно понять, изменился ли ресурс с момента последнего обращения
- Получение метаданных — Content-Type, Content-Length и другие заголовки без передачи данных
- Проверка кэширования — определить, актуален ли кэш клиента
// Пример: проверка существования ресурса через HEAD
func checkResourceExists(url string) (bool, error) {
resp, err := http.Head(url)
if err != nil {
return false, err
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK, nil
}
// Пример: проверка размера файла без его загрузки
func getRemoteFileSize(url string) (int64, error) {
resp, err := http.Head(url)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return resp.ContentLength, nil
}
HTTP-метод OPTIONS
OPTIONS — это HTTP-метод, который запрашивает у сервера информацию о доступных методах и возможностях для конкретного ресурса или сервера в целом.
Основные применения OPTIONS:
1. CORS Preflight-запросы Это наиболее частое применение OPTIONS на практике. Когда браузер отправляет cross-origin запрос с нестандартными методами или заголовками, он сначала отправляет OPTIONS-запрос (preflight), чтобы проверить, разрешён ли такой запрос.
// Пример CORS-обработки с OPTIONS
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://example.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400") // кэш preflight на 24 часа
// Preflight-запрос — отвечаем сразу, не передавая дальше
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
2. Определение поддерживаемых методов Сервер может возвращать заголовок Allow со списком доступных методов:
OPTIONS /api/users/42 HTTP/1.1
HTTP/1.1 204 No Content
Allow: GET, PUT, PATCH, DELETE, OPTIONS
3. Запрос к корневому URL сервера
OPTIONS с символом * запрашивает возможности сервера в целом, а не конкретного ресурса:
OPTIONS * HTTP/1.1
HTTP-метод TRACE
TRACE выполняет эхо-тест обратного пути по пути к целевому ресурсу. Сервер получает запрос и возвращает его тело обратно клиенту без изменений. Используется для диагностики и отладки — чтобы увидеть, как промежуточные прокси и шлюзы модифицируют запрос.
На практике TRACE почти всегда отключён на продакшене из-за уязвимости Cross-Site Tracing (XST), которая может использоваться для кражи аутентификационных данных.
HTTP-метод CONNECT
CONNECT устанавливает туннель к серверу, идентифицированному целевым ресурсом. Основное применение — HTTPS через прокси-сервер. Когда браузер подключается к HTTPS-сайту через прокси, он отправляет CONNECT-запрос, прокси устанавливает TCP-соединение с целевым сервером, и дальнейшая коммуникация идёт напрямую через этот туннель.
CONNECT example.com:443 HTTP/1.1
Полная таблица HTTP-методов
| Метод | Идемпотентный | Безопасный | Тело запроса | Тело ответа |
|---|---|---|---|---|
| GET | Да | Да | Нет | Да |
| POST | Нет | Нет | Да | Да |
| PUT | Да | Нет | Да | Да |
| PATCH | Нет* | Нет | Да | Да |
| DELETE | Да | Нет | Нет | Да |
| HEAD | Да | Да | Нет | Нет |
| OPTIONS | Да | Да | Нет | Да |
| TRACE | Да | Да | Нет | Да |
| CONNECT | Нет | Нет | Да | Да |
*PATCH формально не гарантируется идемпотентным, но может быть реализован как идемпотентный.
Пример полного обработчика OPTIONS в Go
func (h *UserHandler) Options(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Allow", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
w.WriteHeader(http.StatusNoContent)
}
Вопрос 3. Какие HTTP-коды ответов ты знаешь?
Таймкод: 00:02:33
Ответ собеседника: Неполный. Назвал: 200 — всё ок, 400-е — ошибка клиента, 500-е — ошибка сервера, 304 — Not Modified. Не смог объяснить, что означают 300-е коды (редиректы). Не назвал конкретные популярные коды, такие как 201 Created, 401 Unauthorized, 403 Forbidden, 404 Not Found и другие.
Правильный ответ:
HTTP-коды состояния (status codes) делятся на пять классов, определяемых первой цифрой. Каждый класс обозначает общий тип ответа, а конкретные коды уточняют причину.
1xx — Информационные (Informational)
Сервер получил запрос и продолжает обработку. Эти коды редко используются в повседневной разработке API.
100 Continue— сервер получил заголовки запроса и клиент может отправлять тело. Используется при больших загрузках данных101 Switching Protocols— сервер согласен переключить протокол (например, с HTTP на WebSocket)103 Early Hints— предварительные подсказки для предзагрузки ресурсов (используется с Link rel=preload)
2xx — Успешные (Success)
Запрос был успешно получен, понят и обработан.
200 OK— стандартный успешный ответ. Используется для успешных GET, PUT, PATCH, DELETE запросов201 Created— ресурс успешно создан. Стандартный ответ на POST-запрос. Желательно включать заголовок Location с URL созданного ресурса202 Accepted— запрос принят на обработку, но она ещё не завершена. Используется для асинхронных операций (например, запуск длительного фонового задания)204 No Content— запрос успешно обработан, но тело ответа отсутствует. Типичен для успешного DELETE206 Partial Content— частичный ответ. Используется при загрузке файлов по частям (Range-запросы), что позволяет реализовать докачку
// Пример: возврат разных 2xx кодов в Go
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, `{"error": "invalid body"}`, http.StatusBadRequest)
return
}
id, err := h.service.CreateUser(user)
if err != nil {
http.Error(w, `{"error": "creation failed"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Location", fmt.Sprintf("/api/users/%s", id))
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"id": id})
}
func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
userID := mux.Vars(r)["id"]
if err := h.service.DeleteUser(userID); err != nil {
http.Error(w, `{"error": "deletion failed"}`, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent) // 204 — тело не отправляем
}
3xx — Перенаправления (Redirection)
Клиенту необходимо выполнить дополнительные действия для завершения запроса.
301 Moved Permanently— ресурс окончательно перемещён на новый URL. Поисковые системы обновляют индекс. Браузеры кэшируют этот редирект302 Found— временное перенаправление. Исторически браузеры меняли метод с POST на GET (что было нежелательно)303 See Other— результат нужно получить через GET. Используется после POST для перенаправления на страницу результата (Post/Redirect/Get pattern)304 Not Modified— ресурс не изменился с момента последнего запроса. Используется с условными заголовками If-Modified-Since или If-None-Match (ETag). Экономит трафик — сервер не отправляет тело повторно307 Temporary Redirect— временное перенаправление с гарантией сохранения HTTP-метода (POST останется POST)308 Permanent Redirect— постоянное перенаправление с гарантией сохранения HTTP-метода
// Пример: условный запрос с 304 Not Modified
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
userID := mux.Vars(r)["id"]
user, etag, err := h.service.GetUserWithETag(userID)
if err != nil {
http.Error(w, `{"error": "not found"}`, http.StatusNotFound)
return
}
// Проверяем If-None-Match заголовок клиента
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("ETag", etag)
json.NewEncoder(w).Encode(user)
}
4xx — Ошибки клиента (Client Error)
Запрос содержит ошибку или не может быть выполнен по вине клиента.
400 Bad Request— сервер не может обработать запрос из-за синтаксической ошибки или невалидных данных. Общий код для ошибок валидации401 Unauthorized— требуется аутентификация. Клиент не предоставил учётные данные или они недействительны. Обязательно включать заголовок WWW-Authenticate403 Forbidden— доступ запрещён. Клиент аутентифицирован, но не имеет прав на данный ресурс. Сервер знает, кто клиент, но отказывает в доступе404 Not Found— ресурс не найден по указанному URI. Самый известный код ошибки405 Method Not Allowed— HTTP-метод не поддерживается для данного ресурса. Обязательно включать заголовок Allow со списком разрешённых методов409 Conflict— конфликт с текущим состоянием ресурса. Типичный пример — попытка создания дубликата уникального поля410 Gone— ресурс был удалён и больше недоступен. Клиент должен прекратить запросы к этому ресурсу413 Payload Too Large— тело запроса превышает допустимый размер415 Unsupported Media Type— формат данных не поддерживается (например, клиент отправляет XML, а сервер принимает только JSON)422 Unprocessable Entity— запрос синтаксически корректен, но содержит семантические ошибки (ошибки бизнес-валидации)429 Too Many Requests— клиент превысил лимит запросов (rate limiting). Желательно включать заголовок Retry-After
// Пример: обработка различных 4xx ошибок
type APIError struct {
Status int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
Details map[string]string `json:"details,omitempty"`
}
func (e APIError) Error() string {
return e.Message
}
func writeError(w http.ResponseWriter, err APIError) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(err.Status)
json.NewEncoder(w).Encode(err)
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, APIError{
Status: http.StatusBadRequest,
Code: "INVALID_JSON",
Message: "Request body contains invalid JSON",
})
return
}
// Валидация бизнес-логики
validationErrors := validateRequest(req)
if len(validationErrors) > 0 {
writeError(w, APIError{
Status: http.StatusUnprocessableEntity,
Code: "VALIDATION_ERROR",
Message: "Request validation failed",
Details: validationErrors,
})
return
}
user, err := h.service.CreateUser(req)
if err != nil {
if errors.Is(err, ErrDuplicateEmail) {
writeError(w, APIError{
Status: http.StatusConflict,
Code: "DUPLICATE_EMAIL",
Message: "User with this email already exists",
})
return
}
writeError(w, APIError{
Status: http.StatusInternalServerError,
Code: "INTERNAL_ERROR",
Message: "Failed to create user",
})
return
}
w.Header().Set("Location", fmt.Sprintf("/api/users/%s", user.ID))
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
// Middleware для rate limiting
func rateLimitMiddleware(requestsPerMinute int) func(http.Handler) http.Handler {
limiter := rate.NewLimiter(rate.Limit(requestsPerMinute/60), requestsPerMinute)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
w.Header().Set("Retry-After", "60")
writeError(w, APIError{
Status: http.StatusTooManyRequests,
Code: "RATE_LIMIT_EXCEEDED",
Message: "Too many requests. Please try again later.",
})
return
}
next.ServeHTTP(w, r)
})
}
}
5xx — Ошибки сервера (Server Error)
Сервер не смог обработать корректный запрос по своей вине.
500 Internal Server Error— общая ошибка сервера. Используется, когда конкретная причина неизвестна или не должна быть раскрыта клиенту501 Not Implemented— сервер не поддерживает функциональность, необходимую для обработки запроса502 Bad Gateway— сервер, выступающий в роли шлюза или прокси, получил недопустимый ответ от вышестоящего сервера503 Service Unavailable— сервер временно недоступен (перегрузка, техническое обслуживание). Желательно включать заголовок Retry-After504 Gateway Timeout— сервер-шлюз не получил ответ от вышестоящего сервера за отведённое время
// Пример: централизованная обработка 5xx ошибок
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
// Логируем панику с полным стектрейсом
log.Printf("PANIC: %v\n%s", rec, debug.Stack())
// Клиенту отправляем только общую ошибку
writeError(w, APIError{
Status: http.StatusInternalServerError,
Code: "INTERNAL_ERROR",
Message: "An unexpected error occurred",
})
}
}()
next.ServeHTTP(w, r)
})
}
Ключевые различия между похожими кодами
401 vs 403: 401 означает «я не знаю, кто вы» (не аутентифицирован), 403 означает «я знаю, кто вы, но вам сюда нельзя» (недостаточно прав).
301 vs 302 vs 307 vs 308: 301 и 308 — постоянные редиректы, 302 и 307 — временные. 307 и 308 гарантируют сохранение HTTP-метода, а 301 и 302 исторически могли менять POST на GET.
400 vs 422: 400 — синтаксическая ошибка (невалидный JSON, неверный тип данных). 422 — синтаксически корректный запрос с семантическими ошибками (email в неправильном формате, возраст отрицательный).
Вопрос 4. Чем отличаются ключевые слова var, let и const в JavaScript?
Таймкод: 00:03:11
Ответ собеседника: Правильный. Кандидат объяснил, что var — объявление переменной с функциональной областью видимости, let — изменяемая переменная с блочной областью видимости, const — неизменяемая переменная с блочной областью видимости. Верно указал, что основное отличие var от let/const — тип области видимости.
Правильный ответ:
Кандидат ответил верно. Вопрос относится к JavaScript и не связан напрямую с темой интервью на golang-разработчика, поэтому дополним кратко ключевые нюансы.
Область видимости (Scope)
var имеет функциональную область видимости — переменная видна во всей функции, независимо от блока, в котором объявлена. let и const имеют блочную область видимости — переменная видна только внутри блока {}, в котором объявлена.
Hoisting (всплытие)
Переменные var поднимаются на верх своей области видимости и инициализируются значением undefined. Переменные let и const тоже поднимаются, но не инициализируются — обращение к ним до объявления вызывает ReferenceError (temporal dead zone).
Переобъявление
var позволяет переобъявлять переменную в той же области видимости. let и const — нет, это вызовет SyntaxError.
Глобальный объект
При объявлении var в глобальной области переменная становится свойством глобального объекта (window в браузере). let и const не создают свойств глобального объекта.
const — неизменная привязка, не неизменное значение
const запрещает переприсваивание переменной, но не делает значение неизменным. Объект или массив, объявленный через const, можно мутировать — нельзя только присвоить ему новый объект.
Это вопрос по JavaScript, и для позиции golang-разработчика он является дополнительным.
Вопрос 5. Что такое Temporal Dead Zone (TDZ)?
Таймкод: 00:04:01
Ответ собеседника: Правильный. Кандидат верно описал TDZ как ошибку ReferenceError, возникающую при обращении к переменной, объявленной через let или const, до её объявления.
Правильный ответ:
Кандидат ответил верно. Это вопрос по JavaScript, дополнительный для позиции golang-разработчика. Дополним деталями.
Temporal Dead Zone (TDZ) — это период времени между началом области видимости переменной и строкой её объявления, в течение которого обращение к переменной вызывает ReferenceError.
Почему это происходит
Переменные let и const поднимаются (hoisting) так же, как и var, но в отличие от var они не инициализируются значением undefined. Переменная существует в памяти, но движок запрещает к ней обращаться до выполнения строки объявления.
// Пример TDZ
console.log(a); // undefined — var инициализирован undefined
var a = 10;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;
console.log(c); // ReferenceError
const c = 30;
TDZ работает и для блоков
let x = 1;
{
console.log(x); // ReferenceError — x ещё не инициализирован в этом блоке
let x = 2;
}
Внутренний let x создаёт новую переменную в блочной области видимости, и TDZ для неё начинается с начала блока. Обращение к x до let x = 2 попадает в TDZ, даже если во внешней области есть переменная с тем же именем.
TDZ и typeof
Даже безопасный оператор typeof не спасает от TDZ:
console.log(typeof undeclaredVar); // "undefined" — переменная не существует
console.log(typeof tdzVar); // ReferenceError — переменная существует, но в TDZ
let tdzVar = 42;
Практическое значение
TDZ — это не баг, а осознанное решение, которое помогает выявлять ошибки на этапе разработки. Код, обращающийся к переменной до её объявления, заведомо содержит логическую ошибку, и TDZ делает её явной.
Для golang-разработчика это вопрос из области общей эрудиции в веб-разработке.
Вопрос 6. Что такое hoisting (поднятие) в JavaScript?
Таймкод: 00:04:34
Ответ собеседника: Правильный. Кандидат верно описал hoisting как механизм, при котором объявления переменных и функций всплывают в верхнюю область видимости.
Правильный ответ:
Кандидат ответил верно. Дополним деталями для более глубокого понимания.
Hoisting (поднятие) — это поведение JavaScript, при котором объявления переменных и функций перемещаются на верх своей области видимости перед выполнением кода. Важно понимать, что поднимается именно объявление, а не инициализация.
Разные типы hoisting
Объявление функций (Function Declaration) поднимается целиком — и объявление, и тело функции. Поэтому функцию можно вызвать до её определения в коде.
greet(); // работает — "Hello!"
function greet() {
console.log("Hello!");
}
var поднимается и инициализируется значением undefined.
console.log(x); // undefined
var x = 5;
let и const поднимаются, но не инициализируются — попадание в TDZ.
console.log(y); // ReferenceError
let y = 10;
Function Expression и Arrow Functions ведут себя как переменные — поднимается только объявление переменной.
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
console.log("Hi!");
};
Здесь sayHi поднимается как var и равен undefined. Вызов undefined() вызывает TypeError, а не ReferenceError.
Порядок приоритета при hoisting
- Объявления функций поднимаются первыми
- Объявления переменных поднимаются следом
- Если функция и переменная имеют одно имя, объявление функции имеет приоритет над
var, но присваиваниеvarперезапишет его
console.log(typeof foo); // "function"
var foo = 5;
console.log(typeof foo); // "number"
function foo() {}
Практический совет
Hoisting — это не буквальное перемещение кода движком. Это концептуальная модель, объясняющая поведение на этапе компиляции: движок сначала обходит код и регистрирует все объявления, а затем выполняет код. Для читаемости рекомендуется всегда объявлять переменные и функции в начале их области видимости.
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
Вопрос 7. Какие способы объявления функций существуют в JavaScript и в чём разница между Function Declaration и стрелочными функциями?
Таймкод: 00:05:03
Ответ собеседника: Неполный. Кандидат назвал Function Declaration и Function Expression через const со стрелочной функцией. Верно указал различия: стрелочная функция не имеет своего this, не подвержена hoisting, не имеет псевдомассива arguments. Не упомянул, что стрелочную функцию нельзя использовать как конструктор с new.
Правильный ответ:
Кандидат дал в целом хороший ответ, но не полностью раскрыл тему. Дополним.
Способы объявления функций в JavaScript
1. Function Declaration (объявление функции)
function greet(name) {
return `Hello, ${name}!`;
}
Поднимается целиком через hoisting, имеет имя, доступна до объявления в коде.
2. Function Expression (функциональное выражение)
const greet = function(name) {
return `Hello, ${name}!`;
};
Не поднимается целиком — поднимается только переменная. Может быть анонимной или именованной.
3. Arrow Function (стрелочная функция)
const greet = (name) => `Hello, ${name}!`;
Краткий синтаксис, лексический this, не может быть конструктором.
4. Function Constructor
const greet = new Function('name', 'return "Hello, " + name');
Редко используется, работает как eval — потенциальная угроза безопасности.
5. IIFE (Immediately Invoked Function Expression)
(function() {
console.log("Immediately invoked");
})();
Выполняется сразу после объявления, создаёт изолированную область видимости.
Полный список различий между Function Declaration и Arrow Functions
Контекст this
Function Declaration имеет свой собственный this, который определяется способом вызова. Arrow Function не имеет своего this — она захватывает this из окружающей лексической области видимости (лексический this).
const obj = {
name: "Alice",
// Function Declaration — свой this
greetRegular: function() {
console.log(this.name); // "Alice" — this указывает на obj
},
// Arrow Function — лексический this
greetArrow: () => {
console.log(this.name); // undefined — this из внешней области
}
};
Hoisting
Function Declaration поднимается целиком и доступна до объявления. Arrow Function хранится в переменной и ведёт себя как Function Expression — не доступна до объявления (TDZ для const/let).
Объект arguments
Function Declaration имеет встроенный псевдомассив arguments со всеми переданными аргументами. Arrow Function не имеет arguments.
function regular() {
console.log(arguments); // [1, 2, 3]
}
const arrow = () => {
console.log(arguments); // ReferenceError
};
regular(1, 2, 3);
Использование как конструктор (new)
Function Declaration может использоваться как конструктор с оператором new. Arrow Function не может — вызов new вызовет TypeError.
function Person(name) {
this.name = name;
}
const p = new Person("Alice"); // работает
const PersonArrow = (name) => {
this.name = name;
};
// const p2 = new PersonArrow("Bob"); // TypeError: PersonArrow is not a constructor
Методы объекта prototype
Function Declaration имеет свойство prototype. Arrow Function не имеет prototype, что и делает её непригодной для использования как конструктор.
Метод super
В классах Arrow Function захватывает super из лексической области, а Function Declaration имеет свой super.
Возможность быть генератором
Function Declaration может быть генератором (function*). Arrow Function — нет.
function* generator() {
yield 1;
yield 2;
}
Когда что использовать
- Arrow Functions — для колбэков, map/filter/reduce, функций высшего порядка, когда нужно сохранить лексический this
- Function Declaration — для методов объектов, конструкторов, генераторов, когда нужен собственный this или arguments
- Function Expression — когда нужно присвоить функцию переменной с условным определением
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
Вопрос 8. Что такое IIFE (Immediately Invoked Function Expression)?
Таймкод: 00:05:51
Ответ собеседника: Неправильный. Кандидат перепутал IIFE с рекурсией и не знает, что это функция, которая вызывает сама себя сразу после объявления.
Правильный ответ:
IIFE (Immediately Invoked Function Expression) — это функция в JavaScript, которая выполняется сразу же после своего определения. Название расшифровывается как «немедленно вызываемое функциональное выражение».
Синтаксис
Распространённая форма записи — функция оборачивается в круглые скобки, после которых идут вызывающие скобки:
(function() {
console.log("Выполняется сразу!");
})();
Альтернативная форма:
(function() {
console.log("Выполняется сразу!");
}());
Обе формы работают одинаково, разница — в стиле. Первая форма более распространена.
Зачем нужны скобки вокруг функции
JavaScript интерпретирует ключевое слово function в начале строки как Function Declaration, а объявление функции не может быть немедленно вызвано без имени. Скобки превращают объражение в Function Expression, которое можно вызвать сразу.
// Так не работает — SyntaxError
function() {
console.log("Ошибка");
}();
// Так работает — скобки превращают в выражение
(function() {
console.log("Работает!");
})();
Основное назначение — изоляция области видимости
До появления let, const и модулей (ES6) в JavaScript не было блочной области видимости для переменных (только функциональная). IIFE использовались для создания изолированной области видимости, чтобы переменные не засоряли глобальное пространство имён.
// Без IIFE — переменная попадает в глобальную область
var result = "глобальная";
(function() {
var result = "локальная";
console.log(result); // "локальная"
})();
console.log(result); // "глобальная" — не затронута
Передача аргументов
IIFE может принимать аргументы, как обычная функция:
(function(name, age) {
console.log(`Имя: ${name}, Возраст: ${age}`);
})("Алиса", 30);
Это часто используется для передачи глобальных объектов с коротким именем:
(function($) {
// Здесь $ гарантированно ссылается на jQuery,
// даже если глобальный $ перезаписан
$(document).ready(function() {
// ...
});
})(jQuery);
Возврат значения
IIFE может возвращать значение, которое присваивается переменной:
const config = (function() {
const apiKey = "secret-key-123";
const apiUrl = "https://api.example.com";
return {
getApiUrl: function() { return apiUrl; },
// apiKey остаётся приватным
};
})();
console.log(config.getApiUrl()); // "https://api.example.com"
console.log(config.apiKey); // undefined — приватная переменная
Это классический паттерн модуля (Module Pattern) — один из основных способов создания приватных переменных и инкапсуляции в JavaScript до ES6.
IIFE и стрелочные функции
IIIFE также можно записать через стрелочную функцию:
(() => {
console.log("Стрелочная IIFE");
})();
Современная актуальность
С появлением ES6 модулей, let, const и блочной области видимости, IIFE стали менее необходимы. Модули ES6 сами по себе создают изолированную область видимости. Однако IIFE всё ещё встречаются в:
- Старом коде и библиотеках
- Скриптах, загружаемых напрямую в браузер без сборщика
- Утилитах, где нужно выполнить код один раз с изоляцией
- Сборщиках (webpack, rollup), которые оборачивают модули в IIFE для изоляции
IIFE vs рекурсия
IIFE не является рекурсией. Рекурсия — это вызов функцией самой себе внутри своего тела. IIFE — это однократный немедленный вызов функции сразу после объявления, без повторных вызовов.
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
Вопрос 9. Какие методы массивов ты знаешь?
Таймкод: 00:07:04
Ответ собеседника: Неполный. Кандидат назвал map, reduce, indexOf, findIndex, some, every, find, filter, но не упомянул forEach, push, pop, shift, unshift, slice, splice, concat, join, sort, reverse, flat, flatMap, includes, reduceRight и другие.
Правильный ответ:
Методы массивов в JavaScript можно классифицировать по их действию: одни изменяют исходный массив (mutating), другие возвращают новое значение или массив, не изменяя исходный (non-mutating).
Методы, изменяющие исходный массив (Mutating)
push(...items) — добавляет элемент(ы) в конец массива, возвращает новую длину.
pop() — удаляет и возвращает последний элемент.
unshift(...items) — добавляет элемент(ы) в начало массива, возвращает новую длину.
shift() — удаляет и возвращает первый элемент.
splice(start, deleteCount, ...items) — удаляет, заменяет или вставляет элементы по индексу. Универсальный, но опасный метод.
sort(compareFn) — сортирует массив на месте. По умолчанию сортирует как строки, поэтому для чисел нужна функция сравнения.
reverse() — разворачивает массив на месте.
fill(value, start, end) — заполняет массив указанным значением.
const arr = [3, 1, 4, 1, 5];
arr.sort(); // [1, 1, 3, 4, 5] — строковая сортировка
arr.sort((a, b) => a - b); // числовая сортировка по возрастанию
Методы, возвращающие новый массив или значение (Non-mutating)
map(callback) — преобразует каждый элемент, возвращает новый массив той же длины.
filter(callback) — возвращает новый массив с элементами, прошедшими проверку.
reduce(callback, initialValue) — сворачивает массив в одно значение.
reduceRight(callback, initialValue) — то же, что reduce, но справа налево.
slice(start, end) — возвращает копию части массива без изменения исходного.
concat(...arrays) — объединяет массивы, возвращает новый.
flat(depth) — «выравнивает» вложенные массивы на указанную глубину.
flatMap(callback) — map + flat(1) в одном вызове.
Методы поиска и проверки
find(callback) — возвращает первый элемент, удовлетворяющий условию, или undefined.
findIndex(callback) — возвращает индекс первого найденного элемента или -1.
indexOf(value) — возвращает индекс первого вхождения значения (строгое равенство) или -1.
lastIndexOf(value) — то же, что indexOf, но поиск справа налево.
includes(value) — возвращает true, если значение присутствует в массиве (корректно работает с NaN).
some(callback) — возвращает true, если хотя бы один элемент прошёл проверку.
every(callback) — возвращает true, если все элементы прошли проверку.
Методы итерации
forEach(callback) — выполняет функцию для каждого элемента, всегда возвращает undefined. Не прерывается через break — для раннего выхода лучше использовать some или every.
keys() — возвращает итератор ключей (индексов).
values() — возвращает итератор значений.
entries() — возвращает итератор пар [индекс, значение].
Методы преобразования
join(separator) — объединяет элементы в строку с разделителем.
toString() — преобразует массив в строку через запятую.
toLocaleString() — преобразует в строку с учётом локали.
Пример цепочки методов
const users = [
{ name: "Alice", age: 30, active: true },
{ name: "Bob", age: 17, active: true },
{ name: "Charlie", age: 25, active: false },
];
const result = users
.filter(u => u.active)
.filter(u => u.age >= 18)
.map(u => u.name)
.sort();
// ["Alice"]
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
Вопрос 10. Что такое замыкание (closure) в JavaScript? Приведи пример.
Таймкод: 00:07:44
Ответ собеседника: Правильный. Кандидат верно описал замыкание как способность функции запоминать своё лексическое окружение и привёл корректный пример с вложенной функцией, обращающейся к переменной внешней функции.
Правильный ответ:
Кандидат ответил верно. Дополним деталями и практическими примерами.
Замыкание (closure) — это функция вместе с её лексическим окружением (lexical environment). Замыкание «запоминает» переменные из той области видимости, в которой оно было создано, даже после того как эта внешняя функция завершила выполнение.
Как это работает
Когда функция создаётся, она получает внутреннее свойство [[Environment]], которое ссылается на лексическое окружение, в котором она была создана. Когда функция вызывается, она ищет переменные сначала в своём локальном окружении, а затем — в [[Environment]], поднимаясь по цепочке.
Базовый пример
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
const counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3
Переменная count продолжает жить после завершения outer(), потому что inner сохраняет ссылку на её лексическое окружение.
Практическое применение: приватные переменные
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount <= 0) throw new Error("Amount must be positive");
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.withdraw(200); // 1300
console.log(account.getBalance()); // 1300
console.log(account.balance); // undefined — приватная переменная
Практическое применение: фабрика функций
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Классическая ловушка с циклом и var
// Проблема: все функции ссылаются на одну переменную i
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs[0](); // 3, а не 0
funcs[1](); // 3, а не 1
funcs[2](); // 3, а не 2
// Решение с let: каждую итерацию создаётся новая переменная
var funcsFixed = [];
for (let i = 0; i < 3; i++) {
funcsFixed.push(function() {
console.log(i);
});
}
funcsFixed[0](); // 0
funcsFixed[1](); // 1
funcsFixed[2](); // 2
Замыкание и память
Замыкания препятствуют сборке мусора переменных, на которые они ссылаются. Если замыкание хранится в памяти (например, в виде обработчика событий), то и связанные с ним переменные остаются в памяти. Это может приводить к утечкам памяти, если не освобождать ссылки на замыкания, когда они больше не нужны.
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
Вопрос 11. Что такое рекурсия в JavaScript?
Таймкод: 00:08:39
Ответ собеседника: Правильный. Кандидат верно описал рекурсию как функцию, которая вызывает саму себя.
Правильный ответ:
Кандидат ответил верно, но ответ можно дополнить. Это вопрос из общей области программирования, не специфичный для JavaScript.
Рекурсия — это техника, при которой функция вызывает саму себя для решения задачи путём разбиения её на меньшие подзадачи того же типа.
Два обязательных компонента рекурсии
1. Базовый случай (base case) — условие, при котором рекурсия прекращается. Без него функция будет вызывать саму себя бесконечно, пока стек вызовов не переполнится (stack overflow).
2. Рекурсивный случай (recursive case) — вызов функцией самой себя с изменёнными параметрами, приближающими к базовому случаю.
Пример: факториал
function factorial(n) {
// Базовый случай
if (n <= 1) {
return 1;
}
// Рекурсивный случай
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
// 5 * factorial(4)
// 5 * 4 * factorial(3)
// 5 * 4 * 3 * factorial(2)
// 5 * 4 * 3 * 2 * factorial(1)
// 5 * 4 * 3 * 2 * 1 = 120
Пример: обход дерева
function sumTree(node) {
if (node === null) {
return 0; // базовый случай
}
return node.value + sumTree(node.left) + sumTree(node.right);
}
Стек вызовов и переполнение
Каждый рекурсивный вызов добавляет фрейм в стек вызовов. В JavaScript размер стека ограничен (обычно около 10 000–25 000 фреймов в зависимости от движка). Превышение вызывает RangeError: Maximum call stack size exceeded.
Оптимизация хвостовой рекурсии (Tail Call Optimization)
Если рекурсивный вызов является последней операцией в функции (хвостовой вызов), некоторые движки могут оптимизировать это, повторно используя текущий фрейм стека вместо создания нового. Однако в спецификации ES6 это предусмотрено, а реализовано только в Safari (V8 и SpiderMonkey не поддерживают).
// Хвостовая рекурсия
function factorialTail(n, accumulator = 1) {
if (n <= 1) return accumulator;
return factorialTail(n - 1, n * accumulator); // хвостовой вызов
}
Рекурсия vs итерация
Любую рекурсию можно переписать через итерацию (цикл), и наоборот. Итеративное решение обычно эффективнее по памяти (не использует стек вызовов), но рекурсивное часто проще читать для задач с древовидной или вложенной структурой.
// Итеративная версия факториала
function factorialIterative(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
Это общий вопрос по программированию, актуальный для любого языка, включая Go.
Вопрос 12. Для чего используется ключевое слово this в JavaScript?
Таймкод: 00:08:53
Ответ собеседника: Неполный. Кандидат верно указал, что this используется для работы с контекстом и чаще встречается при работе с объектами, но не раскрыл, как именно определяется значение this в разных контекстах.
Правильный ответ:
this в JavaScript — это специальное ключевое слово, которое ссылается на объект, являющийся текущим контекстом выполнения функции. Значение this определяется не местом объявления функции, а способом её вызова — это одно из ключевых отличий от большинства других языков.
Правила определения this
1. Глобальный контекст
В глобальной области видимости (вне любой функции) this ссылается на глобальный объект: window в браузере, global в Node.js (в strict mode — undefined).
console.log(this); // window (в браузере)
2. Метод объекта
Когда функция вызывается как метод объекта, this ссылается на объект, которому принадлежит метод.
const user = {
name: "Alice",
greet() {
console.log(`Hello, ${this.name}`);
}
};
user.greet(); // "Hello, Alice" — this = user
Важно: если метод сохранить в переменную и вызвать отдельно, this будет потерян.
const greetFn = user.greet;
greetFn(); // "Hello, undefined" — this = undefined (strict mode) или window
3. Обычная функция
При вызове обычной функции (не метода) this зависит от strict mode:
- Без strict mode: this = глобальный объект (window/global)
- С strict mode: this = undefined
function show() {
console.log(this);
}
show(); // window (без strict mode) или undefined (strict mode)
4. Конструктор (new)
При вызове функции через new this ссылается на вновь созданный экземпляр объекта.
function Person(name) {
this.name = name;
}
const p = new Person("Alice");
console.log(p.name); // "Alice"
5. call, apply, bind
Эти методы позволяют явно задать значение this.
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const user = { name: "Alice" };
greet.call(user, "Hello", "!"); // "Hello, Alice!"
greet.apply(user, ["Hi", "."]); // "Hi, Alice."
const boundGreet = greet.bind(user, "Hey");
boundGreet("?"); // "Hey, Alice?"
Различия между ними:
call— вызывает функцию сразу с аргументами через запятуюapply— вызывает функцию сразу с аргументами в виде массиваbind— возвращает новую функцию с привязанным this, не вызывая её сразу
6. Стрелочные функции
Стрелочные функции не имеют собственного this. Они захватывают this из окружающей лексической области видимости на момент создания. Это значение нельзя изменить через call, apply или bind.
const obj = {
name: "Alice",
regularFn: function() {
console.log(this.name); // "Alice"
},
arrowFn: () => {
console.log(this.name); // undefined — this из внешней области
}
};
Это делает стрелочные функции особенно полезными в колбэках, где нужно сохранить контекст:
class Timer {
constructor() {
this.seconds = 0;
}
start() {
// Стрелочная функция сохраняет this = экземпляр Timer
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
}
Приоритет правил
Если несколько правил применимы одновременно, приоритет следующий:
new— самый высокий приоритетcall/apply/bind- Вызов как метода объекта
- Обычный вызов функции — самый низкий
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
Вопрос 13. Расскажи о методах bind, call и apply. Чем они отличаются?
Таймкод: 00:09:06
Ответ собеседния: Правильный. Кандидат верно описал, что call и apply привязывают контекст и сразу вызывают функцию, а bind только привязывает контекст без вызова. Корректно указал, что apply принимает аргументы в виде массива, а call — через запятую.
Правильный ответ:
Кандидат ответил верно. Дополним деталями и примерами.
call, apply и bind — это методы объекта Function.prototype, которые позволяют явно управлять значением this при вызове функции.
call(thisArg, arg1, arg2, ...)
Вызывает функцию с указанным this и аргументами, переданными через запятую.
function introduce(greeting, punctuation) {
console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}
const person = { name: "Alice" };
introduce.call(person, "Hello", "!"); // "Hello, I'm Alice!"
apply(thisArg, [argsArray])
Вызывает функцию с указанным this и аргументами, переданными в виде массива (или массивоподобного объекта).
introduce.apply(person, ["Hi", "."]); // "Hi, I'm Alice."
apply особенно полезен, когда аргументы уже собраны в массив, или когда нужно передать аргументы одной функции другой:
// Нахождение максимума в массиве — apply разворачивает массив в аргументы
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers); // 7
// В современном JavaScript это делается через spread
const maxModern = Math.max(...numbers); // 7
bind(thisArg, arg1, arg2, ...)
Возвращает новую функцию с привязанным this (и, опционально, частично применёнными аргументами), но не вызывает её. Связь нельзя изменить — повторный bind не перезапишет this.
const sayHello = introduce.bind(person, "Hello");
sayHello("!"); // "Hello, I'm Alice!"
sayHello("?"); // "Hello, I'm Alice?"
// Повторный bind не работает — this остаётся от первого bind
const rebound = sayHello.bind({ name: "Bob" });
rebound(); // "Hello, I'm Alice!" — не Bob!
Частичное применение аргументов (partial application)
bind позволяет зафиксировать не только this, но и начальные аргументы:
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Практическое применение: потеря контекста
class Button {
constructor(label) {
this.label = label;
}
click() {
console.log(`${this.label} clicked`);
}
// bind в конструкторе — классический паттерн для обработчиков событий
attachHandler(element) {
// Без bind: this будет указывать на element, а не на Button
element.addEventListener('click', this.click.bind(this));
}
}
Современная альтернатива
С появлением стрелочных функций необходимость в call/apply/bind снизилась, так как стрелочные функции автоматически захватывают лексический this. Однако bind по-прежнему полезен для частичного применения аргументов и в случаях, когда стрелочная функция неприменима (например, методы классов в старом коде).
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
Вопрос 14. Какие методы жизненного цикла классовых компонентов React ты знаешь и как они реализуются в функциональных компонентах?
Таймкод: 00:09:52
Ответ собеседника: Неполный. Кандидат верно описал componentDidMount через useEffect с пустым массивом зависимостей, упомянул обновление при изменении зависимостей и функцию очистки для размонтирования. Однако не назвал другие методы жизненного цикла: shouldComponentUpdate, componentDidUpdate, getDerivedStateFromProps, getSnapshotBeforeUpdate, componentDidCatch и их аналоги через хуки.
Правильный ответ:
Фазы жизненного цикла классовых компонентов React
Жизненный цикл компонента делится на три фазы: монтирование (mounting), обновление (updating) и размонтирование (unmounting).
Фаза монтирования (Mounting)
Методы вызываются в следующем порядке при создании и вставке компонента в DOM:
constructor(props)— инициализация состояния и привязка обработчиковstatic getDerivedStateFromProps(props, state)— обновление состояния на основе пропсов перед рендером. Статический метод, возвращает объект для обновления state или nullrender()— возвращает JSX, описывающий UIcomponentDidMount()— компонент вставлен в DOM. Идеальное место для загрузки данных, подписки на события, инициализации сторонних библиотек
Фаза обновления (Updating)
Вызывается при изменении пропсов или состояния:
static getDerivedStateFromProps(props, state)shouldComponentUpdate(nextProps, nextState)— позволяет предотвратить рендер, вернув false. Используется для оптимизации производительностиrender()getSnapshotBeforeUpdate(prevProps, prevState)— вызывается непосредственно перед фиксацией изменений в DOM. Возвращает значение, которое передаётся в componentDidUpdate. Используется, например, для сохранения позиции скроллаcomponentDidUpdate(prevProps, prevState, snapshot)— DOM обновлён. Используется для дополнительных запросов при изменении пропсов, работы с DOM
Фаза размонтирования (Unmounting)
componentWillUnmount()— компонент удаляется из DOM. Используется для очистки: отмена таймеров, отписка от событий, отмена сетевых запросов
Обработка ошибок
static getDerivedStateFromError(error)— вызывается при ошибке в дочернем компоненте, позволяет обновить состояние для отображения fallback UIcomponentDidCatch(error, info)— вызывается при ошибке, позволяет залогировать ошибку
Аналоги в функциональных компонентах через хуки
useEffect — основной хук для побочных эффектов
import { useEffect, useState, useRef, useCallback, memo } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const prevUserIdRef = useRef();
// componentDidMount + componentWillUnmount
useEffect(() => {
console.log('Компонент смонтирован');
return () => {
console.log('Компонент размонтирован');
};
}, []);
// componentDidUpdate для конкретной зависимости
useEffect(() => {
if (prevUserIdRef.current !== userId) {
prevUserIdRef.current = userId;
setLoading(true);
fetchUser(userId).then(data => {
setUser(data);
setLoading(false);
});
}
}, [userId]);
// Функция очистки при каждом изменении зависимости
useEffect(() => {
const timer = setInterval(() => {
console.log('Тик');
}, 1000);
return () => clearInterval(timer); // очистка перед следующим эффектом и при размонтировании
}, [userId]);
return <div>{user?.name}</div>;
}
Сравнение методов жизненного цикла и хуков
| Классовый компонент | Функциональный компонент |
|---|---|
constructor | useState с начальным значением |
getDerivedStateFromProps | useEffect без зависимостей или вычисление при рендере |
componentDidMount | useEffect(() => { ... }, []) |
componentDidUpdate | useEffect(() => { ... }, [dep1, dep2]) |
componentWillUnmount | useEffect(() => { return cleanup }, []) |
shouldComponentUpdate | React.memo с кастомным компаратором |
getSnapshotBeforeUpdate | useLayoutEffect (частично) |
componentDidCatch | Библиотеки типа react-error-boundary (встроенного хука нет) |
getDerivedStateFromError | То же — через библиотеки |
useLayoutEffect — аналог синхронных методов жизненного цикла
Вызывается синхронно после всех изменений DOM, но до отрисовки браузером. Аналог componentDidMount и componentDidUpdate, но блокирует отрисовку.
// Сохранение позиции скролла — аналог getSnapshotBeforeUpdate
useLayoutEffect(() => {
const scrollPosition = window.scrollY;
// Выполняется до отрисовки, пользователь не видит мерцания
return () => {
window.scrollTo(0, scrollPosition);
};
}, [items]);
React.memo — аналог shouldComponentUpdate / PureComponent
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
return <div>{/* дорогой рендер */}</div>;
}, (prevProps, nextProps) => {
// Кастомный компаратор: возвращаем true, чтобы НЕ перерисовывать
return prevProps.data.id === nextProps.data.id;
});
useRef для хранения предыдущих значений
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// Использование
function Counter({ count }) {
const prevCount = usePrevious(count);
// prevCount содержит значение count с предыдущего рендера
}
useMemo и useCallback — оптимизация производительности
// useMemo — мемоизация вычислений
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
// useCallback — мемоизация функций (аналог shouldComponentUpdate для дочерних компонентов)
const handleClick = useCallback(() => {
doSomething(userId);
}, [userId]);
Это вопрос по React из области общей фронтенд-эрудиции для golang-разработчика.
Вопрос 15. Для чего используются рефы (useRef) в React?
Таймкод: 00:12:15
Ответ собеседника: Правильный. Кандидат верно указал, что useRef при изменении не вызывает ре-рендера, и описал его использование для доступа к DOM-элементам.
Правильный ответ:
Кандидат ответил верно. Дополним другими сценариями использования useRef.
useRef возвращает мутабельный объект { current: value }, который сохраняется на протяжении всего жизненного цикла компонента. Изменение свойства current не вызывает ре-рендер.
Основные сценарии использования
1. Доступ к DOM-элементам
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // автофокус при монтировании
}, []);
return <input ref={inputRef} type="text" />;
}
2. Хранение мутабельных значений без ре-рендера
Когда нужно сохранить значение между рендерами, но не хотим вызывать перерисовку:
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
useEffect(() => {
return () => clearInterval(intervalRef.current); // очистка
}, []);
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
3. Сохранение предыдущего значения
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Counter({ count }) {
const prevCount = usePrevious(count);
return <div>Current: {count}, Previous: {prevCount}</div>;
}
4. Хранение идентификаторов и ссылок на экземпляры
function ChatRoom({ roomId }) {
const socketRef = useRef(null);
useEffect(() => {
socketRef.current = createSocket(roomId);
return () => socketRef.current.disconnect();
}, [roomId]);
}
useRef vs useState
| useRef | useState | |
|---|---|---|
| Вызывает ре-рендер | Нет | Да |
| Когда читать значение | Синхронно через .current | Асинхронно — на следующем рендере |
| Назначение | Хранение мутабельных данных без ре-рендера | Управление UI-состоянием |
Это вопрос по React из области общей фронтенд-эрудиции для golang-разработчика.
Вопрос 16. Что такое чистый компонент (Pure Component) в React?
Таймкод: 00:12:55
Ответ собеседника: Правильный. Кандидат верно описал Pure Component как компонент, возвращающий одно и то же значение при неизменных пропсах, и упомянул аналогию с React.memo. Формулировка не совсем точная, но суть передана верно.
Правильный ответ:
Кандидат ответил в целом верно. Дополним деталями.
PureComponent — это классовый компонент, который автоматически реализует shouldComponentUpdate с поверхностным (shallow) сравнением пропсов и состояния. Если пропсы и состояние не изменились, ре-рендер не происходит.
Как работает поверхностное сравнение
// PureComponent сравнивает так:
function shallowEqual(objA, objB) {
if (Object.is(objA, objB)) return true; // примитивы или одна ссылка
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!objB.hasOwnProperty(key) || !Object.is(objA[key], objB[key])) {
return false; // сравнение по ссылке, не по глубине
}
}
return true;
}
PureComponent vs Component
// Обычный компонент — всегда перерисовывается при обновлении родителя
class RegularChild extends Component {
render() {
console.log('RegularChild render');
return <div>{this.props.name}</div>;
}
}
// PureComponent — перерисовывается только при изменении пропсов
class PureChild extends PureComponent {
render() {
console.log('PureChild render');
return <div>{this.props.name}</div>;
}
}
Ограничения PureComponent
Поверхностное сравнение не видит изменения внутри объектов и массивов:
// Проблема: PureComponent не заметит изменение
this.state = { items: [1, 2, 3] };
this.state.items.push(4); // мутация — ссылка та же
this.setState({ items: this.state.items }); // PureComponent НЕ перерисуется
// Правильно: создать новый массив
this.setState({ items: [...this.state.items, 4] }); // PureComponent перерисуется
React.memo — аналог для функциональных компонентов
const PureChild = React.memo(function PureChild({ name }) {
console.log('PureChild render');
return <div>{name}</div>;
});
// С кастомным компаратором
const PureChildCustom = React.memo(function PureChild({ user }) {
return <div>{user.name}</div>;
}, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id; // true = не перерисовывать
});
Когда использовать
- Компонент получает простые пропсы (примитивы)
- Компонент дорогой для рендеринга
- Родитель часто перерисовывается, но пропсы дочернего компонента редко меняются
- Данные неизменяемые (immutable) — всегда создаются новые объекты при изменениях
Это вопрос по React из области общей фронтенд-эрудиции для golang-разработчика.
Вопрос 17. Что такое Virtual DOM и как он работает в React?
Таймкод: 00:13:32
Ответ собеседника: Правильный. Кандидат верно описал Virtual DOM как лёгковесную копию реального DOM и объяснил процесс reconciliation для точечных изменений вместо полного обновления DOM.
Правильный ответ:
Кандидат ответил верно. Дополним деталями о механизме работы.
Virtual DOM — это легковесное представление реального DOM в виде обычных JavaScript-объектов. Каждый элемент Virtual DOM — это объект с типом, пропсами и дочерними элементами.
Зачем нужен Virtual DOM
Работа напрямую с DOM — медленная операция. Браузер при каждом изменении DOM пересчитывает раскладку (reflow) и перерисовывает экран (repaint). Если приложение делает множество мелких изменений, это приводит к падению производительности.
Virtual DOM позволяет собрать все изменения в памяти и применить их одним батчем, отправив в реальный DOM только минимальный набор необходимых операций.
Процесс работы: render, diff, commit
1. Render (рендеринг)
Когда состояние или пропсы компонента меняются, React вызывает функцию рендеринга и создаёт новое дерево Virtual DOM (React-элементов).
// JSX компилируется в вызовы createElement
const element = <h1 className="title">Hello, {name}</h1>;
// Эквивалент:
const element = React.createElement('h1', { className: 'title' }, 'Hello, ', name);
// Результат — обычный объект:
// { type: 'h1', props: { className: 'title', children: ['Hello, ', 'Alice'] }, ... }
2. Diffing (сравнение)
React сравнивает новое дерево Virtual DOM с предыдущим. Алгоритм diffing основан на двух эвристиках:
- Разные типы элементов — если тип элемента изменился (например,
<div>стал<span>), React полностью уничтожает старое поддерево и создаёт новое. Никакого сравнения внутри. - Ключ (key) — при сравнении списков дочерних элементов React использует атрибут
keyдля сопоставления элементов между старым и новым деревом. Это позволяет переиспользовать DOM-узлы при изменении порядка.
// Без key — React не понимает, какой элемент куда переместился
// С key — корректное сопоставление
const list = items.map(item => <li key={item.id}>{item.name}</li>);
3. Commit (фиксация)
React применяет вычисленные минимальные изменения к реальному DOM. Только те узлы, которые реально изменились, обновляются.
Пример diffing
// Было:
<ul>
<li key="a">First</li>
<li key="b">Second</li>
</ul>
// Стало:
<ul>
<li key="c">Zero</li>
<li key="a">First</li>
<li key="b">Second</li>
</ul>
React видит, что ключ c — новый, создаёт для него DOM-узел. Ключи a и b — существующие, переиспользует их. Без ключей React обновил бы содержимое всех трёх элементов.
Reconciliation (согласование)
Reconciliation — это общий процесс определения, какие части дерева нужно обновить. Начиная с React 18 появился Concurrent Rendering, который позволяет React прерывать рендеринг для обработки более приоритетных задач (например, пользовательского ввода), что делает приложение более отзывчивым.
Ограничения
Virtual DOM — не бесплатный оптимизатор. Сам процесс diffing требует вычислительных ресурсов. Для статичных страниц с редкими обновлениями он может быть избыточен. Однако для динамических приложений с частыми изменениями он даёт значительный выигрыш за счёт минимизации операций с реальным DOM.
Это вопрос по React из области общей фронтенд-эрудиции для golang-разработчика.
Вопрос 18. Реализовать функцию callLimit, которая ограничивает число вызовов функции, с методом reset для сброса счётчика и колбэком onFinish, вызываемым после исчерпания лимита.
Таймкод: 00:16:25
Ответ собеседния: Правильный. Кандидат в итоге решил задачу верно, хотя в процессе допустил несколько ошибок: не вернул функцию из callLimit (не использовал замыкание), не передал аргументы в вызываемую функцию, неправильно расположил инкремент счётчика. После подсказок исправил ошибки. Метод reset реализовал через добавление свойства к функции, что корректно, так как в JavaScript функция — это объект.
Правильный ответ:
Задача на замыкания и функции высшего порядка в JavaScript. Кандидат в итоге решил её верно. Приведём финальное решение и альтернативные подходы.
Решение
function callLimit(fn, limit, onFinish) {
let count = 0;
function limitedFunction(...args) {
if (count >= limit) {
return undefined;
}
count++;
if (count === limit && typeof onFinish === 'function') {
onFinish();
}
return fn.apply(this, args);
}
limitedFunction.reset = function() {
count = 0;
};
return limitedFunction;
}
Использование
const greet = (name) => console.log(`Hello, ${name}!`);
const limitedGreet = callLimit(greet, 3, () => {
console.log('Лимит исчерпан!');
});
limitedGreet('Alice'); // "Hello, Alice!"
limitedGreet('Bob'); // "Hello, Bob!"
limitedGreet('Charlie'); // "Hello, Charlie!" + "Лимит исчерпан!"
limitedGreet('Dave'); // undefined — лимит исчерпан
limitedGreet.reset(); // сброс счётчика
limitedGreet('Eve'); // "Hello, Eve!" — снова работает
Ключевые моменты решения
Замыкание — переменная count хранится в замыкании и доступна как limitedFunction, так и limitedFunction.reset.
Сохранение контекста (this) — использование fn.apply(this, args) вместо простого fn(...args) гарантирует, что если limitedFunction вызывается как метод объекта, this корректно передастся в fn.
Передача аргументов — ...args (rest parameters) собирает все аргументы, apply передаёт их в вызываемую функцию.
Метод reset как свойство функции — в JavaScript функция является объектом, поэтому к ней можно добавлять свойства. Это стандартный паттерн для расширения функциональности.
Альтернативная реализация с объектом настроек
function callLimit(fn, limit, { onFinish } = {}) {
let count = 0;
function limited(...args) {
if (count >= limit) return;
count++;
if (count === limit) onFinish?.();
return fn.apply(this, args);
}
limited.reset = () => { count = 0; };
limited.getCount = () => count; // бонус: текущий счётчик
return limited;
}
// Использование
const limitedLog = callLimit(console.log, 2, {
onFinish: () => console.log('Готово!')
});
Важные ошибки, которые допустил кандидат
- Не вернул функцию из callLimit — без замыкания нет сохранения состояния
- Не передал аргументы в fn — обёртка теряла все аргументы вызова
- Неправильное расположение инкремента — счётчик должен увеличиваться до проверки лимита или сразу после, но до вызова функции
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
Вопрос 19. Что будет выведено в консоль при выполнении цепочки промисов: Promise.resolve().then(() => console.log('A')).then(() => { throw new Error(); }).catch(() => console.log('B')).then(() => console.log('C')).finally(() => console.log('D'))?
Таймкод: 00:30:49
Ответ собеседника: Правильный. Кандидат верно определил, что вывод будет ABCD, и корректно объяснил последовательность: Promise.resolve() → A → ошибка → catch → B → продолжение цепочки → C → finally → D.
Правильный ответ:
Кандидат ответил верно. Вывод: A B C D (каждый на новой строке).
Пошаговый разбор
Promise.resolve() — создаёт зарезолвленный промис.
.then(() => console.log('A')) — колбэк попадает в микротасочную очередь, выполняется первым. Выводит A. Возвращает undefined (успех).
.then(() => { throw new Error(); }) — получает успех от предыдущего then, выбрасывает ошибку. Промис переходит в состояние rejected.
.catch(() => console.log('B')) — перехватывает ошибку. Выводит B. Возвращает undefined (успех) — это ключевой момент: catch, обработав ошибку, «лечит» цепочку и возвращает её в состояние resolved.
.then(() => console.log('C')) — получает успех от catch. Выводит C.
.finally(() => console.log('D')) — выполняется всегда, независимо от результата. Выводит D. Не получает аргументов, не изменяет значение, передаёт его дальше прозрачно.
Почему после catch цепочка продолжается
Это важная особенность: catch — это по сути then(null, onRejected). Когда он успешно обрабатывает ошибку (не выбрасывает новую), возвращённый промис считается resolved, и следующий then выполняется как при успешном результате.
Порядок выполнения с учётом микротасок
Все колбэки промисов выполняются в микротасочной очереди после завершения текущего синхронного кода. В данном случае весь синхронный код — это построение цепочки, а все колбэки последовательно выполняются один за другим в порядке цепочки.
Важные нюансы
finallyне принимает аргументов — нельзя узнать, был это resolve или rejectfinallyпробрасывает значение дальше без изменений (в отличие от then/catch, которые могут его изменить)- Если бы catch тоже выбросил ошибку, следующий then был бы пропущен, но finally всё равно выполнился бы
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
Вопрос 20. Что будет выведено в консоль при выполнении кода с setTimeout и циклом: for (var i = 10; i--;) { setTimeout(() => console.log(i), 0); }? И как исправить, чтобы выводилось 9, 8, 7...0?
Таймкод: 00:35:04
Ответ собеседника: Правильный. Кандидат определил, что будет выведено 10 значений, объял механизм постфиксного декремента в условии цикла, и предложил корректные способы исправления через замыкание и передачу аргумента в setTimeout.
Правильный ответ:
Кандидат ответил верно после подсказки. Разберём подробно.
Что будет выведено
Выведется 10 значений -1.
Пошаговый разбор цикла
Условие i-- — это постфиксный декремент. Он возвращает текущее значение i, а затем уменьшает его на 1. Цикл продолжается, пока условие не станет falsy (0).
i = 10 → i-- возвращает 10 (truthy), i становится 9 → итерация с i=9
i = 9 → i-- возвращает 9 (truthy), i становится 8 → итерация с i=8
...
i = 1 → i-- возвращает 1 (truthy), i становится 0 → итерация с i=0
i = 0 → i-- возвращает 0 (falsy), i становится -1 → цикл завершается
Цикл выполняется 10 раз (для значений i от 9 до 0 в теле цикла), но к моменту выполнения колбёков setTimeout переменная i уже равна -1. Поскольку var имеет функциональную область видимости, все 10 колбэков ссылаются на одну и ту же переменную i.
Способы исправления
1. Замыкание через локальную переменную
for (var i = 10; i--;) {
const a = i; // захватываем текущее значение
setTimeout(() => console.log(a), 0);
}
// Вывод: 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
2. Третий аргумент setTimeout
for (var i = 10; i--;) {
setTimeout((val) => console.log(val), 0, i);
}
// Вывод: 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
Третий аргумент setTimeout передаётся в колбэк как параметр. Это стандартная возможность, поддерживаемая всеми браузерами.
3. Использование let вместо var
for (let i = 10; i--;) {
setTimeout(() => console.log(i), 0);
}
// Вывод: 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
let создаёт новую переменную i на каждой итерации цикла, поэтому каждый колбэк захватывает своё собственное значение.
4. IIFE (Immediately Invoked Function Expression)
for (var i = 10; i--;) {
(function(val) {
setTimeout(() => console.log(val), 0);
})(i);
}
// Вывод: 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
Это классический паттерн до появления let в ES6.
Рекомендуемый способ
Самый простой и современный способ — заменить var на let. Это решает проблему без дополнительного кода.
Это вопрос по JavaScript из области общей веб-эрудиции для golang-разработчика.
