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

Фронтенд собеседование 2025 | Junior Frontend | Реальные вопросы и задачи

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

Сегодня мы разберём собеседование с 16-летним кандидатом Женей, который продемонстрировал сильную теоретическую базу по JavaScript и React, но столкнулся с трудностями при решении практической задачи на преобразование массива объектов в хэш-таблицу. Интервьюеры отметили, что кандидат попал в типичную ловушку начинающих разработчиков — обширные, но поверхностные знания без глубокого понимания синтаксиса и практики написания кода, и дали рекомендации сосредоточиться на отработке базовых методов массивов и работе с объектами через простые задачи на Codewars.

Вопрос 1. Сколько часов в день ты занимаешься программированием и как давно начал?.

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

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

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

Ответ собеседника корректен — это вопрос о личном опыте и вовлечённости, и здесь нет «правильного» или «неправильного» варианта. Однако для подготовки к интервью стоит понимать, что такой вопрос задают для оценки мотивации и дисциплины.

На что обращает внимание интервьюер:

  • Устойчивость привычки. 10–15 часов в день — это много, и важно показать, что это не выгорание, а осознанный ритм. Лучше подчеркнуть регулярность (например, «каждый день по 6–8 часов в течение нескольких месяцев»), чем эпизодические марафоны.

  • Качество vs количество. Интервьюер хочет понять, что кандидат не просто сидит за кодом, а делает осмысленные проекты, читает документацию, разбирает open-source.

  • Баланс. Упоминание перерывов — это плюс. Демонстрация того, что человек понимает важность отдыха и не работает на износ.

Рекомендуемая структура ответа для подготовки:

> «Я занимаюсь программированием около X лет / месяцев. В последнее время уделяю этому Y часов в день — обычно утром работаю над проектами, днём изучаю новые технологии, вечером — код-ревью или чтение статей. Стараюсь соблюдать баланс, чтобы избежать выгорания».

Главное — честность и демонстрация системного подхода к обучению, а не просто количество часов.

Вопрос 2. Какой у тебя стек технологий и что ты изучал по месяцам (февраль — май)?.

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

Ответ собеседника: Правильный. Февраль — начало изучения JavaScript, март — закончил базу JS, апрель — TypeScript, май — практика и написание проекта. Также изучал React, Redux (сначала чистый, потом RTK), React Query, пробовал RTK Query. Бэкенд писал на NestJS с Sequelize.

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

Ответ собеседателя логичен и демонстрирует последовательное обучение. Однако важно отметить, что это интервью на позицию Golang-разработчика, и стек, описанный кандидатом, полностью состоит из технологий экосистемы JavaScript/TypeScript.

Ключевые замечания:

  • Несоответствие стека вакансии. Для позиции Golang-разработчика ожидается знание Go, его стандартной библиотеки, экосистемы (Gin, Echo, GORM, sqlx и т.д.), а также понимание конкурентности в Go, работы с каналами, горутинами и т.п.

  • Что можно было бы улучшить. Если кандидат позиционирует себя как full-stack или планирует переход в Go, стоит это явно озвучить: «Я начинал с JS/TS, но сейчас активно изучаю Go и планирую развиваться в этом направлении».

Рекомендуемая структура ответа для Golang-разработчика:

> «Мой основной стек — Go. За последние месяцы я углублялся в следующие области: > - Февраль: основы Go, работа с горутинами и каналами > - Март: стандартная библиотека (net/http, context, sync), написание REST API > - Апрель: работа с базами данных (database/sql, sqlx, GORM), миграции > - Май: тестирование (testing, testify), профилирование (pprof), деплой (Docker, CI/CD)»

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

package main

import (
"context"
"fmt"
"net/http"
"time"
)

func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

result := make(chan string, 1)
go func() {
// Имитация долгой операции
time.Sleep(1 * time.Second)
result <- "Hello, World!"
}()

select {
case msg := <-result:
fmt.Fprint(w, msg)
case <-ctx.Done():
http.Error(w, "Request timeout", http.StatusGatewayTimeout)
}
}

func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}

Этот пример демонстрирует понимание контекста, горутин, каналов и обработки таймаутов — ключевые концепции для любого Go-разработчика.

Вопрос 3. Расскажи подробнее о проекте, который ты писал в мае. Какой стек использовал, что реализовал?.

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

Ответ собеседника: Правильный. Написал фулстек приложение — подобие интернет-магазина. Фронтенд на React 19 + TypeScript, state-менеджер Redux, асинхронный state-менеджер React Query (не понравился RTK Query). Бэкенд на NestJS + Sequelize. Реализовал JWT-авторизацию, создание и выдачу ролей, полноценную админ-панель с возможностью менять почту и пароль. Платёжку не делал, так как не знал, как это реализовать. Проект залит на GitHub, но не задеплоен.

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

Ответ собеседателя демонстрирует практический опыт и понимание fullstack-разработки. Однако, как и в предыдущем вопросе, стек не соответствует вакансии Golang-разработчика. Ниже приведён пример того, как стоило бы ответить на этот вопрос, если бы проект был реализован на Go.

Структура хорошего ответа о проекте:

  1. Описание проекта. Что это за приложение, какие проблемы решает, для кого предназначено.

  2. Архитектура. Как организован проект, какие паттерны использовались (clean architecture, hexagonal architecture, MVC и т.д.).

  3. Стек технологий. Конкретные библиотеки, фреймворки, базы данных.

  4. Реализованные фичи. Авторизация, роли, админ-панель, интеграции.

  5. Проблемы и решения. С какими сложностями столкнулся и как их решил.

  6. DevOps. Деплой, CI/CD, мониторинг.

Пример ответа для Golang-проекта:

> «Я разработал backend для системы управления задачами (аналог Trello). Архитектура — clean architecture с разделением на слои: handler, service, repository. > > Стек: Go 1.22, Gin (роутинг), GORM (ORM для PostgreSQL), JWT-авторизация, Redis (кэширование), Docker, GitHub Actions (CI/CD). > > Реализовал: > - REST API с версионированием (v1, v2) > - RBAC (Role-Based Access Control) с ролями admin, manager, user > - WebSocket для обновлений в реальном времени > - Graceful shutdown и health-check endpoints > - Middleware для логирования, метрик и rate limiting > > Проблемы: При масштабировании WebSocket-соединений столкнулся с утечкой горутин. Решил с помощью context.WithCancel и отслеживания количества активных соединений. > > Проект задеплоен на AWS (EC2 + RDS), настроен CI/CD пайплайн.»

Пример кода на Go — JWT middleware:

package middleware

import (
"net/http"
"strings"
"time"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("your-secret-key")

type Claims struct {
UserID uint `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}

func GenerateToken(userID uint, role string) (string, error) {
claims := Claims{
UserID: userID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}

func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
return
}

tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims := &Claims{}

token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})

if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
return
}

c.Set("userID", claims.UserID)
c.Set("role", claims.Role)
c.Next()
}
}

func RoleMiddleware(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("role")
if !exists {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Role not found"})
return
}

for _, role := range roles {
if userRole == role {
c.Next()
return
}
}

c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
}
}

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

func SetupRouter() *gin.Engine {
r := gin.Default()

// Публичные маршруты
r.POST("/api/v1/auth/login", loginHandler)
r.POST("/api/v1/auth/register", registerHandler)

// Защищённые маршруты
authorized := r.Group("/api/v1")
authorized.Use(middleware.AuthMiddleware())
{
authorized.GET("/profile", profileHandler)

// Только для админов
admin := authorized.Group("/admin")
admin.Use(middleware.RoleMiddleware("admin"))
{
admin.GET("/users", listUsersHandler)
admin.PUT("/users/:id/role", updateUserRoleHandler)
}
}

return r
}

Пример SQL-схемы для RBAC:

CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);

-- Таблица для аудита действий админа
CREATE TABLE admin_actions (
id SERIAL PRIMARY KEY,
admin_id INTEGER REFERENCES users(id),
action VARCHAR(100) NOT NULL,
target_user_id INTEGER REFERENCES users(id),
details JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Рекомендации для подготовки:

  • Если у вас есть проект на другом стеке, подчеркните transferable skills: понимание REST, работа с БД, авторизация, архитектурные паттерны.

  • Если вы претендуете на позицию Go-разработчика, обязательно имейте хотя бы один pet-project на Go, чтобы показать практические навыки.

  • Деплой проекта (даже на бесплатном хостинге) — это большой плюс, показывающий зрелость разработчика.

Вопрос 4. Когда ты занимался вёрсткой и использовал ли ChatGPT при разработке?.

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

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

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

Ответ собеседния частично корректен, но содержит спорное утверждение о ChatGPT. Давайте разберём оба аспекта вопроса.

Вёрстка:

Умение вёрстки для backend-разработчика — это бонус, но не ключевой навык. Важно показать, что вы понимаете основы и можете при необходимости поработать с фронтендом.

Использование AI-инструментов (ChatGPT, Copilot и т.д.):

Отказ от использования AI-инструментов может быть воспринят двояко:

  • Плюс: кандидат предпочитает глубокое понимание кода, а не слепое копирование.
  • Минус: в реальной разработке AI-инструменты широко используются для повышения продуктивности. Полный отказ может сигнализировать о негибкости или нежелании использовать современные инструменты.

Рекомендуемый подход:

Лучше показать зрелую позицию: AI — это инструмент, который нужно использовать осознанно, проверяя и понимая результат.

Пример хорошего ответа:

> «Вёрстку изучал на начальном этапе — HTML, CSS, немного JavaScript. Это помогает мне лучше понимать фронтенд-разработчиков и при необходимости быстро собрать прототип. > > Что касается ChatGPT и подобных инструментов — я их использую, но осознанно. Например: > - Для генерации boilerplate-кода > - Для быстрого поиска синтаксиса или API библиотек > - Для написания регулярных выражений > - Для объяснения сложных концепций > > Но я всегда проверяю и понимаю сгенерированный код, прежде чем использовать его в проекте. AI — это помощник, а не замена мышлению.»

Пример осознанного использования AI в Go-разработке:

Допустим, вы попросили ChatGPT сгенерировать функцию для валидации email. Он выдал:

func ValidateEmail(email string) bool {
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, email)
return matched
}

Правильная реакция разработчика:

  1. Проверить, что регулярное выражение корректно.
  2. Обработать ошибку от MatchString.
  3. Скомпилировать регулярку заранее для производительности.
  4. Добавить тесты.

Улучшенная версия:

package validator

import (
"fmt"
"net/mail"
"regexp"
)

var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

func ValidateEmail(email string) error {
if len(email) > 254 {
return fmt.Errorf("email too long: %d characters", len(email))
}

if !emailRegex.MatchString(email) {
return fmt.Errorf("invalid email format: %s", email)
}

// Дополнительная проверка через стандартную библиотеку
_, err := mail.ParseAddress(email)
if err != nil {
return fmt.Errorf("invalid email address: %w", err)
}

return nil
}

Тесты:

package validator

import (
"testing"
)

func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{"valid email", "user@example.com", false},
{"valid email with plus", "user+tag@example.com", false},
{"missing @", "userexample.com", true},
{"missing domain", "user@", true},
{"empty string", "", true},
{"too long", string(make([]byte, 255)) + "@test.com", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateEmail(%q) error = %v, wantErr %v", tt.email, err, tt.wantErr)
}
})
}
}

Итог:

Для подготовки к интервью важно показать, что вы:

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

Вопрос 5. Ты используешь Redux в чистом виде или с Redux Toolkit?.

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

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

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

Ответ собеседника корректен и демонстрирует понимание эволюции работы с Redux. Однако для позиции Golang-разработчика этот вопрос и ответ не имеют прямого отношения к целевой вакансии.

Контекст вопроса:

Redux и Redux Toolkit — это библиотеки для управления состоянием в React-приложениях (фронтенд). Для Go-разработчика аналогичные концепции относятся к:

  • Управлению состоянием сервера (in-memory хранилища, Redis)
  • Паттернам работы с данными (Repository, Unit of Work)
  • Конкурентному доступу к данным (mutex, channels)

Если вопрос задаётся на интервью для Go-разработчика:

Интервьюер может проверять общий кругозор или уточнять опыт fullstack-разработки. В этом случае стоит дать краткий ответ и перевести к релевантным темам.

Рекомендуемый ответ:

> «Да, я работал с Redux в pet-проектах. Начинал с чистого Redux для понимания принципов (actions, reducers, store), затем перешел на Redux Toolkit, который упрощает код через createSlice и createAsyncThunk. > > Однако мой основной фокус — backend на Go. Если говорить об аналогичных концепциях в Go, то я работал с: > - Паттерном Repository для абстракции доступа к данным > - Конкурентным доступом к состоянию через sync.Mutex и channels > - In-memory кэшированием с использованием sync.Map»

Пример управления состоянием в Go (аналог состояния в Redux):

package store

import (
"sync"
)

// User представляет сущность пользователя
type User struct {
ID uint `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
}

// UserStore — thread-safe хранилище пользователей
type UserStore struct {
mu sync.RWMutex
users map[uint]User
}

// NewUserStore создаёт новый store
func NewUserStore() *UserStore {
return &UserStore{
users: make(map[uint]User),
}
}

// Set добавляет или обновляет пользователя (аналог dispatch action)
func (s *UserStore) Set(user User) {
s.mu.Lock()
defer s.mu.Unlock()
s.users[user.ID] = user
}

// Get возвращает пользователя по ID (аналог selector)
func (s *UserStore) Get(id uint) (User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[id]
return user, ok
}

// Delete удаляет пользователя
func (s *UserStore) Delete(id uint) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.users, id)
}

// GetAll возвращает всех пользователей
func (s *UserStore) GetAll() []User {
s.mu.RLock()
defer s.mu.RUnlock()

result := make([]User, 0, len(s.users))
for _, user := range s.users {
result = append(result, user)
}
return result
}

Пример использования с каналами (реактивный подход):

package store

// Action представляет действие над store
type Action struct {
Type string
Payload interface{}
}

// UserStoreReactive — store с использованием каналов
type UserStoreReactive struct {
users map[uint]User
actions chan Action
done chan struct{}
}

func NewUserStoreReactive() *UserStoreReactive {
s := &UserStoreReactive{
users: make(map[uint]User),
actions: make(chan Action, 100),
done: make(chan struct{}),
}
go s.run()
return s
}

func (s *UserStoreReactive) run() {
for {
select {
case action := <-s.actions:
s.handleAction(action)
case <-s.done:
return
}
}
}

func (s *UserStoreReactive) handleAction(action Action) {
switch action.Type {
case "SET_USER":
if user, ok := action.Payload.(User); ok {
s.users[user.ID] = user
}
case "DELETE_USER":
if id, ok := action.Payload.(uint); ok {
delete(s.users, id)
}
}
}

func (s *UserStoreReactive) Dispatch(action Action) {
s.actions <- action
}

func (s *UserStoreReactive) Close() {
close(s.done)
}

Итог для подготовки:

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

Вопрос 6. На какой версии React ты писал проект?.

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

Ответ собеседника: Правильный. Писал на React 19.

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

Ответ собеседника фактически корректен, но, как и предыдущие вопросы, не имеет прямого отношения к вакансии Golang-разработчика.

Контекст:

React 19 — это актуальная версия фреймворка (выпущена в декабре 2022, стабильная версия). Знание современных версий фронтенд-технологий показывает, что кандидат следит за обновлениями экосистемы.

Рекомендуемый ответ для Go-разработчика:

> «Да, в pet-проекте использовал React 19. Это помогло мне понять, как фронтенд взаимодействует с backend через API. > > Если говорить о Go, то я работаю с Go 1.22, который включает: > - Улучшенный range over integers > - Поддержка переменных циклов (исправление проблемы с замыканиями) > - Улучшения в net/http для маршрутизации»

Пример новых возможностей Go 1.22:

package main

import "fmt"

func main() {
// Go 1.22: range over integers
for i := range 5 {
fmt.Println(i) // 0, 1, 2, 3, 4
}
}

Пример исправления проблемы с замыканиями в циклах:

package main

import "fmt"

func main() {
// До Go 1.22: классическая ловушка
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() { fmt.Println(i) })
}
// В Go 1.21 и ранее: выведет 3, 3, 3
// В Go 1.22+: выведет 0, 1, 2

for _, f := range funcs {
f()
}
}

Итог для подготовки:

  • Вопросы о версиях фронтенд-технологий на интервью для Go-разработчика — это проверка общего кругозора.
  • Дайте краткий ответ и переведите к релевантным темам (версия Go, новые фичи, экосистема).
  • Покажите, что вы следите за обновлениями в своей основной технологии.

Вопрос 7. Сколько тебе лет, где учишься и какие планы на будущее?.

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

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

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

Это вопрос о личных обстоятельствах и планах. Здесь нет «правильного» или «неправильного» ответа, но есть рекомендации, как лучше преподнести информацию.

На что обращает внимание интервьюер:

  • Мотивация. Кандидат должен показать искренний интерес к разработке, а не просто желание заработать.
  • Реалистичность планов. Планы должны быть конкретными и достижимыми.
  • Зрелость. Понимание своих сильных и слабых сторон, готовность учиться.

Рекомендуемая структура ответа:

  1. Возраст и образование. Кратко и честно.
  2. Текущий уровень. Что уже умеешь, какие проекты есть.
  3. Планы развития. Конкретные цели на ближайшие 6-12 месяцев.
  4. Отношение к работе. Готовность к обучению, ответственность.

Пример ответа:

> «Мне 16 лет, учусь в колледже. Программированием занимаюсь около полугода — за это время освоил JavaScript, TypeScript, React, написал pet-project (fullstack-приложение на NestJS + React). > > Планы на ближайшие месяцы: > - Углубить знания в backend-разработке (планирую изучить Go или продолжить с Node.js) > - Подтянуть алгоритмы и структуры данных > - Разобраться с DevOps: Docker, CI/CD, деплой > > Долгосрочно: хочу развиваться в сторону backend-разработки или fullstack. Понимаю, что мне нужно ещё много учиться, но готов активно работать над собой. > > Понимаю, что на полную занятость смогу выйти после 18 лет, но буду рад возможности поработать на стажировке или фрилансе уже сейчас.»

Рекомендации для подготовки:

  • Не бойтесь говорить о возрасте. Молодость — это не недостаток, а потенциал. Многие компании готовы инвестировать в мотивированных начинающих разработчиков.
  • Будьте конкретны. Вместо «хочу стать фулстеком» лучше сказать «хочу научиться писать production-ready backend на Go, разобраться с микросервисной архитектурой».
  • Покажите осведомлённость. Упоминание конкретных технологий, паттернов, инструментов демонстрирует серьёзный подход.

Пример плана развития для начинающего Go-разработчика:

ПериодЦельРезультат
1-2 месяцаОсновы GoКонсольные утилиты, понимание типов, интерфейсов, горутин
3-4 месяцаWeb-разработкаREST API на Gin/Echo, работа с БД
5-6 месяцевПродвинутые темыТестирование, Docker, CI/CD
7-8 месяцевАлгоритмыРешение 100+ задач на LeetCode
9-12 месяцевПортфолио2-3 полноценных проекта на GitHub

Итог:

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

Вопрос 8. Знаешь ли ты, что такое работа разработчика, и думал ли ты, в какую компанию хочешь пойти?.

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

Ответ собеседника: Неполный. Ответ на этот вопрос не был дан — видео обрывается на вопросе интервьюера.

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

Этот вопрос проверяет понимание кандидатом профессии и его карьерных ожиданий. Ниже приведён пример полного и структурированного ответа.

Понимание работы разработчика:

Работа разработчика — это не только написание кода. Вот из чего она состоит:

1. Написание кода (30-40% времени)

Это основная, но не единственная часть работы. Код должен быть:

  • Читаемым и поддерживаемым
  • Покрытым тестами
  • Соответствующим стандартам команды

2. Код-ревью (15-20% времени)

Проверка кода коллег — важная часть процесса. Это помогает:

  • Ловить баги до попадания в production
  • Распространять знания в команде
  • Поддерживать единый стиль кода

3. Планирование и обсуждение (15-20% времени)

  • Планирование спринтов
  • Обсуждение архитектурных решений
  • Оценка трудоёмкости задач

4. Документация (5-10% времени)

  • Написание README, API-документации
  • Комментирование сложных частей кода
  • Ведение Wiki проекта

5. Отладка и исправление багов (10-15% времени)

  • Воспроизведение и локализация проблем
  • Написание тестов, воспроизводящих баг
  • Исправление и регрессионное тестирование

6. Изучение нового (постоянно)

  • Чтение документации
  • Изучение новых технологий
  • Участие в конференциях и митапах

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

> «Да, я понимаю, что работа разработчика — это не только написание кода. Это ещё и код-ревью, планирование, документация, отладка, постоянное обучение. Я готов к этому. > > Что касается компании — я хочу работать в месте, где: > - Есть менторство и возможность учиться у опытных коллег > - Используются современные технологии и практики (code review, CI/CD, тестирование) > - Есть интересные задачи, а не только рутина > - Ценится качество кода, а не скорость любой ценой > > Для меня сейчас важнее получить опыт и вырасти как разработчику, чем максимальная зарплата.»

Типы компаний и на что обращать внимание:

А. Стартапы

Плюсы:

  • Широкий круг задач
  • Быстрый рост
  • Возможность влиять на продукт

Минусы:

  • Высокий риск (компания может закрыться)
  • Часто хаотичные процессы
  • Высокая нагрузка

Б. Продуктовые компании среднего размера

Плюсы:

  • Стабильность
  • Выстроенные процессы
  • Возможность работать над одним продуктом долго

Минусы:

  • Медленнее принимаются решения
  • Может быть бюрократия

В. Аутсорс-компании

Плюсы:

  • Разнообразие проектов
  • Быстрый опыт в разных доменах

Минусы:

  • Частая смена контекста
  • Меньше влияния на архитектуру
  • Возможны овертаймы

Г. Крупные корпорации (FAANG и подобные)

Плюсы:

  • Высокие зарплаты
  • Сильные инженерные практики
  • Бренд в резюме

Минусы:

  • Высокий порог входа
  • Может быть узкая специализация
  • Конкуренция

Рекомендации для подготовки:

  • Изучите компанию, в которую идёте на интервью. Посмотрите их технологический блог, GitHub, отзывы сотрудников.
  • Подготовьте вопросы для интервьюера: «Какие технологии вы используете?», «Как устроен процесс код-ревью?», «Есть ли менторство для джуниоров?»
  • Покажите, что вы понимаете разницу между «написать код» и «создать продукт».

Итог:

Хороший ответ демонстрирует:

  • Реалистичное понимание профессии
  • Осознанный выбор направления
  • Готовность учиться и вкладываться в работу
  • Интерес к конкретной компании, а не просто «куда-нибудь»

Вопрос 9. Честно оцени свой уровень по шкале от 1 до 10 по технологиям HTML, CSS, JavaScript, React.

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

Ответ собеседника: Правильный. HTML и CSS оценивает на 7, JavaScript также на 7-8, React и остальные технологии примерно на том же уровне.

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

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

Как правильно оценивать свой уровень:

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

Уровни владения технологией:

1-3: Начальный (Junior)

  • Знаете базовый синтаксис
  • Можете написать простые примеры
  • Нужна помощь при решении задач
  • Мало практики в реальных проектах

4-5: Базовый (Junior+)

  • Понимаете основные концепции
  • Можете решать типовые задачи
  • Работали над учебными проектами
  • Знаете основные паттерны и best practices

6-7: Средний (Middle-)

  • Уверенно работаете с технологией
  • Понимаете продвинутые концепции
  • Имеете опыт в реальных проектах
  • Можете объяснить решения и обосновать выбор

8-9: Продвинутый (Middle+/Senior)

  • Глубокое понимание внутренней работы
  • Можете проектировать архитектуру
  • Оптимизируете производительность
  • Менторите других разработчиков

10: Эксперт

  • Вносите вклад в экосистему
  • Знаете все тонкости и граничные случаи
  • Можете создавать библиотеки и фреймворки
  • Признанный авторитет в сообществе

Рекомендуемый ответ для Go-разработчика:

> «Если оценивать мой уровень честно: > > HTML/CSS: 6/10 — понимаю семантику, flexbox, grid, могу сверстать адаптивный макет. Не эксперт в анимациях и сложных layout-ах. > > JavaScript: 6/10 — знаю основы, асинхронность, замыкания, прототипы. Работал с промисами и async/await. Но не углублялся в движок V8 и оптимизацию. > > React: 5/10 — понимаю компоненты, хуки, жизненный цикл. Написал pet-project с Redux и React Query. Но не работал с серверным рендерингом и сложной оптимизацией. > > Go: 4/10 — изучаю параллельно. Знаю основы синтаксиса, горутины, каналы, интерфейсы. Написал несколько небольших проектов. Активно углубляю знания. > > Я понимаю, что для позиции Go-разработчика мои фронтенд-навыки — это бонус, а не основная компетенция. Поэтому фокус делаю на изучении Go и backend-разработки.»

Пример для самопроверки уровня Go:

Чтобы объективно оценить свой уровень в Go, попробуйте ответить на эти вопросы:

Уровень 4-5 (Junior+):

  • Что такое горутины и чем отличаются от потоков ОС?
  • Как работают каналы? В чём разница между buffered и unbuffered?
  • Что такое интерфейсы и как они реализуются?
  • Как обрабатывать ошибки в Go?

Уровень 6-7 (Middle-):

  • Как работает планировщик Go (GPM модель)?
  • Что такое context и зачем он нужен?
  • Как избежать утечки горутин?
  • Как работает garbage collector в Go?
  • Что такое escape analysis?

Уровень 8-9 (Middle+/Senior):

  • Как профилировать и оптимизировать Go-программы?
  • Как работают sync.Pool, sync.Once, errgroup?
  • Как реализовать graceful shutdown?
  • Как работает reflect и когда его использовать?
  • Как писать бенчмарки и интерпретировать результаты?

Итог для подготовки:

  • Будьте честны в самооценке. Завышение уровня легко раскроется техническими вопросами.
  • Лучше сказать «я знаю X на 5/10, но активно учусь», чем завысить и не ответить на вопрос.
  • Для позиции Go-разработчика важнее показать потенциал и способность учиться, чем идеальное знание фронтенда.

Вопрос 10. Что такое специфичность селекторов в CSS и каков порядок приоритетности селекторов?.

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

Ответ собеседника: Правильный. Специфичность — это приоритетность селекторов. Наивысшая приоритетность у инлайновых стилей, затем стили в теге style в head, ключевое слово !important перебивает всё. Далее идут id, потом классы и теги, затем псевдоклассы и псевдоэлементы.

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

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

Что такое специфичность (Specificity):

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

Формула расчёта специфичности:

Специфичность вычисляется как кортеж из четырёх значений: (a, b, c, d)

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

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

  1. !important — переопределяет всё (использовать с осторожностью)
  2. Инлайн-стили (style="...") — специфичность (1, 0, 0, 0)
  3. ID селекторы (#id) — специфичность (0, 1, 0, 0)
  4. Классы (.class), псевдоклассы (:hover, :focus), атрибутные селекторы ([type="text"]) — специфичность (0, 0, 1, 0)
  5. Теги (div, p), псевдоэлементы (::before, ::after) — специфичность (0, 0, 0, 1)
  6. Универсальный селектор (*) — специфичность (0, 0, 0, 0)

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

  • Псевдоклассы (:hover, :nth-child(), :not()) имеют ту же специфичность, что и классы.
  • Псевдоэлементы (::before, ::after, ::first-line) имеют ту же специфичность, что и теги.
  • Комбинаторы (>, +, ~, пробел) не влияют на специфичность.
  • :where() всегда имеет специфичность 0.

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

/* Специфичность: (0, 0, 0, 1) */
div {
color: black;
}

/* Специфичность: (0, 0, 1, 1) */
div.content {
color: blue;
}

/* Специфичность: (0, 1, 0, 0) */
#main {
color: green;
}

/* Специфичность: (0, 1, 1, 1) */
#main.content:hover {
color: red;
}

/* Специфичность: (0, 0, 2, 2) */
ul.nav li.active::before {
content: "→";
}

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

<div id="app" class="container">
<p class="text active">Hello</p>
</div>
/* (0, 0, 0, 1) — проигрывает */
p {
color: black;
}

/* (0, 0, 1, 1) — проигрывает */
.container p {
color: blue;
}

/* (0, 0, 2, 0) — побеждает! */
.text.active {
color: green;
}

/* (0, 1, 0, 1) — победил бы, если бы был */
#app p {
color: red;
}

Рекомендации по работе со специфичностью:

  • Избегайте !important — он ломает естественный каскад CSS.
  • Не злоупотребайте ID селекторами — они слишком специфичны и не переиспользуемы.
  • Используйте классы как основной способ стилизации.
  • Если нужно переопределить стиль, добавьте класс, а не повышайте специфичность.

Итог для подготовки:

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

  • Что такое специфичность и зачем она нужна.
  • Основной порядок: инлайн > ID > классы/псевдоклассы > теги/псевдоэлементы.
  • Что !important — это крайняя мера.

Вопрос 11. Что такое комбинаторы в CSS?.

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

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

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

Ответ собеседника частично верный, но содержит неточности. Давайте разберём тему подробно.

Что такое комбинаторы:

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

Четыре основных комбинатора в CSS:

1. Потомок (descendant) — пробел

Выбирает все элементы-потомки, независимо от глубины вложенности.

/* Все <p> внутри .container, на любом уровне вложенности */
.container p {
color: blue;
}
<div class="container">
<p>Будет синий</p>
<div>
<p>Тоже синий</p>
</div>
</div>

2. Прямой потомок (child) — >

Выбирает только непосредственные дочерние элементы.

/* Только прямые дочерние <p> внутри .container */
.container > p {
color: blue;
}
<div class="container">
<p>Будет синий</p>
<div>
<p>НЕ будет синий — это внук, не прямой потомок</p>
</div>
</div>

3. Соседний брат (adjacent sibling) — +

Выбирает элемент, который непосредственно следует за указанным элементом (на том же уровне вложенности).

/* <p>, который идёт сразу после h2 */
h2 + p {
font-weight: bold;
}
<h2>Заголовок</h2>
<p>Будет жирный — идёт сразу после h2</p>
<p>НЕ будет жирный — не соседний</p>

4. Общий брат (general sibling) — ~

Выбирает все элементы, которые следуют за указанным элементом (на том же уровне вложенности).

/* Все <p>, которые идут после h2 (не обязательно сразу) */
h2 ~ p {
color: green;
}
<h2>Заголовок</h2>
<p>Будет зелёный</p>
<div>Промежуточный элемент</div>
<p>Тоже зелёный</p>

Важное замечание об амперсанде &:

Амперсанд & — это не CSS-комбинатор. Это синтаксис препроцессоров (SASS, LESS) и CSS-модулей, который ссылается на родительский селектор.

// SASS пример
.button {
background: blue;

&:hover {
background: darkblue;
}

&--primary {
background: green;
}
}

// Компилируется в:
.button { background: blue; }
.button:hover { background: darkblue; }
.button--primary { background: green; }

В нативном CSS (без препроцессоров) & используется в правиле @nest (CSS Nesting), которое пока имеет ограниченную поддержку.

Сводная таблица комбинаторов:

КомбинаторСинтаксисОписание
Поток (descendant)A BB внутри A на любой глубине
Прямой потомок (child)A > BB — прямой дочерний элемент A
Соседний брат (adjacent sibling)A + BB идёт сразу после A
Общий брат (general sibling)A ~ BB идёт после A (не обязательно сразу)

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

/* Навигация */
.nav > li {
display: inline-block;
}

/* Подменю показывается только при наведении на прямой потомок */
.nav > li:hover > .submenu {
display: block;
}

/* Первый параграф после заголовка имеет отступ */
h2 + p {
margin-top: 0;
}

/* Все параграфы после заголовка второго уровня */
h2 ~ p {
font-size: 0.9em;
}

Итог для подготовки:

  • Запомните четыре комбинатора: пробел (поток), > (прямой потомок), + (соседний брат), ~ (общий брат).
  • Амперсанд & — это не CSS-комбинатор, а синтаксис препроцессоров.
  • Комбинаторы не влияют на специфичность селектора.
  • Для позиции Go-разработчика это базовые знания, которые показывают общую грамотность.

Вопрос 12. Можно ли внутрь тега button положить другой тег button?.

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

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

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

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

Короткий ответ:

Нет, вкладывать <button> внутрь <button> нельзя. Это нарушает спецификацию HTML и приводит к непредсказуемому поведению.

Почему это нельзя:

1. Нарушение спецификации HTML

Согласно спецификации HTML, элемент <button> имеет phrasing content в качестве разрешённого содержимого. Это означает, что внутри кнопки можно размещать текст, изображения, иконки, но не интерактивные элементы.

Категории контента, которые запрещены внутри <button>:

  • Другие <button>
  • <a> с атрибутом href
  • <input> типа submit, button, checkbox, radio
  • <select>, <textarea>
  • <details>, <summary>
  • Любые элементы с tabindex

2. Непредсказуемое поведение браузеров

Если вложить кнопку в кнопку, браузер может:

  • Игнорировать вложенную кнопку
  • Сломать обработку кликов (какой обработчик должен сработать?)
  • Сломать навигацию с клавиатуры (Tab)
  • Сломать отправку формы

3. Проблемы с accessibility (a11y)

  • Экранные читалки не смогут корректно объяснить структуру
  • Пользователи клавиатуры не смогут понять, какая кнопка активна
  • Нарушается принцип одной действия на одну кнопку

Пример неправильного кода:

<!-- НЕПРАВИЛЬНО: вложенные кнопки -->
<button onclick="openModal()">
Открыть форму
<button onclick="closeModal()">×</button>
</button>

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

А. Использовать отдельные кнопки:

<div class="modal-header">
<h2>Заголовок формы</h2>
<button type="button" class="close-button" onclick="closeModal()" aria-label="Закрыть">
×
</button>
</div>
<button type="button" class="open-button" onclick="openModal()">
Открыть форму
</button>

Б. Использовать <div> с ролью кнопки (если нужна сложная структура):

<!-- Если нужен кликабельный блок с несколькими зонами -->
<div class="card" role="group" aria-label="Действия с карточкой">
<div class="card-content">
<h3>Заголовок</h3>
<p>Описание</p>
</div>
<div class="card-actions">
<button type="button" onclick="editItem()">Редактировать</button>
<button type="button" onclick="deleteItem()">Удалить</button>
</div>
</div>

В. Использовать <a> для навигации и <button> для действий:

<nav class="toolbar">
<a href="/profile" class="toolbar-link">Профиль</a>
<button type="button" class="toolbar-button" onclick="logout()">
Выйти
</button>
</nav>

Что можно вкладывать в <button>:

<!-- Правильное содержимое кнопки -->
<button type="submit">
<svg class="icon"><!-- иконка --></svg>
<span>Отправить</span>
</button>

<button type="button">
<img src="icon.png" alt="">
<strong>Жирный текст</strong>
<em>Курсив</em>
</button>

Итог для подготовки:

  • Вкладывать <button> в <button> нельзя — это нарушает спецификацию HTML.
  • Браузеры могут по-разному обрабатывать такую вложённость, что приводит к багам.
  • Для accessibility это критическая ошибка.
  • Используйте отдельные кнопки или другие элементы для сложных интерфейсов.
  • Для позиции Go-разработчика это базовые знания HTML, которые показывают общую грамотность.

Вопрос 13. Как реализована конкурентность в JavaScript? Расскажи про Event Loop, микротаски и макротаски.

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

Ответ собеседника: Правильный. JavaScript — однопоточный язык, который не блокирует поток благодаря Event Loop. Event Loop есть и в Node.js. Механизм позволяет выполнять ввод-вывод без задержек. Есть микротаски (промисы, queueMicrotask, observer) и макротаски (обработчики событий, setTimeout, setInterval, fetch). Микротаски имеют больший приоритет и выполняются раньше макротасок. Синхронный код попадает сразу в call stack, макротаски — сначала в Web API, потом в callback queue, затем в call stack. Если в процессе выполнения макротаски появляются микротаски, они выполняются до завершения текущей макротаски.

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

Ответ собеседника в целом верный и демонстрирует хорошее понимание Event Loop. Давайте дополним и структурируем информацию.

JavaScript и конкурентность:

JavaScript — однопоточный язык с неблокирующей моделью ввода-вывода. Конкурентность достигается через механизм Event Loop, а не через многопоточность.

Компоненты Event Loop:

1. Call Stack (стек вызовов)

Это структура данных, которая отслеживает текущую выполняемую функцию. Когда функция вызывается — она помещается в стек, когда завершается — извлекается.

function first() {
console.log('first');
second();
console.log('first end');
}

function second() {
console.log('second');
}

first();

// Call Stack:
// 1. [first]
// 2. [first, second]
// 3. [first]
// 4. []

2. Web APIs (браузерные API)

Это API, предоставляемые браузером или Node.js для асинхронных операций:

  • setTimeout, setInterval
  • fetch, XMLHttpRequest
  • DOM-события
  • requestAnimationFrame

3. Callback Queue (очередь макротасок)

Очередь, в которую попадают callback-функции после завершения асинхронных операций.

4. Microtask Queue (очередь микротасок)

Очередь с более высоким приоритетом, чем Callback Queue. Сюда попадают:

  • Promise.then(), Promise.catch(), Promise.finally()
  • queueMicrotask()
  • MutationObserver
  • process.nextTick() (в Node.js)

Порядок выполнения Event Loop:

  1. Выполнить весь синхронный код (Call Stack)
  2. Выполнить все микротаски (Microtask Queue)
  3. Выполнить одну макротаску (Callback Queue)
  4. Повторить шаги 2-3

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

После каждой макротаски Event Loop выполняет все накопившиеся микротаски, прежде чем перейти к следующей макротаске.

Пример для понимания порядка выполнения:

console.log('1: синхронный код');

setTimeout(() => {
console.log('2: макротаска (setTimeout)');

Promise.resolve().then(() => {
console.log('3: микротаска внутри макротаски');
});
}, 0);

Promise.resolve().then(() => {
console.log('4: микротаска (Promise)');

setTimeout(() => {
console.log('5: макротаска внутри микротаски');
}, 0);
});

console.log('6: синхронный код');

// Вывод:
// 1: синхронный код
// 6: синхронный код
// 4: микротаска (Promise)
// 2: макротаска (setTimeout)
// 3: микротаска внутри макротаски
// 5: макротаска внутри микротаски

Более сложный пример:

console.log('start');

setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => console.log('promise in timeout 1'));
}, 0);

Promise.resolve().then(() => {
console.log('promise 1');
setTimeout(() => console.log('timeout in promise 1'), 0);
});

Promise.resolve().then(() => {
console.log('promise 2');
});

console.log('end');

// Вывод:
// start
// end
// promise 1
// promise 2
// timeout 1
// promise in timeout 1
// timeout in promise 1

Сравнение с Go:

Для Go-разработчика полезно провести аналогию:

JavaScriptGo
Event LoopRuntime scheduler
Call StackGoroutine stack
Microtask QueueНет прямого аналога
Callback QueueChannel operations
ОднопоточностьM:N scheduling (M горутин на N потоков ОС)

Пример аналогичного поведения в Go:

package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("1: синхронный код")

// Аналог setTimeout — макротаска
go func() {
time.Sleep(0) // Минимальная задержка
fmt.Println("2: горутина (аналог макротаски)")

// Аналог Promise.then — микротаска
go func() {
fmt.Println("3: вложенная горутина (аналог микротаски)")
}()
}()

// Аналог Promise.then — микротаска
go func() {
fmt.Println("4: горутина (аналог микротаски)")
}()

fmt.Println("5: синхронный код")

time.Sleep(time.Millisecond * 100) // Даём горутинам выполниться
}

Итог для подготовки:

  • JavaScript однопоточный, но неблокирующий благодаря Event Loop.
  • Микротаски (Promise, queueMicrotask) имеют приоритет над макротасками (setTimeout, setInterval).
  • После каждой макротаски выполняются все накопившиеся микротаски.
  • Для позиции Go-разработчика это базовые знания, но понимание конкурентности в любом языке показывает глубину знаний.

Вопрос 14. Если есть 10 микротасок и 2 макротаски, в каком порядке они выполнятся? А если в процессе выполнения первой макротаски появятся ещё 5 микротасок?.

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

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

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

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

Сценарий 1: 10 микротасок и 2 макротаски

// Изначально в очередях:
// Microtask Queue: [m1, m2, m3, m4, m5, m6, m7, m8, m9, m10]
// Macrotask Queue: [M1, M2]

// Порядок выполнения:
// 1. Синхронный код (Call Stack)
// 2. m1, m2, m3, m4, m5, m6, m7, m8, m9, m10 (все микротаски)
// 3. M1 (первая макротаска)
// 4. M2 (вторая макротаска)

Сценарий 2: В процессе M1 появляются 5 микротасок

// Изначально в очередях:
// Microtask Queue: [m1, m2, m3, m4, m5, m6, m7, m8, m9, m10]
// Macrotask Queue: [M1, M2]

// Порядок выполнения:
// 1. Синхронный код
// 2. m1, m2, m3, m4, m5, m6, m7, m8, m9, m10 (все начальные микротаски)
// 3. M1 (первая макротаска) — во время выполнения добавляет m11-m15
// 4. m11, m12, m13, m14, m15 (новые микротаски выполняются сразу после M1)
// 5. M2 (вторая макротаска)

Код для проверки:

console.log('Синхронный код начало');

// Создаём 10 микротасок
for (let i = 1; i <= 10; i++) {
Promise.resolve().then(() => console.log(`Микротаска ${i}`));
}

// Создаём 2 макротаски
setTimeout(() => {
console.log('Макротаска 1');

// В процессе первой макротаски создаём ещё 5 микротасок
for (let i = 11; i <= 15; i++) {
Promise.resolve().then(() => console.log(`Микротаска ${i}`));
}
}, 0);

setTimeout(() => {
console.log('Макротаска 2');
}, 0);

console.log('Синхронный код конец');

// Вывод:
// Синхронный код начало
// Синхронный код конец
// Микротаска 1
// Микротаска 2
// ...
// Микротаска 10
// Макротаска 1
// Микротаска 11
// Микротаска 12
// ...
// Микротаска 15
// Макротаска 2

Ключевые правила Event Loop:

  1. Синхронный код всегда первым — он выполняется сразу в Call Stack.

  2. Микротаски имеют приоритет над макротасками — все микротаски из очереди выполняются перед следующей макротаска.

  3. Микротаски, добавленные во время макротаски, выполняются сразу после текущей макротаски — перед следующей макротаска.

  4. Макротаски выполняются по одной — после каждой проверяется очередь микротасок.

Аналогия из реальной жизни:

Представьте очередь в банке:

  • Call Stack — клиент у окна (обслуживается прямо сейчас)
  • Microtask Queue — VIP-клиенты (обслуживаются в первую очередь после текущего)
  • Macrotask Queue — обычная очередь (обслуживается, когда VIP-клиентов нет)

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

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);
});

Promise.resolve().then(() => console.log('6'));

console.log('7');

// Какой будет вывод?
// Ответ: 1, 7, 4, 6, 2, 3, 5

Итог для подготовки:

  • Запомните порядок: синхронный код → все микротаски → одна макротаска → все микротаски → одна макротаска → ...
  • Микротаски никогда не прерывают выполнение макротаски, но выполняются сразу после неё.
  • Для позиции Go-разработчика это базовые знания, но понимание конкурентности в любом языке показывает глубину знаний.

Вопрос 15. Есть синхронный цикл, который выводит числа. Как сделать его асинхронным, чтобы сначала вывелись числа 1 и 2, а затем все семёрки из цикла?.

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

Ответ собеседника: Неполный. Кандидат затруднился с решением. Обсуждал варианты с async/await и промисами, но не смог самостоятельно написать рабочее решение.

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

Это классическая задача на понимание Event Loop и приоритетности микротасок/макротасок. Давайте разберём её подробно.

Исходный код (синхронный):

console.log(1);

for (let i = 0; i < 5; i++) {
console.log(7);
}

console.log(2);

// Вывод: 1, 7, 7, 7, 7, 7, 2

Задача:

Нужно получить вывод: 1, 2, 7, 7, 7, 7, 7

То есть числа из цикла должны вывестись после числа 2, хотя в коде цикл стоит между 1 и 2.

Решение с использованием setTimeout (макротаска):

console.log(1);

for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(7), 0);
}

console.log(2);

// Вывод: 1, 2, 7, 7, 7, 7, 7

Почему это работает:

  1. console.log(1) — синхронный код, выполняется сразу
  2. setTimeout — регистрирует callback в Macrotask Queue, но не выполняет его
  3. console.log(2) — синхронный код, выполняется сразу
  4. Call Stack пуст → Event Loop берёт макротаски из очереди → выполняются все console.log(7)

Решение с использованием Promise (микротаска):

console.log(1);

for (let i = 0; i < 5; i++) {
Promise.resolve().then(() => console.log(7));
}

console.log(2);

// Вывод: 1, 2, 7, 7, 7, 7, 7

Почему это работает:

  1. console.log(1) — синхронный код
  2. Promise.resolve().then() — добавляет callback в Microtask Queue
  3. console.log(2) — синхронный код
  4. Call Stack пуст → Event Loop проверяет Microtask Queue → выполняются все console.log(7)

Важное замечание:

Оба решения работают, но с разными типами задач:

  • Promise.then() — микротаска (выполняется раньше)
  • setTimeout() — макротаска (выполняется позже)

Сложная версия задачи:

Что, если использовать оба варианта одновременно?

console.log(1);

for (let i = 0; i < 3; i++) {
setTimeout(() => console.log('timeout', i), 0);
}

for (let i = 0; i < 3; i++) {
Promise.resolve().then(() => console.log('promise', i));
}

console.log(2);

// Вывод: 1, 2, promise 0, promise 1, promise 2, timeout 0, timeout 1, timeout 2

Продвинутое решение с async/await:

async function main() {
console.log(1);

for (let i = 0; i < 5; i++) {
// Испускаем микротаску через await
await Promise.resolve();
console.log(7);
}

console.log(2);
}

main();

// Вывод: 1, 7, 7, 7, 7, 7, 2
// Но это НЕ то, что нам нужно!

Правильное решение с async/await:

async function main() {
console.log(1);

// Создаём массив промисов
const promises = [];
for (let i = 0; i < 5; i++) {
promises.push(Promise.resolve().then(() => console.log(7)));
}

console.log(2);

// Ждём завершения всех промисов
await Promise.all(promises);
}

main();

// Вывод: 1, 2, 7, 7, 7, 7, 7

Аналогия в Go:

Для понимания, как это работает в Go:

package main

import (
"fmt"
"sync"
)

func main() {
fmt.Println(1)

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(7)
}()
}

fmt.Println(2)

wg.Wait() // Ждём завершения всех горутин
}

// Вывод: 1, 2, затем семёрки в произвольном порядке

Итог для подготовки:

  • Используйте setTimeout(fn, 0) или Promise.resolve().then(fn) для отложенного выполнения.
  • Микротаски (Promise) выполняются раньше макротасок (setTimeout).
  • Синхронный код всегда выполняется первым.
  • Для позиции Go-разработчика это задача на понимание конкурентности, что полезно для общего кругозора.

Вопрос 16. Что такое Web Workers и для чего они они используются?.

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

Ответ собеседника: Правильный. Web Workers — это механизм, позволяющий выполнять сложные вычисления в отдельном потоке, чтобы интерфейс не зависал. Задача выполняется с интервалом до 16 мс, что предотвращает блокировку UI.

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

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

Что такое Web Workers:

Web Workers — это API браузера, которое позволяет выполнять JavaScript-код в отдельном потоке (thread), параллельно с основным потоком (main thread).

Ключевые характеристики:

1. Отдельный поток выполнения

Web Worker работает в своём потоке, отдельном от основного потока, где выполняется UI-код.

2. Нет доступа к DOM

Worker не может напрямую манипулировать DOM, обращаться к window, document или другим объектам основного потока.

3. Коммуникация через сообщения

Основной поток и Worker общаются через механизм postMessage и onmessage.

4. Собственный контекст

Worker имеет свой глобальный объект (self вместо window), свои переменные и функции.

Типы Web Workers:

ТипОписаниеДоступность
Dedicated WorkerПривязан к одной страницеВсе браузеры
Shared WorkerДоступен нескольким страницамChrome, Firefox, Edge
Service WorkerПрокси для сетевых запросов, работает offlineВсе современные браузеры

Пример использования Dedicated Worker:

Основной файл (main.js):

// Создаём Worker
const worker = new Worker('worker.js');

// Отправляем данные в Worker
worker.postMessage({ type: 'CALCULATE', data: 1000000 });

// Получаем результат от Worker
worker.onmessage = function(event) {
console.log('Результат из Worker:', event.data);
};

// Обработка ошибок
worker.onerror = function(error) {
console.error('Ошибка в Worker:', error);
};

// Завершение Worker
// worker.terminate();

Worker файл (worker.js):

// Получаем данные из основного потока
self.onmessage = function(event) {
const { type, data } = event.data;

if (type === 'CALCULATE') {
// Выполняем тяжёлые вычисления
const result = heavyCalculation(data);

// Отправляем результат обратно
self.postMessage(result);
}
};

function heavyCalculation(n) {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += Math.sqrt(i);
}
return sum;
}

Передача больших данных с помощью Transferable Objects:

// Основной поток
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100 МБ

// Передача без копирования (ownership transfer)
worker.postMessage({ buffer }, [buffer]);

// После этого buffer недоступен в основном потоке
console.log(buffer.byteLength); // 0

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

1. Обработка изображений:

// worker.js
self.onmessage = function(event) {
const { imageData, filter } = event.data;

// Применяем фильтр к каждому пикселю
for (let i = 0; i < imageData.data.length; i += 4) {
// Пример: чёрно-белый фильтр
const avg = (imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2]) / 3;
imageData.data[i] = avg;
imageData.data[i + 1] = avg;
imageData.data[i + 2] = avg;
}

self.postMessage({ imageData }, [imageData.data.buffer]);
};

2. Парсинг больших JSON:

// worker.js
self.onmessage = function(event) {
const jsonString = event.data;

// Парсим большой JSON без блокировки UI
const data = JSON.parse(jsonString);

// Обрабатываем данные
const processed = data.map(item => ({
...item,
processed: true,
timestamp: Date.now()
}));

self.postMessage(processed);
};

3. Шифрование/хеширование:

// worker.js
self.onmessage = function(event) {
const { password, salt } = event.data;

// Тяжёлая операция хеширования
const hash = bcrypt.hashSync(password, salt);

self.postMessage({ hash });
};

Ограничения Web Workers:

  • Нет доступа к DOM
  • Нет доступа к window, document, parent
  • Нельзя использовать некоторые методы alert(), confirm()
  • Ограниченный доступ к API (но есть fetch, IndexedDB, WebSockets)

Аналогия в Go:

Web Workers можно сравнить с горутинами в Go:

package main

import (
"fmt"
"math"
"sync"
)

func worker(id int, jobs <-chan int, results chan<- float64, wg *sync.WaitGroup) {
defer wg.Done()

for n := range jobs {
// Тяжёлые вычисления
sum := 0.0
for i := 0; i < n; i++ {
sum += math.Sqrt(float64(i))
}
results <- sum
fmt.Printf("Worker %d completed job %d\n", id, n)
}
}

func main() {
const numJobs = 5
const numWorkers = 3

jobs := make(chan int, numJobs)
results := make(chan float64, numJobs)

var wg sync.WaitGroup

// Запускаем workers
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}

// Отправляем задачи
for j := 1; j <= numJobs; j++ {
jobs <- j * 100000
}
close(jobs)

// Ждём завершения
go func() {
wg.Wait()
close(results)
}()

// Получаем результаты
for result := range results {
fmt.Println("Result:", result)
}
}

Итог для подготовки:

  • Web Workers позволяют выполнять код в отдельном потоке, не блокируя UI.
  • Они не имеют доступа к DOM и общаются через postMessage.
  • Используются для тяжёлых вычислений, обработки данных, парсинга.
  • Для позиции Go-разработчика это базовые знания фронтенда, но понимание многопоточности полезно.

Вопрос 17. Что такое прототип в JavaScript?.

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

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

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

Ответ собеседника верный, но слишком краткий. Давайте разберём тему подробно.

Что такое прототип:

Прототип — это механизм наследования в JavaScript, при котором объекты могут наследовать свойства и методы от других объектов. Каждый объект в JavaScript имеет скрытое свойство [[Prototype]], которое ссылается на другой объект (его прототип) или null.

Цепочка прототипов (Prototype Chain):

Когда вы обращаетесь к свойству или методу объекта, JavaScript сначала ищет его в самом объекте. Если не находит — ищет в прототипе, затем в прототипе прототипа, и так далее, пока не найдёт или не дойдёт до null.

myObject → parentObject → grandparentObject → ... → Object.prototype → null

Способы работы с прототипами:

1. Создание объекта с прототипом через Object.create():

const animal = {
type: 'Animal',
speak() {
console.log(`${this.name} makes a sound`);
}
};

const dog = Object.create(animal);
dog.name = 'Rex';
dog.bark = function() {
console.log(`${this.name} barks!`);
};

dog.speak(); // "Rex makes a sound" — унаследовано от animal
dog.bark(); // "Rex barks!" — собственный метод

console.log(dog.hasOwnProperty('name')); // true
console.log(dog.hasOwnProperty('speak')); // false

2. Функции-конструкторы:

function Person(name, age) {
this.name = name;
this.age = age;
}

// Методы добавляются в прототип, а не в каждый экземпляр
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};

Person.prototype.getAge = function() {
return this.age;
};

const alice = new Person('Alice', 30);
const bob = new Person('Bob', 25);

alice.greet(); // "Hello, my name is Alice"
bob.greet(); // "Hello, my name is Bob"

// Оба объекта используют один и тот же метод из прототипа
console.log(alice.greet === bob.greet); // true

3. Классы (синтаксический сахар над прототипами):

class Vehicle {
constructor(brand, model) {
this.brand = brand;
this.model = model;
}

info() {
return `${this.brand} ${this.model}`;
}
}

class Car extends Vehicle {
constructor(brand, model, doors) {
super(brand, model);
this.doors = doors;
}

carInfo() {
return `${this.info()}, ${this.doors} doors`;
}
}

const myCar = new Car('Toyota', 'Camry', 4);
console.log(myCar.info()); // "Toyota Camry"
console.log(myCar.carInfo()); // "Toyota Camry, 4 doors"

Получение и установка прототипа:

const parent = { x: 10 };
const child = { y: 20 };

// Получить прототип
console.log(Object.getPrototypeOf(child)); // {} (Object.prototype)

// Установить прототип
Object.setPrototypeOf(child, parent);

// Теперь child наследует от parent
console.log(child.x); // 10 — унаследовано от parent
console.log(child.y); // 20 — собственное свойство

Проверка прототипов:

function Dog(name) {
this.name = name;
}

const rex = new Dog('Rex');

// Проверка прототипа
console.log(rex instanceof Dog); // true
console.log(rex instanceof Object); // true

// Проверка наличия прототипа
console.log(Dog.prototype.isPrototypeOf(rex)); // true

// Получение прототипа
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true

Прототипное наследование vs классическое:

Прототипное (JavaScript)Классическое (Java, C++)
Объекты наследуют от объектовКлассы наследуют от классов
Динамическое наследованиеСтатическое наследование
Можно менять прототип во время выполненияСтруктура фиксирована при компиляции
Композиция через делегированиеКомпозиция через наследование

Аналогия в Go:

В Go нет прототипного наследования, но есть композиция через встраивание (embedding):

package main

import "fmt"

// Базовая структура (аналог прототипа)
type Animal struct {
Name string
}

func (a *Animal) Speak() {
fmt.Printf("%s makes a sound\n", a.Name)
}

// Структура с встраиванием (аналог наследования)
type Dog struct {
Animal // Встраивание — аналог прототипа
Breed string
}

func (d *Dog) Bark() {
fmt.Printf("%s barks!\n", d.Name)
}

func main() {
dog := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}

dog.Speak() // "Rex makes a sound" — унаследовано
dog.Bark() // "Rex barks!" — собственный метод
}

Проблемы прототипного наследования:

1. Разделяемые свойства:

function User(name) {
this.name = name;
}

// Массив в прототипе будет разделяться между всеми экземплярами
User.prototype.friends = [];

const alice = new User('Alice');
const bob = new User('Bob');

alice.friends.push('Charlie');
console.log(bob.friends); // ['Charlie'] — неожиданно!

Решение:

function User(name) {
this.name = name;
this.friends = []; // Каждый экземпляр получает свой массив
}

2. Отсутствие приватных свойств (до ES2022):

function Counter() {
this.count = 0; // Публичное свойство
}

Counter.prototype.increment = function() {
this.count++;
};

const counter = new Counter();
counter.increment();
counter.count = 100; // Можно изменить напрямую

Итог для подготовки:

  • Прототип — это механизм наследования, при котором объекты делегируют свойства и методы другим объектам.
  • Цепочка прототипов позволяет искать свойства вверх по иерархии.
  • Классы в JavaScript — это синтаксический сахар над прототипами.
  • Для позиции Go-разработчика это базовые знания JavaScript, но понимание ООП-паттернов полезно.

Вопрос 18. Что такое функция-конструктор и что делает оператор new?.

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

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

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

Это важный вопрос для понимания объектно-ориентированного программирования в JavaScript. Давайте разберём его подробно.

Функция-конструктор:

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

function Person(name, age) {
this.name = name;
this.age = age;

this.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
}

const alice = new Person('Alice', 30);
alice.greet(); // "Hello, my name is Alice"

Что делает оператор new (пошагово):

Оператор new выполняет следующие действия:

  1. Создаёт новый пустой объект: {}
  2. Устанавливает прототип объекта: obj.__proto__ = Constructor.prototype
  3. Привязывает this к новому объекту и выполняет тело функции
  4. Возвращает объект:
    • Если функция вернула объект — возвращает его
    • Если функция вернула примитив или ничего — возвращает созданный объект

Реализация new вручную:

function myNew(Constructor, ...args) {
// 1. Создаём новый объект
const obj = {};

// 2. Устанавливаем прототип
Object.setPrototypeOf(obj, Constructor.prototype);

// 3. Вызываем конструктор с привязкой this
const result = Constructor.apply(obj, args);

// 4. Возвращаем объект (или результат, если это объект)
return result instanceof Object ? result : obj;
}

// Использование
function Person(name, age) {
this.name = name;
this.age = age;
}

const alice = myNew(Person, 'Alice', 30);
console.log(alice.name); // "Alice"
console.log(alice.age); // 30

Примеры поведения new:

А. Обычный случай:

function Car(brand) {
this.brand = brand;
}

const myCar = new Car('Toyota');
console.log(myCar.brand); // "Toyota"
console.log(myCar instanceof Car); // true

Б. Возврат объекта из конструктора:

function Foo() {
this.x = 10;
return { y: 20 }; // Возвращаем другой объект
}

const foo = new Foo();
console.log(foo.x); // undefined — this не вернулся
console.log(foo.y); // 20 — вернулся явно указанный объект

В. Возврат примитива из конструктора:

function Bar() {
this.x = 10;
return 42; // Примитив игнорируется
}

const bar = new Bar();
console.log(bar.x); // 10 — вернулся this

Г. Стрелочные функции не могут быть конструкторами:

const Foo = () => {};
const bar = new Foo(); // TypeError: Foo is not a constructor

Стрелочные функции не имеют своего this и prototype, поэтому не могут использоваться с new.

Функция-конструктор vs генератор:

Кандидат перепутал функцию-конструктор с генератором. Вот разница:

Функция-конструкторГенератор
function Person() {}function* generator() {}
Используется с newВызывается как обычная функция
Возвращает объектВозвращает итератор
Выполняется сразуВыполняется пошагово через yield
// Функция-конструктор
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');

// Генератор
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }

Аналогия в Go:

В Go нет конструкторов в классическом смысле, но есть функции-фабрики:

package main

import "fmt"

type Person struct {
name string
age int
}

// Функция-фабрика (аналог конструктора)
func NewPerson(name string, age int) *Person {
return &Person{
name: name,
age: age,
}
}

// Метод
func (p *Person) Greet() {
fmt.Printf("Hello, my name is %s\n", p.name)
}

func main() {
alice := NewPerson("Alice", 30)
alice.Greet() // "Hello, my name is Alice"
}

Итог для подготовки:

  • Функция-конструктор — это обычная функция, вызываемая с new.
  • new создаёт объект, устанавливает прототип, привязывает this, возвращает объект.
  • Генератор — это совершенно другой механизм (function*, yield).
  • Для позиции Go-разработчика это базовые знания JavaScript, но понимание ООП-паттернов полезно.

Вопрос 19. Что такое this в JavaScript и можно ли использовать его вне функции?.

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

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

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

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

Что такое this:

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

Правила определения this:

1. Глобальный контекст

// В браузере
console.log(this); // window

// В Node.js
console.log(this); // {} (пустой объект модуля)

// В strict mode
'use strict';
console.log(this); // undefined (в функциях)

2. Контекст объекта

const person = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
}
};

person.greet(); // "Hello, Alice" — this ссылается на person

3. Контекст функции-конструктора

function Person(name) {
this.name = name;
}

const alice = new Person('Alice');
console.log(alice.name); // "Alice" — this ссылается на новый объект

4. Явная привязка (call, apply, bind)

function greet() {
console.log(`Hello, ${this.name}`);
}

const person = { name: 'Alice' };

greet.call(person); // "Hello, Alice"
greet.apply(person); // "Hello, Alice"

const boundGreet = greet.bind(person);
boundGreet(); // "Hello, Alice"

5. Стрелочные функции

Стрелочные функции не имеют своего this. Они захватывают this из окружающего лексического контекста.

const person = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
},
greetArrow: () => {
console.log(`Hello, ${this.name}`);
}
};

person.greet(); // "Hello, Alice"
person.greetArrow(); // "Hello, undefined" — this из глобального контекста

Проблема с потерей контекста:

const person = {
name: 'Alice',
greet() {
console.log(`Hello, ${this.name}`);
}
};

// Проблема: this теряется
const greet = person.greet;
greet(); // "Hello, undefined" — this не привязан

// Решение 1: bind
const boundGreet = person.greet.bind(person);
boundGreet(); // "Hello, Alice"

// Решение 2: стрелочная функция
const person2 = {
name: 'Alice',
greet: () => {
console.log(`Hello, ${this.name}`);
}
};
// Но это не работает для методов объекта!

this в обработчиках событий:

class Button {
constructor(element) {
this.element = element;
this.text = 'Click me';

// Проблема: this теряется в обработчике
// this.element.addEventListener('click', this.handleClick);

// Решение 1: bind
this.element.addEventListener('click', this.handleClick.bind(this));

// Решение 2: стрелочная функция
this.element.addEventListener('click', () => this.handleClick());
}

handleClick() {
console.log(this.text);
}
}

Сводная таблица правил определения this:

КонтекстЗначение this
Глобальный контекстwindow (браузер) / {} (Node.js)
Метод объектаОбъект, вызвавший метод
Функция-конструкторНовый созданный объект
call/apply/bindЯвно указанный объект
Стрелочная функцияthis из окружающего контекста
Обработчик событий (обычная функция)Элемент, на котором произошло событие
Обработчик событий (стрелочная функция)this из окружающего контекста

Аналогия в Go:

В Go нет this, но есть получатель метода (receiver):

package main

import "fmt"

type Person struct {
name string
}

// Получатель метода — аналог this
func (p *Person) Greet() {
fmt.Printf("Hello, %s\n", p.name)
}

func main() {
alice := &Person{name: "Alice"}
alice.Greet() // "Hello, Alice"
}

Итог для подготовки:

  • this определяется способом вызова функции, а не местом объявления.
  • Стрелочные функции не имеют своего this — они захватывают его из окружения.
  • call, apply, bind позволяют явно задать this.
  • Для позиции Go-разработчика это базовые знания JavaScript, но понимание контекста выполнения полезно.

Вопрос 20. Что такое React и Virtual DOM? Расскажи подробнее о том, как работает Virtual DOM.

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

Ответ собеседника: Неполный. React — это библиотека для построения SPA, использующая Virtual DOM. Virtual DOM — это облегчённая версия DOM-дерева в оперативной памяти. При изменении состояния создаётся копия Virtual DOM, происходит сравнение (diffing) и через reconciliation применяются изменения к реальному DOM. Кандидат не знал, что Virtual DOM — это обычный JavaScript-объект, и не до конца разобрался в деталях.

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

Это один из ключевых вопросов для понимания React. Давайте разберём его подробно.

Что такое React:

React — это библиотека для построения пользовательских интерфейсов, разработанная Facebook (Meta) и выпущенная в 2013 году. React использует компонентный подход и Virtual DOM для эффективного обновления UI.

Что такое Virtual DOM:

Virtual DOM — это легковесное представление реального DOM в виде JavaScript-объектов. Это не специфичная для React концепция, а паттерн, который помогает оптимизировать работу с DOM.

Почему Virtual DOM «облегчённый»:

Реальный DOM — это сложная структура с множеством свойств и методов. Например, один элемент <div> в реальном DOM имеет сотни свойств:

// Реальный DOM-элемент
const element = document.createElement('div');
console.log(Object.keys(element).length); // 200+ свойств

Virtual DOM хранит только необходимую информацию:

// Virtual DOM элемент (упрощённо)
const virtualElement = {
type: 'div',
props: {
className: 'container',
children: [
{ type: 'h1', props: { children: 'Hello' } },
{ type: 'p', props: { children: 'World' } }
]
}
};

Как работает Virtual DOM (пошагово):

1. Начальный рендер:

function App() {
const [count, setCount] = useState(0);

return (
<div className="app">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

React создаёт Virtual DOM:

{
type: 'div',
props: {
className: 'app',
children: [
{
type: 'h1',
props: { children: ['Count: ', 0] }
},
{
type: 'button',
props: {
onClick: function() { ... },
children: 'Increment'
}
}
]
}
}

2. Изменение состояния:

Когда пользователь нажимает кнопку, setCount(1) вызывает ре-рендер.

3. Создание нового Virtual DOM:

{
type: 'div',
props: {
className: 'app',
children: [
{
type: 'h1',
props: { children: ['Count: ', 1] } // Изменилось!
},
{
type: 'button',
props: {
onClick: function() { ... },
children: 'Increment'
}
}
]
}
}

4. Diffing (сравнение):

React сравнивает старый и новый Virtual DOM:

  • <div> — тип не изменился, пропсы не изменились → пропускаем
  • <h1> — тип не изменился, но изменились children → обновляем текст
  • <button> — ничего не изменилось → пропускаем

5. Reconciliation (применение изменений):

React применяет минимальный набор изменений к реальному DOM:

// Только это изменение применяется к реальному DOM
h1Element.textContent = 'Count: 1';

Алгоритм Diffing:

React использует эвристический алгоритм сравнения с двумя ключевыми допущениями:

1. Разные типы элементов дают разные деревья:

// Старый
<div>
<Counter />
</div>

// Новый
<span>
<Counter />
</span>

// React уничтожит div и создаст span с нуля

2. Ключ (key) помогает идентифицировать элементы:

// Без ключей — React не может понять, какой элемент переместился
<ul>
<li>First</li>
<li>Second</li>
</ul>

// С ключами — React понимает, что элемент переместился
<ul>
<li key="1">First</li>
<li key="2">Second</li>
</ul>

Пример оптимизации с keys:

function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
);
}

Без key React будет перерисовывать все элементы при каждом изменении. С key он понимает, какой элемент добавился/удалился/переместился.

Fiber Architecture (React 16+):

Начиная с React 16, используется Fiber — новый алгоритм reconciliation:

  • Разбивает работу на чанки (chunks)
  • Позволяет прерывать рендеринг для приоритетных задач
  • Включает поддержку Concurrent Mode
// Упрощённая структура Fiber-узла
const fiber = {
type: 'div',
props: { className: 'app' },
child: Fiber, // Первый дочерний узел
sibling: Fiber, // Следующий братский узел
return: Fiber, // Родительский узел
stateNode: DOMElement, // Ссылка на реальный DOM-элемент
effectTag: 'UPDATE', // Тип изменения
};

Аналогия в Go:

В Go нет Virtual DOM, но есть похожие концепции в веб-фреймворках:

package main

import (
"html/template"
"net/http"
)

type PageData struct {
Count int
}

func main() {
tmpl := template.Must(template.New("page").Parse(`
<div class="app">
<h1>Count: {{.Count}}</h1>
<button onclick="increment()">Increment</button>
</div>
`))

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := PageData{Count: 0}
tmpl.Execute(w, data)
})

http.ListenAndServe(":8080", nil)
}

Итог для подготовки:

  • Virtual DOM — это JavaScript-объект, представляющий структуру UI.
  • Он «облегчённый», потому что хранит только необходимую информацию.
  • Diffing сравнивает старый и новый Virtual DOM.
  • Reconciliation применяет минимальные изменения к реальному DOM.
  • Keys помогают React идентифицировать элементы при перестановке.
  • Для позиции Go-разработчика это базовые знания фронтенда, но понимание оптимизации рендеринга полезно.

Вопрос 21. Для чего нужен useCallback в React и как он работает?.

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

Ответ собеседника: Неполный. useCallback мемоизирует саму функцию, а не результат, как useMemo. Это позволяет избежать лишних перерисовок дочерних компонентов, которые получают эту функцию как prop. Кандидат правильно понял идею мемоизации функции, но не смог чётко объяснить практическую пользу.

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

Это важный вопрос для понимания оптимизации в React. Давайте разберём его подробно.

Что такое useCallback:

useCallback — это хук React, который возвращает мемоизированную версию функции. Функция пересоздаётся только при изменении зависимостей.

const memoizedCallback = useCallback(
() => {
// тело функции
},
[dependencies] // массив зависимостей
);

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

В JavaScript функции — это объекты. При каждом рендере компонента создаётся новая функция с новым адресом в памяти.

function Parent() {
const [count, setCount] = useState(0);

// При каждом рендере создаётся НОВАЯ функция
const handleClick = () => {
console.log('Clicked');
};

return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</div>
);
}

Даже если handleClick делает одно и то же, при каждом рендере Parent передаёт Child новую ссылку на функцию. Это приводит к ре-рендеру Child.

Решение с useCallback:

function Parent() {
const [count, setCount] = useState(0);

// Функция пересоздаётся только при изменении зависимостей
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Пустой массив зависимостей — функция создаётся один раз

return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</div>
);
}

useCallback vs useMemo:

useCallbackuseMemo
Мемоизирует функциюМемоизирует результат вычисления
useCallback(fn, deps)useMemo(() => fn, deps)
Возвращает функциюВозвращает значение
// Эти два варианта эквивалентны:
const memoizedFn = useCallback(() => compute(a, b), [a, b]);
const memoizedFn = useMemo(() => () => compute(a, b), [a, b]);

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

import React, { useState, useCallback, memo } from 'react';

// Дочерний компонент обёрнут в memo
const ExpensiveChild = memo(({ onAction, label }) => {
console.log(`Rendering: ${label}`);

return (
<button onClick={onAction}>
{label}
</button>
);
});

function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');

// Без useCallback — функция пересоздаётся при каждом рендере
// const handleClick = () => console.log('Clicked');

// С useCallback — функция стабильна
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>

<input
value={text}
onChange={(e) => setText(e.target.value)}
/>

{/* ExpensiveChild НЕ будет ре-рендериться при изменении count или text */}
<ExpensiveChild onAction={handleClick} label="Action" />
</div>
);
}

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

  1. Функция передаётся в мемоизированный компонент (memo):
const MemoChild = memo(Child);

function Parent() {
const handleClick = useCallback(() => {
// ...
}, []);

return <MemoChild onClick={handleClick} />;
}
  1. Функция используется в зависимостях других хуков:
useEffect(() => {
const result = fetchData(query);
// ...
}, [query, fetchData]); // fetchData должен быть стабильным

const fetchData = useCallback((q) => {
return api.getData(q);
}, []);
  1. Функция передаётся в контекст:
const AppContext = createContext();

function AppProvider({ children }) {
const [user, setUser] = useState(null);

const login = useCallback((userData) => {
setUser(userData);
}, []);

const logout = useCallback(() => {
setUser(null);
}, []);

const value = useMemo(() => ({
user,
login,
logout
}), [user, login, logout]);

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

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

  1. Компонент не обёрнут в memo и не получает функцию как prop:
function Parent() {
// Нет смысла в useCallback — функция не передаётся никуда
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);

return <button onClick={handleClick}>Click</button>;
}
  1. Функция используется только в текущем компоненте:
function Component() {
// useCallback не нужен — функция не передаётся дочерним компонентам
const handleSubmit = useCallback((e) => {
e.preventDefault();
// ...
}, []);

return <form onSubmit={handleSubmit}>...</form>;
}

Аналогия в Go:

В Go нет прямого аналога useCallback, но есть похожие концепции с замыканиями:

package main

import "fmt"

type Button struct {
OnClick func()
}

func NewButton(onClick func()) *Button {
return &Button{OnClick: onClick}
}

func main() {
count := 0

// Создаём замыкание (аналог useCallback с зависимостями)
handleClick := func() {
count++
fmt.Println("Count:", count)
}

button := NewButton(handleClick)
button.OnClick() // Count: 1
button.OnClick() // Count: 2
}

Итог для подготовки:

  • useCallback мемоизирует функцию, предотвращая её пересоздание при каждом рендере.
  • Используется для оптимизации дочерних компонентов, обёрнутых в memo.
  • Не стоит использовать везде — только когда есть реальная проблема с производительностью.
  • Для позиции Go-разработчика это базовые знания React, но понимание мемозации полезно.

Вопрос 22. Что такое React.memo и как он работает?.

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

Ответ собеседника: Неполный. React.memo — это компонент высшего порядка, который мемоизирует компонент. Он предотвращает повторный рендер компонента, если его props не изменились. Кандидат правильно описал базовую идею, но не смог чётко сформулировать условия работы.

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

Это важный вопрос для понимания оптимизации в React. Давайте разберём его подробно.

Что такое React.memo:

React.memo — это компонент высшего порядка (Higher-Order Component, HOC), который мемоизирует результат рендеринга функционального компонента. Если props не изменились, React использует результат предыдущего рендера.

const MemoizedComponent = React.memo(Component);

Как работает React.memo:

  1. При рендере родительского компонента React проверяет, изменились ли props дочернего компонента.
  2. Если props не изменились — React пропускает рендер дочернего компонента.
  3. Если props изменились — React рендерит компонент заново.

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

import React, { useState, memo } from 'react';

// Обычный компонент — рендерится при каждом рендере родителя
function RegularChild({ name }) {
console.log('RegularChild rendered');
return <p>Hello, {name}!</p>;
}

// Мемоизированный компонент — рендерится только при изменении props
const MemoizedChild = memo(function MemoizedChild({ name }) {
console.log('MemoizedChild rendered');
return <p>Hello, {name}!</p>;
});

function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Alice');

return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>

{/* Рендерится при каждом клике на Count */}
<RegularChild name={name} />

{/* НЕ рендерится при клике на Count */}
<MemoizedChild name={name} />
</div>
);
}

Поведение по умолчанию (shallow comparison):

React.memo по умолчанию использует поверхностное сравнение (shallow comparison) props:

// Поверхностное сравнение проверяет:
// 1. Примитивы: 5 === 5 → true, 'hello' === 'hello' → true
// 2. Объекты/массивы: {} === {} → false, [] === [] → false (разные ссылки)
// 3. Функции: () => {} === () => {} → false (разные ссылки)

Проблема с объектами и функциями:

function Parent() {
const [count, setCount] = useState(0);

// При каждом рендере создаётся новый объект
const user = { name: 'Alice', age: 30 };

// При каждом рендере создаётся новая функция
const handleClick = () => console.log('Clicked');

return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>

{/* React.memo НЕ поможет — объект всегда новый */}
<MemoizedChild user={user} onClick={handleClick} />
</div>
);
}

Решение с useMemo и useCallback:

function Parent() {
const [count, setCount] = useState(0);

// Мемоизируем объект
const user = useMemo(() => ({ name: 'Alice', age: 30 }), []);

// Мемоизируем функцию
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);

return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>

{/* Теперь React.memo работает — props стабильны */}
<MemoizedChild user={user} onClick={handleClick} />
</div>
);
}

Кастомная функция сравнения:

Можно передать свою функцию сравнения как второй аргумент:

const MemoizedChild = memo(
function Child({ user }) {
return <p>{user.name}</p>;
},
(prevProps, nextProps) => {
// Возвращаем true, если рендер НЕ нужен
// Возвращаем false, если рендер нужен
return prevProps.user.id === nextProps.user.id;
}
);

Сравнение React.memo и shouldComponentUpdate:

React.memoshouldComponentUpdate
Для функциональных компонентовДля классовых компонентов
Сравнивает только propsСравнивает props и state
По умолчанию shallow comparisonПолный контроль
// Классовый компонент с shouldComponentUpdate
class Child extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.name !== this.props.name;
}

render() {
return <p>Hello, {this.props.name}!</p>;
}
}

// Эквивалент с React.memo
const Child = memo(
function Child({ name }) {
return <p>Hello, {name}!</p>;
},
(prevProps, nextProps) => prevProps.name === nextProps.name
);

Когда использовать React.memo:

  1. Компонент рендерится часто с одинаковыми props:
const ListItem = memo(function ListItem({ item, onSelect }) {
return (
<li onClick={() => onSelect(item.id)}>
{item.name}
</li>
);
});

function List({ items }) {
const [selectedId, setSelectedId] = useState(null);

return (
<ul>
{items.map(item => (
<ListItem
key={item.id}
item={item}
onSelect={setSelectedId}
/>
))}
</ul>
);
}
  1. Компонент выполняет дорогие вычисления:
const ExpensiveChart = memo(function ExpensiveChart({ data }) {
// Дорогие вычисления для построения графика
const processedData = processData(data);

return <Chart data={processedData} />;
});

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

  1. Props часто меняются:
// Нет смысла — timestamp всегда новый
const MemoizedClock = memo(Clock);
<MemoizedClock timestamp={Date.now()} />
  1. Компонент простой и рендерится быстро:
// Нет смысла — компонент слишком простой
const MemoizedText = memo(function Text({ children }) {
return <span>{children}</span>;
});

Аналогия в Go:

В Go нет прямого аналога React.memo, но есть похожие концепции с кэшированием:

package main

import (
"fmt"
"sync"
)

// Простой кэш для мемоизации
type MemoCache struct {
mu sync.RWMutex
cache map[string]interface{}
}

func NewMemoCache() *MemoCache {
return &MemoCache{
cache: make(map[string]interface{}),
}
}

func (c *MemoCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.cache[key]
return val, ok
}

func (c *MemoCache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.cache[key] = value
}

func main() {
cache := NewMemoCache()

// Имитация мемоизации
key := "expensive_computation"

if result, ok := cache.Get(key); ok {
fmt.Println("From cache:", result)
} else {
result := expensiveComputation()
cache.Set(key, result)
fmt.Println("Computed:", result)
}
}

func expensiveComputation() int {
// Дорогие вычисления
return 42
}

Итог для подготовки:

  • React.memo предотвращает ре-рендер компонента, если props не изменились.
  • По умолчанию использует поверхностное сравнение.
  • Для объектов и функций нужны useMemo и useCallback.
  • Не стоит использовать везде — только когда есть реальная проблема с производительностью.
  • Для позиции Go-разработчика это базовые знания React, но понимание мемоизации полезно.

Вопрос 23. Напиши функцию-конвертер, которая принимает массив объектов пользователей (с полями id, name, age, score) и возвращает объект, где ключи — это возраст, а значения — объекты с массивом пользователей данного возраста и их средним баллом (averageScore).

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

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

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

Это классическая задача на работу с массивами и объектами. Давайте разберём её подробно.

Условие задачи:

Написать функцию, которая:

  • Принимает: массив объектов { id, name, age, score }
  • Возвращает: объект, где ключи — возраст, значения — { users: [...], averageScore: number }

Пример входных данных:

const users = [
{ id: 1, name: 'Alice', age: 25, score: 85 },
{ id: 2, name: 'Bob', age: 30, score: 90 },
{ id: 3, name: 'Charlie', age: 25, score: 95 },
{ id: 4, name: 'David', age: 30, score: 80 },
{ id: 5, name: 'Eve', age: 25, score: 70 },
];

Ожидаемый результат:

{
25: {
users: [
{ id: 1, name: 'Alice', age: 25, score: 85 },
{ id: 3, name: 'Charlie', age: 25, score: 95 },
{ id: 5, name: 'Eve', age: 25, score: 70 }
],
averageScore: 83.33 // (85 + 95 + 70) / 3
},
30: {
users: [
{ id: 2, name: 'Bob', age: 30, score: 90 },
{ id: 4, name: 'David', age: 30, score: 80 }
],
averageScore: 85 // (90 + 80) / 2
}
}

Решение 1: Простой подход с forEach

function groupUsersByAge(users) {
const result = {};

// Группируем пользователей по возрасту
users.forEach(user => {
const age = user.age;

if (!result[age]) {
result[age] = {
users: [],
totalScore: 0
};
}

result[age].users.push(user);
result[age].totalScore += user.score;
});

// Вычисляем средний балл для каждой группы
Object.keys(result).forEach(age => {
const group = result[age];
group.averageScore = Math.round((group.totalScore / group.users.length) * 100) / 100;
delete group.totalScore; // Удаляем временное поле
});

return result;
}

Решение 2: С использованием reduce

function groupUsersByAge(users) {
// Первый проход: группировка и подсчёт суммы
const grouped = users.reduce((acc, user) => {
const age = user.age;

if (!acc[age]) {
acc[age] = {
users: [],
totalScore: 0
};
}

acc[age].users.push(user);
acc[age].totalScore += user.score;

return acc;
}, {});

// Второй проход: вычисление среднего балла
return Object.entries(grouped).reduce((acc, [age, group]) => {
acc[age] = {
users: group.users,
averageScore: Math.round((group.totalScore / group.users.length) * 100) / 100
};
return acc;
}, {});
}

Решение 3: Один проход с reduce

function groupUsersByAge(users) {
const result = {};

users.forEach(user => {
const { age, score } = user;

if (!result[age]) {
result[age] = {
users: [],
totalScore: 0,
count: 0
};
}

result[age].users.push(user);
result[age].totalScore += score;
result[age].count += 1;
});

// Преобразуем в финальный формат
const formatted = {};
for (const age in result) {
formatted[age] = {
users: result[age].users,
averageScore: Math.round((result[age].totalScore / result[age].count) * 100) / 100
};
}

return formatted;
}

Решение 4: С использованием Map

function groupUsersByAge(users) {
const map = new Map();

users.forEach(user => {
const { age } = user;

if (!map.has(age)) {
map.set(age, { users: [], totalScore: 0 });
}

const group = map.get(age);
group.users.push(user);
group.totalScore += user.score;
});

const result = {};
map.forEach((group, age) => {
result[age] = {
users: group.users,
averageScore: Math.round((group.totalScore / group.users.length) * 100) / 100
};
});

return result;
}

Тестирование:

const users = [
{ id: 1, name: 'Alice', age: 25, score: 85 },
{ id: 2, name: 'Bob', age: 30, score: 90 },
{ id: 3, name: 'Charlie', age: 25, score: 95 },
{ id: 4, name: 'David', age: 30, score: 80 },
{ id: 5, name: 'Eve', age: 25, score: 70 },
];

const result = groupUsersByAge(users);
console.log(JSON.stringify(result, null, 2));

Аналогия в Go:

package main

import (
"fmt"
"math"
)

type User struct {
ID int
Name string
Age int
Score int
}

type AgeGroup struct {
Users []User
AverageScore float64
}

func groupUsersByAge(users []User) map[int]AgeGroup {
// Первый проход: группировка
grouped := make(map[int][]User)
scores := make(map[int]int)

for _, user := range users {
grouped[user.Age] = append(grouped[user.Age], user)
scores[user.Age] += user.Score
}

// Второй проход: вычисление среднего
result := make(map[int]AgeGroup)
for age, group := range grouped {
avg := float64(scores[age]) / float64(len(group))
result[age] = AgeGroup{
Users: group,
AverageScore: math.Round(avg*100) / 100,
}
}

return result
}

func main() {
users := []User{
{ID: 1, Name: "Alice", Age: 25, Score: 85},
{ID: 2, Name: "Bob", Age: 30, Score: 90},
{ID: 3, Name: "Charlie", Age: 25, Score: 95},
{ID: 4, Name: "David", Age: 30, Score: 80},
{ID: 5, Name: "Eve", Age: 25, Score: 70},
}

result := groupUsersByAge(users)

for age, group := range result {
fmt.Printf("Age %d: %d users, avg score: %.2f\n", age, len(group.Users), group.AverageScore)
}
}

SQL-аналог:

-- Группировка пользователей по возрасту с подсчётом среднего балла
SELECT
age,
JSON_AGG(
JSON_BUILD_OBJECT(
'id', id,
'name', name,
'score', score
)
) AS users,
ROUND(AVG(score), 2) AS average_score
FROM users
GROUP BY age
ORDER BY age;

Итог для подготовки:

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

Вопрос 24. Работал ли ты с промисами? Реализовывал ли самописный промис или есть представление, как это сделать?.

Таймкод: 00:52:53

Ответ собеседника: Неполный. Кандидат подтвердил, что работал с промисами (использовал для HTTP-запросов), но самописный промис не реализовывал и представления о том, как это сделать, не имеет.

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

Это отличный вопрос для проверки глубины понимания асинхронности. Давайте разберём тему подробно.

Что такое Promise:

Promise — это объект, представляющий результат асинхронной операции. Он может находиться в одном из трёх состояний:

  • Pending — ожидание (начальное состояние)
  • Fulfilled — выполнено успешно (с результатом)
  • Rejected — выполнено с ошибкой

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

// Создание промиса
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('Data loaded');
} else {
reject(new Error('Failed to load'));
}
}, 1000);
});

// Использование промиса
promise
.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log('Done'));

Реализация самописного промиса (MyPromise):

const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

class MyPromise {
constructor(executor) {
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];

const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};

const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};

try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}

then(onFulfilled, onRejected) {
// Значения по умолчанию для пропуска в цепочке
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

const promise2 = new MyPromise((resolve, reject) => {
if (this.state === FULFULLED) {
// Используем setTimeout для асинхронности
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}

if (this.state === REJECTED) {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}

if (this.state === PENDING) {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});

this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
});

return promise2;
}

catch(onRejected) {
return this.then(null, onRejected);
}

static resolve(value) {
return new MyPromise((resolve) => resolve(value));
}

static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}

// Вспомогательная функция для обработки возвращаемого значения then
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'));
}

let called = false;

if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then;
if (typeof then === 'function') {
then.call(
x,
y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
r => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
resolve(x);
}
}

Тестирование MyPromise:

// Тест 1: Базовый промис
const p1 = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('Success'), 100);
});

p1.then(result => {
console.log('Test 1:', result); // "Success"
});

// Тест 2: Цепочка then
const p2 = new MyPromise((resolve) => {
resolve(1);
});

p2
.then(value => {
console.log('Test 2a:', value); // 1
return value + 1;
})
.then(value => {
console.log('Test 2b:', value); // 2
return value + 1;
})
.then(value => {
console.log('Test 2c:', value); // 3
});

// Тест 3: Обработка ошибок
const p3 = new MyPromise((resolve, reject) => {
reject(new Error('Something went wrong'));
});

p3.catch(error => {
console.log('Test 3:', error.message); // "Something went wrong"
});

// Тест 4: Промис с промисом
const p4 = new MyPromise((resolve) => {
resolve(
new MyPromise((resolve) => {
setTimeout(() => resolve('Nested promise'), 50);
})
);
});

p4.then(result => {
console.log('Test 4:', result); // "Nested promise"
});

Дополнительные методы (Promise.all, Promise.race):

class MyPromise {
// ... предыдущий код ...

static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let completed = 0;

if (promises.length === 0) {
resolve(results);
return;
}

promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
results[index] = value;
completed++;
if (completed === promises.length) {
resolve(results);
}
},
reason => reject(reason)
);
});
});
}

static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
MyPromise.resolve(promise).then(resolve, reject);
});
});
}

static allSettled(promises) {
return new MyPromise((resolve) => {
const results = [];
let completed = 0;

if (promises.length === 0) {
resolve(results);
return;
}

promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
results[index] = { status: 'fulfilled', value };
completed++;
if (completed === promises.length) {
resolve(results);
}
},
reason => {
results[index] = { status: 'rejected', reason };
completed++;
if (completed === promises.length) {
resolve(results);
}
}
);
});
});
}
}

Тестирование дополнительных методов:

// Promise.all
MyPromise.all([
MyPromise.resolve(1),
MyPromise.resolve(2),
new MyPromise(resolve => setTimeout(() => resolve(3), 100))
]).then(results => {
console.log('Promise.all:', results); // [1, 2, 3]
});

// Promise.race
MyPromise.race([
new MyPromise(resolve => setTimeout(() => resolve('slow'), 200)),
new MyPromise(resolve => setTimeout(() => resolve('fast'), 50))
]).then(result => {
console.log('Promise.race:', result); // "fast"
});

// Promise.allSettled
MyPromise.allSettled([
MyPromise.resolve(1),
MyPromise.reject(new Error('Failed')),
MyPromise.resolve(3)
]).then(results => {
console.log('Promise.allSettled:', results);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: Error },
// { status: 'fulfilled', value: 3 }
// ]
});

Аналогия в Go:

В Go нет промисов, но есть каналы и горутины для асинхронности:

package main

import (
"fmt"
"time"
)

// Promise — аналог промиса на Go
type Promise struct {
result chan interface{}
err chan error
}

func NewPromise(fn func() (interface{}, error)) *Promise {
p := &Promise{
result: make(chan interface{}, 1),
err: make(chan error, 1),
}

go func() {
result, err := fn()
if err != nil {
p.err <- err
} else {
p.result <- result
}
}()

return p
}

func (p *Promise) Then(onSuccess func(interface{}), onError func(error)) {
select {
case result := <-p.result:
onSuccess(result)
case err := <-p.err:
onError(err)
}
}

func All(promises []*Promise) *Promise {
return NewPromise(func() (interface{}, error) {
results := make([]interface{}, len(promises))
for i, p := range promises {
select {
case result := <-p.result:
results[i] = result
case err := <-p.err:
return nil, err
}
}
return results, nil
})
}

func main() {
// Создаём промис
p := NewPromise(func() (interface{}, error) {
time.Sleep(100 * time.Millisecond)
return "Success", nil
})

// Используем промис
p.Then(
func(result interface{}) {
fmt.Println("Result:", result)
},
func(err error) {
fmt.Println("Error:", err)
},
)

// Ждём завершения
time.Sleep(200 * time.Millisecond)
}

Итог для подготовки:

  • Promise — это объект для работы с асинхронными операциями.
  • Состояния: pending → fulfilled/rejected (необратимо).
  • then возвращает новый промис, что позволяет строить цепочки.
  • Реализация самописного промиса — отличный способ понять внутреннюю работу.
  • Для позиции Go-разработчика понимание промисов показывает глубину знаний асинхронности, хотя в Go используются каналы и горутины.

Вопрос 25. Расскажи, как работает new Promise — что он принимает, какие состояния имеет, как используются resolve, reject, then, catch?.

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

Ответ собеседника: Правильный. Promise принимает колбэк с параметрами resolve и reject. Промис имеет три состояния: pending, resolved, rejected. После вызова resolve или reject состояние меняется и не может быть изменено повторно. Для обработки результата используются then и catch. Кандидат также упомянул async/await.

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

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

Конструктор Promise:

const promise = new Promise((resolve, reject) => {
// Асинхронная операция
// resolve(value) — при успехе
// reject(reason) — при ошибке
});

Три состояния Promise:

СостояниеОписаниеМожно изменить?
pendingНачальное состояние, ожиданиеДа → fulfilled или rejected
fulfilledУспешно завершён с результатомНет
rejectedЗавершён с ошибкойНет

Важное свойство: состояние меняется только один раз.

const promise = new Promise((resolve, reject) => {
resolve('First'); // Состояние меняется на fulfilled
resolve('Second'); // Игнорируется
reject('Error'); // Игнорируется
});

promise.then(value => {
console.log(value); // "First"
});

Методы then и catch:

const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve(`Success: ${random}`);
} else {
reject(new Error(`Failed: ${random}`));
}
}, 1000);
});

// then принимает два колбэка: onFulfilled и onRejected
promise.then(
value => console.log('Fulfilled:', value),
error => console.log('Rejected:', error.message)
);

// catch — это сахар для then(null, onRejected)
promise.catch(error => console.log('Error:', error.message));

// finally выполняется в любом случае
promise.finally(() => console.log('Done'));

Цепочка промисов:

fetch('/api/user')
.then(response => {
if (!response.ok) {
throw new Error('Network error');
}
return response.json();
})
.then(user => {
console.log('User:', user);
return fetch(`/api/posts?userId=${user.id}`);
})
.then(response => response.json())
.then(posts => {
console.log('Posts:', posts);
})
.catch(error => {
console.error('Error:', error);
})
.finally(() => {
console.log('Request completed');
});

async/await — синтаксический сахар над промисами:

// С промисами
function getUserData(userId) {
return fetch(`/api/user/${userId}`)
.then(response => response.json())
.then(user => {
return fetch(`/api/posts?userId=${user.id}`);
})
.then(response => response.json());
}

// С async/await — тот же код, но читаемее
async function getUserData(userId) {
try {
const userResponse = await fetch(`/api/user/${userId}`);
const user = await userResponse.json();

const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsResponse.json();

return posts;
} catch (error) {
console.error('Error:', error);
throw error;
} finally {
console.log('Done');
}
}

Разница между then и async/await:

// Параллельное выполнение с then
const promise1 = fetch('/api/users');
const promise2 = fetch('/api/posts');

Promise.all([promise1, promise2])
.then(([usersResponse, postsResponse]) => {
// Обработка
});

// Параллельное выполнение с async/await
async function loadData() {
const [usersResponse, postsResponse] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts')
]);
// Обработка
}

Аналогия в Go:

В Go нет промисов, но каналы работают похоже:

package main

import (
"fmt"
"time"
)

// Асинхронная функция, возвращающая канал (аналог Promise)
func asyncOperation() <-chan string {
ch := make(chan string, 1)

go func() {
time.Sleep(100 * time.Millisecond)
ch <- "Success"
}()

return ch
}

// Асинхронная функция с ошибкой (аналог Promise с reject)
func asyncOperationWithError() (<-chan string, <-chan error) {
resultCh := make(chan string, 1)
errCh := make(chan error, 1)

go func() {
time.Sleep(100 * time.Millisecond)
if time.Now().Unix()%2 == 0 {
resultCh <- "Success"
} else {
errCh <- fmt.Errorf("random error")
}
}()

return resultCh, errCh
}

func main() {
// Простой аналог then
result := <-asyncOperation()
fmt.Println("Result:", result)

// Аналог then/catch
resultCh, errCh := asyncOperationWithError()
select {
case result := <-resultCh:
fmt.Println("Success:", result)
case err := <-errCh:
fmt.Println("Error:", err)
}
}

Итог для подготовки:

  • Promise принимает функцию с параметрами resolve и reject.
  • Три состояния: pending, fulfilled, rejected — меняются только один раз.
  • then обрабатывает успех, catch — ошибку, finally — в любом случае.
  • async/await — синтаксический сахар над промисами.
  • Для позиции Go-разработчика понимание промисов показывает глубину знаний асинхронности.

Вопрос 26. Где лучше решать задачи для практики — LeetCode или Codewars? Какие задачи подходят для начинающего фронтенд-разработчика?.

Таймкод: 01:10:15

Ответ собеседника: Правильный. Кандидат спросил, где решать задачи, так как на LeetCode сталкивается с задачами, требующими знания алгоритмов. Интервьюер рекомендовал Codewars с уровнями 8-6 kyu для начинающих, а LeetCode оставить для более продвинутого уровня.

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

Это хороший вопрос для самообразования. Давайте разберём его подробно.

Сравнение платформ:

LeetCode:

  • Фокус на алгоритмах и структурах данных
  • Задачи от лёгких (Easy) до сложных (Hard)
  • Популярен для подготовки к интервью в крупные компании (FAANG)
  • Много задач на математику, графы, динамическое программирование

Codewars:

  • Фокус на практике языка и решении задач
  • Уровни от 8 kyu (самые простые) до 1 kyu (самые сложные)
  • Можно видеть решения других пользователей
  • Больше задач на манипуляции с данными, строками, массивами

Рекомендации для начинающего:

УровеньПлатформаТип задач
НачинающийCodewars 8-7 kyuБазовые операции со строками, массивами
ПродолжающийCodewars 6-5 kyuРекурсия, объекты, методы массивов
СреднийLeetCode EasyБазовые алгоритмы, хеш-таблицы
ПродвинутыйLeetCode MediumДеревья, графы, динамическое программирование

Что важно для фронтенд-разработчика:

1. Методы массивов:

// map — преобразование каждого элемента
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]

// filter — фильтрация элементов
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]

// reduce — свёртка в одно значение
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15

// find — поиск первого совпадения
const found = numbers.find(n => n > 3); // 4

// some/every — проверка условия
const hasEven = numbers.some(n => n % 2 === 0); // true
const allPositive = numbers.every(n => n > 0); // true

// sort — сортировция
const sorted = [3, 1, 4, 1, 5].sort((a, b) => a - b); // [1, 1, 3, 4, 5]

2. Работа с объектами:

// Деструктуризация
const user = { name: 'Alice', age: 30, role: 'admin' };
const { name, age } = user;

// Spread оператор
const updatedUser = { ...user, age: 31 };

// Object.keys, Object.values, Object.entries
Object.keys(user); // ['name', 'age', 'role']
Object.values(user); // ['Alice', 30, 'admin']
Object.entries(user); // [['name', 'Alice'], ['age', 30], ['role', 'admin']]

// Группировка данных
const users = [
{ name: 'Alice', role: 'admin' },
{ name: 'Bob', role: 'user' },
{ name: 'Charlie', role: 'admin' }
];

const grouped = users.reduce((acc, user) => {
if (!acc[user.role]) acc[user.role] = [];
acc[user.role].push(user.name);
return acc;
}, {});
// { admin: ['Alice', 'Charlie'], user: ['Bob'] }

3. Работа со строками:

const str = 'Hello, World!';

// Основные методы
str.length; // 13
str.includes('World'); // true
str.indexOf('World'); // 7
str.slice(0, 5); // 'Hello'
str.replace('World', 'JavaScript'); // 'Hello, JavaScript!'
str.split(', '); // ['Hello', 'World!']
str.toLowerCase(); // 'hello, world!'
str.trim(); // Убирает пробелы по краям

// Шаблонные строки
const name = 'Alice';
const greeting = `Hello, ${name}! You have ${5} new messages.`;

4. Асинхронность:

// Работа с промисами
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
}

// Параллельные запросы
async function loadDashboard() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { user, posts, comments };
}

Примеры задач для начинающего:

Codewars 8 kyu:

  • Сумма чисел в массиве
  • Переворот строки
  • Проверка на палиндром
  • Конвертация регистра

Codewars 7 kyu:

  • Удаление дубликатов из массива
  • Подсчёт гласных в строке
  • Поиск уникального элемента
  • Группировка анаграмм

Codewars 6 kyu:

  • Реализация стека/очереди
  • Парсинг и валидация данных
  • Рекурсивные алгоритмы
  • Работа с деревьями

Ресурсы для подготовки:

РесурсТипДля кого
CodewarsЗадачи на языкНачинающие
LeetCode EasyБазовые алгоритмыJunior+
LeetCode MediumПродвинутые алгоритмыMiddle+
HackerRankЗадачи на алгоритмыВсе уровни
ExercismМенторство + задачиНачинающие
Project EulerМатематические задачиПродвинутые

Итог для подготовки:

  • Начинающим лучше начать с Codewars (8-6 kyu) для отработки базовых навыков.
  • LeetCode подходит для подготовки к интервью в крупные компании.
  • Для фронтенд-разработчика важны: методы массивов, работа с объектами, асинхронность.
  • Для позиции Go-разработчика алгоритмическая подготовка на LeetCode также полезна.

Вопрос 27. Какие есть способы удалить свойство из объекта в JavaScript?.

Таймкод: 01:15:19

Ответ собеседника: Неполный. Кандидат назвал только один способ — оператор delete. Не смог назвать другие варианты, такие как деструктуризация с rest-оператором, присвоение undefined, или создание нового объекта без нужного свойства.

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

Это базовый вопрос о работе с объектами в JavaScript. Давайте разберём все способы подробно.

Способ 1: Оператор delete

const obj = { a: 1, b: 2, c: 3 };
delete obj.b;

console.log(obj); // { a: 1, c: 3 }
console.log('b' in obj); // false

Особенности:

  • Удаляет свойство полностью
  • Возвращает true при успешном удалении
  • Работает медленнее других способов в горячих циклах
  • Не может удалить свойства, унаследованные от прототипа

Способ 2: Деструктуризация с rest-оператором

const obj = { a: 1, b: 2, c: 3 };
const { b, ...rest } = obj;

console.log(rest); // { a: 1, c: 3 }
console.log(b); // 2

Особенности:

  • Создаёт новый объект без указанного свойства
  • Оригинальный объект не изменяется (иммутабельность)
  • Предпочтительный способ в React и функциональном программировании
  • Можно удалить несколько свойств одновременно
const obj = { a: 1, b: 2, c: 3, d: 4 };
const { b, d, ...rest } = obj;

console.log(rest); // { a: 1, c: 3 }

Способ 3: Присвоение undefined

const obj = { a: 1, b: 2, c: 3 };
obj.b = undefined;

console.log(obj); // { a: 1, b: undefined, c: 3 }
console.log('b' in obj); // true — свойство всё ещё существует!

Особенности:

  • Свойство остаётся в объекте
  • Ключ всё ещё присутствует в Object.keys(), for...in, JSON.stringify()
  • Не рекомендуется, если нужно именно удалить свойство
const obj = { a: 1, b: undefined, c: 3 };

console.log(Object.keys(obj)); // ['a', 'b', 'c']
console.log(JSON.stringify(obj)); // {"a":1,"c":3} — JSON игнорирует undefined

Способ 4: Создание нового объекта (Object.fromEntries + filter)

const obj = { a: 1, b: 2, c: 3 };

const rest = Object.fromEntries(
Object.entries(obj).filter(([key]) => key !== 'b')
);

console.log(rest); // { a: 1, c: 3 }

Особенности:

  • Гибкий способ для сложной фильтрации
  • Создаёт новый объект
  • Подходит для удаления по условию

Способ 5: Утилитарная функция omit

function omit(obj, keysToRemove) {
return Object.keys(obj)
.filter(key => !keysToRemove.includes(key))
.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {});
}

const obj = { a: 1, b: 2, c: 3, d: 4 };
const rest = omit(obj, ['b', 'd']);

console.log(rest); // { a: 1, c: 3 }

Способ 6: Использование библиотеки (Lodash)

import { omit } from 'lodash';

const obj = { a: 1, b: 2, c: 3 };
const rest = omit(obj, ['b']);

console.log(rest); // { a: 1, c: 3 }

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

СпособИзменяет оригиналУдаляет полностьюПроизводительность
deleteДаДаМедленная
ДеструктуризацияНетДаБыстрая
= undefinedДаНетБыстрая
Object.fromEntriesНетДаСредняя
omit (Lodash)НетДаБыстрая

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

Пример 1: Удаление свойства в React state

// Плохо — мутируем state
const handleRemove = (key) => {
delete state[key];
setState(state);
};

// Хорошо — создаём новый объект
const handleRemove = (key) => {
const { [key]: _, ...newState } = state;
setState(newState);
};

Пример 2: Очистка объекта от пустых значений

const obj = { a: 1, b: null, c: undefined, d: '', e: 'hello' };

const cleaned = Object.fromEntries(
Object.entries(obj).filter(([_, value]) => {
return value !== null && value !== undefined && value !== '';
})
);

console.log(cleaned); // { a: 1, e: 'hello' }

Пример 3: Удаление чувствительных данных перед отправкой

const userData = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
password: 'secret1289',
ssn: '123-45-6789'
};

// Удаляем чувствительные поля перед логированием
const { password, ssn, ...safeData } = userData;
console.log(safeData); // { id: 1, name: 'Alice', email: 'alice@example.com' }

Аналогия в Go:

В Go нет прямого аналога, но можно использовать мапы:

package main

import "fmt"

func main() {
// Удаление из мапы
m := map[string]int{"a": 1, "b": 2, "c": 3}
delete(m, "b")
fmt.Println(m) // map[a:1 c:3]

// Создание новой мапы без ключа (аналог деструктуризации)
original := map[string]int{"a": 1, "b": 2, "c": 3}
filtered := make(map[string]int)
for k, v := range original {
if k != "b" {
filtered[k] = v
}
}
fmt.Println(filtered) // map[a:1 c:3]
}

Итог для подготовки:

  • delete — классический способ, но медленный.
  • Деструктуризация с rest — предпочтительный способ в современном JS (иммутабельность).
  • Присвоение undefined — не удаляет свойство, а только обнуляет значение.
  • Для React и функционального программирования используйте деструктуризацию.
  • Для позиции Go-разработчика это базовые знания JavaScript.