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

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

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

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

Вопрос 1. Что представляет собой проект Sneak Max в резюме и считать ли его коммерческим опытом?

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

Ответ собеседника: Правильный. Это продуктовый лендинг с кроссовками, чисто пет-проект, выложенный на бесплатный хостинг GitHub.

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

Sneak Max — это пет-проект, представляющий собой лендинг-страницу для продажи кроссовок, размещённый на GitHub Pages. Кандидат верно идентифицировал его как не коммерческий опыт, а учебный/демонстрационный проект.

Почему это не коммерческий опыт:

  • Отсутствие реальной бизнес-логики (платежи, инвентарь, заказы)
  • Нет реальных пользователей и транзакций
  • Размещение на бесплатном хостинге указывает на демонстрационный характер
  • Основная цель — показать навыки разработки, а не заработок

Как правильно позиционировать такие проекты на интервью:

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

Что можно добавить при обсуждении пет-проекта:

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

Вопрос 2. Почему проект Sneak Max указан в разделе опыта работы, а не в отдельном разделе пет-проектов?

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

Ответ собеседника: Неполный. Не знал, где указывать пет-проекты, поэтому просто поместил в опыт работы, не разобрался в структуре резюме.

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

Кандидат честно признал недочёт в структурировании резюме, но ответ поверхностный. Вот как правильно подходить к оформлению опыта в резюме.

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

1. Коммерческий опыт работы

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

2. Пет-проекты / Проекты

Отдельный раздел для личных проектов, в котором указываются:

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

3. Open-source участие (если есть)

Контрибуции в открытые проекты выделяются отдельно и очень ценятся работодателями.

Почему важно разделять:

  • Рекрутер тратит в среднем 6-10 секунд на первичный просмотр резюме
  • Смешение коммерческого и учебного опыта создаёт ложное впечатление
  • На техническом интервью могут задать вопросы про процессы разработки, командную работу, code review — на которые для пет-проекта ответить будет нечем
  • Честность и структурированность резюме говорят о зрелости кандидата

Как правильно оформить раздел проектов:

Проекты

Sneak Max — лендинг для продажи кроссовок
Стек: Go, PostgreSQL, Docker, GitHub Actions
Реализовал: REST API для каталога товаров, авторизацию через JWT,
интеграцию с платёжной системой Stripe (тестовый режим)
Ссылка: github.com/username/sneak-max

Такой формат показывает техническую глубину и при этом не вводит в заблуждение относительно характера опыта.

Вопрос 3. Что такое Urban University и каков был опыт обучения там?

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

Ответ собеседника: Правильный. Это были курсы по разработке. Sneak Max должен был стать дипломной работой. Компания закрылась — это Нетология/Urban University, которая работала на средства инвесторов. Обучение длилось полгода, в итоге не трудоустроили, компания прекратила существование. Стоимость обучения около 120 000 рублей. В договоре была указана стажировка, но точные условия не помнит.

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

Urban University — это образовательный проект, связанный с Нетологией, который позиционировал себя как интенсивная программа подготовки разработчиков с гарантией трудоустройства или стажировки. Проект прекратил существование, что является известным случаем на российском рынке онлайн-образования.

Контекст ситуации:

Кандидат прошёл полугодовое обучение, создал дипломный проект (Sneak Max), но не получил обещанной стажировки или трудоустройства из-за закрытия проекта. Это довольно распространённая история среди выпускников подобных интенсивных программ.

Как правильно говорить об этом на интервью:

Что стоит подчеркнуть:

  • Какие конкретные навыки были получены (Go, работа с БД, Docker и т.д.)
  • Что удалось реализовать в дипломном проекте
  • Как кандидат продолжил развитие после закрытия программы

Чего лучше избегать:

  • Деталей о финансовых потерях и юридических аспектах
  • Обвинительного тона в адрес образовательной компании
  • Излишнего акцента на том, что что-то не дали или не выполнили обещаний

Пример правильного формулирования:

«Прошёл интенсивную программу по Go-разработке длительностью 6 месяцев. В рамках обучения разработал полноценный проект — REST API для интернет-магазина с авторизацией, работой с базой данных и контейнеризацией. К сожалению, программа завершилась раньше запланированного срока, но полученные знания позволили мне продолжить самостоятельное развитие и формирование портфолио».

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

Вопрос 4. Стоит ли указывать образование из онлайн-курсов в резюме и как это воспринимается работодателями?

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

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

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

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

Общий принцип:

Онлайн-курсы стоит указывать, но в правильном разделе и с правильными формулировками. Это демонстрирует стремление к обучению и проактивность.

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

1. Высшее/среднее специальное образование

Указывается в первую очередь, даже если оно не связано с IT. Наличие любого высшего образования — это плюс, так как показывает способность учиться системно и доводить дело до конца.

2. Дополнительное образование / Курсы

Сюда помещаются онлайн-курсы, интентивы, сертификации. Формат:

Дополнительное образование

Urban University (Нетология) — Go-разработчик
Интенсивная программа, 6 месяцев, 2024
Стек: Go, PostgreSQL, Docker, REST API
Дипломный проект: REST API для интернет-магазина

Как это воспринимается работодателями:

Позитивно:

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

Нейтрально:

  • Курсы не заменяют коммерческий опыт, но и не дискредитируют кандидата
  • Большинство работодателей понимают, что рынок IT-образования разнообразен

Негативно (чего избегать):

  • Указание множества незавершённых курсов создаёт впечатление поверхностности
  • Формулировки вроде «получил диплом разработчика» могут ввести в заблуждение
  • Завышенные ожидания от уровня знаний после курсов

Практические рекомендации:

  • Указывайте только те курсы, которые реально завершили
  • Перечисляйте конкретные технологии, которые изучили
  • Связывайте курсы с проектами, которые реализовали на их основе
  • Не переоценивайте значимость курсов — они дополняют, но не заменяют опыт

Важно понимать:

Для позиций junior и middle разработчика наличие структурированного обучения — это нормально и ожидаемо. Работодатели ценят честность и способность учиться больше, чем идеальное образование. Главное — уметь продемонстрировать практические навыки, полученные в процессе обучения.

Вопрос 5. Как оценить свои знания по HTML/CSS, JavaScript и React по 10-балльной шкале?

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

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

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

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

Что означают эти оценки на практике:

HTML/CSS — 6-7 из 10:

Это уровень, на котором разработчик может:

  • Создавать семантически корректную разметку
  • Верстать адаптивные макеты с помощью Flexbox и Grid
  • Использовать медиа-запросы для разных устройств
  • Применять базовые принципы доступности (accessibility)
  • Работать с CSS-препроцессорами на базовом уровне

JavaScript — 5-6 из 10:

Это уровень, на котором разработчик понимает:

  • Основы синтаксиса и типов данных
  • Работу с DOM на базовом уровне
  • Асинхронные операции (Promise, async/await)
  • Обработку событий
  • Может писать простые скрипты и модули

React — 6 из 10:

Это уровень, на котором разработчик способен:

  • Создавать функциональные компоненты
  • Использовать основные хуки (useState, useEffect, useContext)
  • Управлять состоянием на уровне компонента
  • Работать с пропсами и событиями
  • Понимать базовый жизненный цикл компонентов

Почему для Go-разработчика это нормально:

Бэкенд-разработчику не требуется экспертный уровень фронтенда. Достаточно понимать, как работает фронтенд, чтобы:

  • Эффективно взаимодействовать с фронтенд-командой
  • Понимать требования к API со стороны клиента
  • При необходимости дебажить проблемы на стыке фронтенда и бэкенда
  • Прототипировать простые интерфейсы для внутренних инструментов

Как правильно говорить об этом на интервью:

«Основной фокус моей экспертизы — бэкенд на Go. Фронтенд-технологии знаю на уровне, достаточном для понимания работы полного стека и эффективного взаимодействия с фронтенд-разработчиками. Могу при необходимости разобраться в клиентском коде и отладить интеграционные проблемы».

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

Вопрос 6. Что такое замыкание (closure) в JavaScript?

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

Ответ собеседника: Правильный. Замыкание — это способность функции запоминать своё лексическое окружение или контекст.

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

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

Формальное определение:

Замыкание — это комбинация функции и лексического окружения, в котором эта функция была определённо. Другими словами, замыкание даёт функции доступ к внешней области видимости, даже после того как внешняя функция завершила выполнение.

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

Когда функция создаётся внутри другой функции, внутренняя функция получает доступ к переменным внешней функции. Даже после того как внешняя функция вернула результат и её контекст выполнения удалён из стека, переменные остаются доступны через замыкание, потому что они хранятся в специальном внутреннем свойстве [[Environment]].

Пример замыкания:

function createCounter() {
let count = 0;

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

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

В этом примере функция increment запоминает переменную count из внешней области видимости. Каждый вызов counter() увеличивает и возвращает значение count.

Практические применения замыканий:

1. Создание приватных переменных:

function createUser(name) {
let _name = name; // приватная переменная

return {
getName: function() {
return _name;
},
setName: function(newName) {
_name = newName;
}
};
}

const user = createUser("Alice");
console.log(user.getName()); // "Alice"
console.log(user._name); // undefined — доступа нет

2. Фабричные функции:

function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

3. Обработчики событий и колбэки:

function setupButton(buttonId, message) {
document.getElementById(buttonId).addEventListener('click', function() {
alert(message); // message доступен через замыкание
});
}

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

// Неправильно — все функции выводят одно и то же значение
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Выведет: 3, 3, 3

// Правильно — используем let или IIFE
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Выведет: 0, 1, 2

Важные моменты:

  • Замыкания хранят ссылку на переменные, а не их копии
  • Каждое замыкание имеет доступ к своему собственному лексическому окружению
  • Замыкания могут приводить к утечкам памяти, если не освобождать ссылки правильно
  • Механизм замыканий основан на лексическом связывании (lexical scoping)

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

Вопрос 7. Привести пример замыкания без использования вложенных функций.

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

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

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

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

Замыкание через возврат функции из объекта:

function createConfig() {
const apiKey = "secret-key-123";
const baseUrl = "https://api.example.com";

return {
makeRequest: function(endpoint) {
// Эта функция имеет замыкание на apiKey и baseUrl
console.log(`Request to ${baseUrl}/${endpoint} with key ${apiKey}`);
}
};
}

const config = createConfig();
config.makeRequest("users"); // Замыкание сохраняет доступ к apiKey и baseUrl

Замыкание через передачу функции как колбэка:

function fetchData(url, callback) {
// Имитация запроса
const data = { users: ["Alice", "Bob"] };

// Колбэк вызывается позже, но сохраняет доступ к своему лексическому окружению
setTimeout(function() {
callback(data);
}, 1000);
}

function processUsers() {
const processingMessage = "Processing...";

fetchData("/api/users", function(data) {
// Этот колбэк имеет замыкание на processingMessage
console.log(processingMessage);
console.log(data.users);
});
}

processUsers();

Замыкание через обработчик событий:

function setupForm(formId) {
const formData = {
attempts: 0,
maxAttempts: 3
};

const form = document.getElementById(formId);

// Обработчик не является вложенной функцией в классическом смысле,
// но имеет замыкание на formData
form.addEventListener("submit", function(event) {
event.preventDefault();
formData.attempts++;

if (formData.attempts >= formData.maxAttempts) {
console.log("Max attempts reached");
this.disabled = true;
}
});
}

setupForm("login-form");

Замыкание через метод объекта:

const userService = (function() {
let userCount = 0; // приватная переменная

return {
createUser: function(name) {
userCount++;
console.log(`User ${name} created. Total: ${userCount}`);
},
getUserCount: function() {
return userCount;
}
};
})();

userService.createUser("Alice"); // "User Alice created. Total: 1"
userService.createUser("Bob"); // "User Bob created. Total: 2"
console.log(userService.getUserCount()); // 2

Ключевой момент:

Замыкание — это не синтаксис вложенных функций, а механизм сохранения ссылки на лексическое окружение. Функция «запоминает» переменные из того контекста, где она была создана, независимо от того, где она будет вызвана. Это происходит благодаря внутреннему свойству [[Environment]], которое устанавливается при создании функции.

Вопрос 8. Что такое ключевое слово this в JavaScript?

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

Ответ собеседника: Правильный. Это ключевое слово для обращения к контексту объекта или функции.

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

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

Что такое this:

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

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

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

console.log(this); // В браузере: window, в Node.js: global/undefined (strict mode)

2. Метод объекта:

const user = {
name: "Alice",
greet: function() {
console.log(this.name); // this ссылается на user
}
};

user.greet(); // "Alice"

3. Вызов функции как конструктора (с new):

function Person(name) {
this.name = name; // this ссылается на новый объект
}

const person = new Person("Bob");
console.log(person.name); // "Bob"

4. Явное связывание (call, apply, bind):

function greet() {
console.log(this.name);
}

const context = { name: "Charlie" };

greet.call(context); // "Charlie"
greet.apply(context); // "Charlie"

const boundGreet = greet.bind(context);
boundGreet(); // "Charlie"

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

const obj = {
name: "David",
regularFunc: function() {
console.log(this.name); // "David"
},
arrowFunc: () => {
console.log(this.name); // undefined — стрелочная функция не имеет своего this
}
};

obj.regularFunc();
obj.arrowFunc();

Частые проблемы с this:

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

const user = {
name: "Alice",
greet: function() {
console.log(this.name);
}
};

const greetFunc = user.greet;
greetFunc(); // undefined — контекст потерян

// Решение: привязать контекст
const boundGreet = user.greet.bind(user);
boundGreet(); // "Alice"

Потеря контекста в колбэках:

const button = {
text: "Click me",
handleClick: function() {
console.log(this.text);
}
};

// Неправильно — контекст потерян
document.getElementById("btn").addEventListener("click", button.handleClick);

// Правильно — используем bind или стрелочную функцию
document.getElementById("btn").addEventListener("click", button.handleClick.bind(button));
// или
document.getElementById("btn").addEventListener("click", () => button.handleClick());

Стрелочные функции и замыкание this:

const team = {
name: "Backend",
members: ["Alice", "Bob"],

// Неправильно — стрелочная функция не имеет своего this
showTeamBroken: () => {
console.log(this.name); // undefined
},

// Правильно — обычная функция
showTeam: function() {
this.members.forEach(member => {
// Стрелочная функция наследует this от showTeam
console.log(`${member} in ${this.name}`);
});
}
};

team.showTeam();
// "Alice in Backend"
// "Bob in Backend"

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

  1. new — создание нового объекта
  2. call/apply/bind — явное связывание
  3. Вызов как метода объекта — неявное связывание
  4. Глобальный контекст (или undefined в strict mode)

Важно помнить:

  • this не является фиксированным — он определяется при вызове функции
  • Стрелочные функции не имеют своего this, они наследуют его из внешнего лексического окружения
  • В strict mode глобальный this равен undefined вместо window/global
  • Методы call, apply, bind позволяют явно задать контекст выполнения

Понимание this критически важно для работы с объектами, классами, обработчиками событий и асинхронным кодом в JavaScript.

Вопрос 9. Что такое CORS (Cross-Origin Resource Sharing)?

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

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

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

Кандидат дал точное и понятное объяснение. CORS — это важный механизм безопасности, с которым регулярно сталкиваются веб-разработчики.

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

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

Что такое «origin» (источник):

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

https://example.com:443/page1
https://example.com:443/page2
→ Одинаковый origin

https://example.com:443
http://example.com:443
→ Разный origin (разный протокол)

https://example.com:443
https://api.example.com:443
→ Разный origin (разный домен)

https://example.com:443
https://example.com:8080
→ Разный origin (разный порт)

Same-Origin Policy (SOP):

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

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

Простые запросы (Simple Requests):

Для запросов с методами GET, POST, HEAD и определёнными заголовками браузер сразу отправляет запрос с заголовком Origin:

GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com

Сервер должен ответить с заголовком Access-Control-Allow-Origin:

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

Предварительные запросы (Preflight Requests):

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

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

Сервер отвечает с разрешением:

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

Только после этого браузер отправляет основной запрос.

Основные CORS-заголовки:

Заголовки запроса (отправляются браузером):

  • Origin — источник запроса
  • Access-Control-Request-Method — запрашиваемый метод (preflight)
  • Access-Control-Request-Headers — запрашиваемые заголовки (preflight)

Заголовки ответа (отправляются сервером):

  • Access-Control-Allow-Origin — разрешённые источники
  • Access-Control-Allow-Methods — разрешённые HTTP-методы
  • Access-Control-Allow-Headers — разрешённые заголовки
  • Access-Control-Allow-Credentials — разрешены ли credentials (cookies, auth)
  • Access-Control-Max-Age — время кэширования preflight-ответа

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

package main

import (
"net/http"
"github.com/rs/cors"
)

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

// Настройка CORS
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://frontend.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 86400,
})

handler := c.Handler(mux)
http.ListenAndServe(":8080", handler)
}

Или вручную через middleware:

func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://frontend.example.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
w.Header().Set("Access-Control-Allow-Credentials", "true")

// Обработка preflight-запроса
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}

next.ServeHTTP(w, r)
})
}

Частые ошибки и решения:

Ошибка: No 'Access-Control-Allow-Origin' header

Access to fetch at 'https://api.example.com/data' from origin 'https://frontend.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Решение: Настроить сервер для отправки правильных CORS-заголовков.

Ошибка: Credentials не поддерживаются с wildcard

The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*'
when the request's credentials mode is 'include'.

Решение: Указать конкретный origin вместо * при использовании credentials.

Важные моменты:

  • CORS — это браузерная защита, серверные запросы (curl, Postman, сервер-сервер) не подвержены CORS
  • Заголовок Access-Control-Allow-Origin: * не работает с credentials
  • Preflight-запросы могут снижать производительность — используйте Access-Control-Max-Age для кэширования
  • CORS не защищает от CSRF-атак — для этого нужны дополнительные механизмы (CSRF-токены)

Вопрос 10. Как обойти ограничения CORS?

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

Ответ собеседника: Неполный. Можно обратиться к бэкенду или настроить прокси. На бэкенде нужно указать конкретные домены в заголовках или использовать звёздочку для разрешения всем доменам, но кандидат не помнит точное название заголовка (Access-Control-Allow-Origin).

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

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

Способы решения CORS-проблем:

1. Настройка сервера (правильный способ)

Основной заголовок — Access-Control-Allow-Origin:

// Go пример с правильными CORS-заголовками
func enableCORS(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://frontend.example.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")

if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
}

Варианты значений Access-Control-Allow-Origin:

  • Конкретный домен: https://frontend.example.com
  • Звёздочка (без credentials): *
  • Динамическое определение на основе заголовка Origin в запросе

2. Проксирование через собственный сервер

Когда нет доступа к настройкам целевого сервера:

// Go-прокси для обхода CORS
func proxyHandler(w http.ResponseWriter, r *http.Request) {
targetURL := "https://external-api.com" + r.URL.Path

proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// Копируем заголовки
for key, values := range r.Header {
for _, value := range values {
proxyReq.Header.Add(key, value)
}
}

client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()

// Копируем ответ
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}

3. Nginx reverse proxy

server {
listen 80;
server_name frontend.example.com;

location /api/ {
proxy_pass https://api.external.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

4. CORS-прокси для разработки

Для локальной разработки можно использовать:

  • Webpack Dev Server proxy
  • Vite proxy
  • Пакет cors-anywhere (не для продакшена)
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}

5. JSONP (устаревший метод)

Работает только для GET-запросов:

function handleResponse(data) {
console.log(data);
}

const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);

Чего нельзя делать:

  • Отключать CORS в браузере для продакшена (--disable-web-security)
  • Использовать * с credentials
  • Открывать доступ для всех доменов без необходимости

Рекомендации:

  • Всегда настраивайте CORS на сервере — это правильный подход
  • Используйте прокси только когда нет доступа к целевому серверу
  • Указывайте конкретные домены вместо * для безопасности
  • Кэшируйте preflight-запросы через Access-Control-Max-Age

Неспособность назвать Access-Control-Allow-Origin — это красный флаг, так как это базовый заголовок, с которым разработчик сталкивается практически ежедневно при работе с API.

Вопрос 11. Реализовать функцию sleep, которая принимает количество миллисекунд и заставляет код ждать указанное время.

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

Ответ собеседника: Неправильный. Кандидат начал писать код, использовал setTimeout, но не смог завершить реализацию. После подсказки о необходимости использовать Promise, не смог написать корректный код с конструктором Promise.

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

Это классическое задание на понимание асинхронности в JavaScript. Кандидат должен был продемонстрировать знание Promise и уметь обернуть колбэк-функцию в промис.

Реализация sleep на JavaScript:

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

Использование с async/await:

async function example() {
console.log('Start');
await sleep(2000);
console.log('After 2 seconds');
await sleep(1000);
console.log('After 1 more second');
}

example();

Разбор реализации:

Функция sleep возвращает Promise, который резолвится через указанное количество миллисекунд. Ключевые моменты:

  • new Promise(resolve => ...) — создаём новый промис
  • setTimeout(resolve, ms) — через ms миллисекунд вызываем resolve, промис завершается
  • await sleep(2000) — приостанавливает выполнение async-функции до завершения промиса

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

// НЕПРАВИЛЬНО — код продолжит выполняться сразу
function sleepBroken(ms) {
setTimeout(() => {}, ms);
}

console.log('Before');
sleepBroken(2000);
console.log('After'); // Выполнится сразу, не ждёт 2 секунды

Аналог на Go:

Для сравнения, как выглядит sleep в Go:

package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("Start")
time.Sleep(2 * time.Second)
fmt.Println("After 2 seconds")
}

В Go sleep блокирует горутину напрямую, без необходимости в промисах, потому что Go имеет встроенную поддержку конкурентности через горутины и каналы.

Продвинутая версия с возможностью отмены:

function sleep(ms, signal) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(resolve, ms);

if (signal) {
signal.addEventListener('abort', () => {
clearTimeout(timeout);
reject(new Error('Sleep aborted'));
});
}
});
}

// Использование с AbortController
const controller = new AbortController();

async function example() {
try {
await sleep(5000, controller.signal);
} catch (e) {
console.log('Sleep was cancelled');
}
}

// Отмена через 1 секунду
setTimeout(() => controller.abort(), 1000);

Что проверяет это задание:

  • Понимание асинхронного выполнения в JavaScript
  • Знание Promise API
  • Умение обернуть колбэк-стиль в промис
  • Понимание async/await синтаксиса

Неспособность реализовать эту функцию указывает на слабое понимание асинхронного программирования в JavaScript, что критично для любого разработчика, работающего с веб-технологиями.

Вопрос 12. Что такое промисы (Promises) в JavaScript?

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

Ответ собеседника: Неполный. Промисы — это некое обещание, которое выполняется через какой-то промежуток времени. Знает, что у промисов есть статусы (pending, fulfilled), но не может полноценно объяснить концепцию.

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

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

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

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

Три состояния промиса:

  • Pending — начальное состояние, операция ещё не завершена
  • Fulfilled (Resolved) — операция успешно завершена, есть результат
  • Rejected — операция завершилась с ошибкой

Состояние промиса может измениться только один раз: из pending в fulfilled или rejected. После этого изменение невозможно.

Создание промиса:

const promise = new Promise((resolve, reject) => {
// Асинхронная операция
setTimeout(() => {
const success = true;

if (success) {
resolve('Данные получены'); // Успешное завершение
} else {
reject(new Error('Ошибка сети')); // Завершение с ошибкой
}
}, 1000);
});

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

promise
.then(result => {
console.log(result); // "Данные получены"
})
.catch(error => {
console.error(error.message); // "Ошибка сети"
})
.finally(() => {
console.log('Операция завершена'); // Выполняется в любом случае
});

Цепочки промисов (chaining):

fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(response => response.json())
.then(posts => console.log(posts))
.catch(error => console.error(error));

Статические методы Promise:

Promise.all — ждёт выполнения всех промисов:

const usersPromise = fetch('/api/users').then(r => r.json());
const postsPromise = fetch('/api/posts').then(r => r.json());

Promise.all([usersPromise, postsPromise])
.then(([users, posts]) => {
console.log('Все данные загружены:', users, posts);
})
.catch(error => {
console.error('Ошибка при загрузке:', error);
});

Promise.race — возвращает результат первого завершённого промиса:

const fast = new Promise(resolve => setTimeout(() => resolve('Fast'), 100));
const slow = new Promise(resolve => setTimeout(() => resolve('Slow'), 500));

Promise.race([fast, slow]).then(result => {
console.log(result); // "Fast"
});

Promise.allSettled — ждёт завершения всех промисов, независимо от результата:

const promises = [
Promise.resolve('Success'),
Promise.reject('Error'),
Promise.resolve('Another success')
];

Promise.allSettled(promises).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 'Success' },
// { status: 'rejected', reason: 'Error' },
// { status: 'fulfilled', value: 'Another success' }
// ]
});

Async/await — синтаксический сахар над промисами:

async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
throw error;
}
}

Аналогия на Go:

В Go нет промисов в классическом понимании, но аналогом являются каналы:

package main

import (
"fmt"
"time"
)

func fetchData() <-chan string {
ch := make(chan string)

go func() {
time.Sleep(1 * time.Second)
ch <- "Данные получены"
}()

return ch
}

func main() {
result := <-fetchData() // Ожидаем данные из канала
println(result)
}

Важные моменты:

  • Промис нельзя «отменить» — после создания он будет выполнен
  • Ошибка без catch приведёт к unhandled promise rejection
  • Цепочка then возвращает новый промис, что позволяет строить цепочки
  • async/await делает асинхронный код похожим на синхронный, но под капотом всё ещё работают промисы

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

Вопрос 13. Что такое каррирование (currying) и как реализовать функцию curry?

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

Ответ собеседника: Неправильный. Кандидат впервые услышал термин каррирование и не смог реализовать функцию. Признал полное отсутствие понимания концепции. В процессе совместного разбора не знал о существовании rest-оператора (...args) для работы с переменным числом аргументов.

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

Кандидат продемонстрировал серьёзные пробелы в знании JavaScript. Каррирование — это продвинутая, но хорошо документированная концепция, а незнание rest-оператора указывает на слабое владение базовым синтаксисом ES6.

Определение каррирования:

Каррирование — это техника преобразования функции с несколькими аргументами в последовательность функций, каждая из которых принимает один аргумент. То есть функция f(a, b, c) преобразуется в f(a)(b)(c).

Пример без каррирования:

function add(a, b, c) {
return a + b + c;
}

add(1, 2, 3); // 6

Тот же пример с каррированием:

function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}

curriedAdd(1)(2)(3); // 6

Зачем нужно каррирование:

1. Создание специализированных функций:

function multiply(a) {
return function(b) {
return a * b;
};
}

const double = multiply(2);
const triple = multiply(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

2. Переиспользование функций:

function greet(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}

const sayHello = greet('Hello');
const sayHi = greet('Hi');

console.log(sayHello('Alice')); // "Hello, Alice!"
console.log(sayHi('Bob')); // "Hi, Bob!"

3. Композиция функций:

function filter(predicate) {
return function(array) {
return array.filter(predicate);
};
}

function map(transform) {
return function(array) {
return array.map(transform);
};
}

const isEven = n => n % 2 === 0;
const double = n => n * 2;

const filterEven = filter(isEven);
const mapDouble = map(double);

const numbers = [1, 2, 3, 4, 5, 6];
const result = mapDouble(filterEven(numbers));
console.log(result); // [4, 8, 12]

Универсальная функция curry:

function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}

Разбор реализации:

  • fn.length — количество параметров исходной функции
  • ...args — rest-оператор собирает все переданные аргументы в массив
  • Если аргументов достаточно — вызываем исходную функцию
  • Если недостаточно — возвращаем новую функцию, ожидающую оставшиеся аргументы
  • args.concat(args2) — объединяем аргументы из разных вызовов

Использование универсальной curry:

function add(a, b, c) {
return a + b + c;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6

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

function log(level, message, timestamp) {
console.log(`[${level}] ${timestamp}: ${message}`);
}

const curriedLog = curry(log);

const errorLog = curriedLog('ERROR');
const errorLogNow = errorLog('Something went wrong');
errorLogNow(new Date().toISOString());
// [ERROR] 2024-01-15T10:30:00.000Z: Something went wrong

Отличие от частичного применения (partial application):

Каррирование и частичное применение — разные концепции:

// Каррирование: каждый вызов принимает ровно один аргумент
curriedAdd(1)(2)(3)

// Частичное применение: можно зафиксировать несколько аргументов
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}

const addOne = partial(add, 1);
addOne(2, 3); // 6

Важные моменты:

  • Каррирование работает только с функциями, у которых известно количество аргументов
  • Стрелочные функции с несколькими параметрами нельзя каррировать автоматически
  • Каррирование широко используется в функциональном программировании
  • Библиотеки типа Ramda, Lodash предоставляют готовые реализации curry

Незнание каррирования допустимо для начинающего разработчика, но незнание rest-оператора — это серьёзный пробел в базовых знаниях JavaScript ES6.

Вопрос 14. Как добавить свой метод в прототип массива (например, myFilter)?

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

Ответ собеседника: Неправильный. Кандидат никогда не работал с прототипами в JavaScript и не знал, как добавить кастомный метод в прототип Array.

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

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

Прототипы в JavaScript:

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

Добавление метода в прототип Array:

Array.prototype.myFilter = function(callback, thisArg) {
const result = [];

for (let i = 0; i < this.length; i++) {
if (callback.call(thisArg, this[i], i, this)) {
result.push(this[i]);
}
}

return result;
};

// Использование
const numbers = [1, 2, 3, 4, 5, 6];
const evens = numbers.myFilter(num => num % 2 === 0);
console.log(evens); // [2, 4, 6]

Разбор реализации:

  • Array.prototype — прототип всех массивов
  • this внутри метода ссылается на массив, для которого вызван метод
  • callback(element, index, array) — стандартная сигнатура колбэка для методов массива
  • thisArg — необязательный контекст для вызова колбэка

Реализация myMap:

Array.prototype.myMap = function(callback, thisArg) {
const result = [];

for (let i = 0; i < this.length; i++) {
result.push(callback.call(thisArg, this[i], i, this));
}

return result;
};

// Использование
const numbers = [1, 2, 3];
const doubled = numbers.myMap(num => num * 2);
console.log(doubled); // [2, 4, 6]

Реализация myReduce:

Array.prototype.myReduce = function(callback, initialValue) {
let accumulator = initialValue !== undefined ? initialValue : this[0];
let startIndex = initialValue !== undefined ? 0 : 1;

for (let i = startIndex; i < this.length; i++) {
accumulator = callback(accumulator, this[i], i, this);
}

return accumulator;
};

// Использование
const numbers = [1, 2, 3, 4];
const sum = numbers.myReduce((acc, num) => acc + num, 0);
console.log(sum); // 10

Важные предостережения:

1. Не изменяйте нативные прототипы в продакшине:

// ПЛОХО — может конфликтовать с другими библиотеками
Array.prototype.myMethod = function() { ... };

// ХОРОШО — используйте утилитарные функции
function myFilter(array, callback) {
const result = [];
for (let i = 0; i < array.length; i++) {
if (callback(array[i], i, array)) {
result.push(array[i]);
}
}
return result;
}

2. Проверяйте существование метода перед добавлением:

if (!Array.prototype.myFilter) {
Array.prototype.myFilter = function(callback) {
// реализация
};
}

3. Используйте Object.defineProperty для правильных атрибутов:

Object.defineProperty(Array.prototype, 'myFilter', {
value: function(callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (callback(this[i], i, this)) {
result.push(this[i]);
}
}
return result;
},
enumerable: false, // не будет виден в for...in
writable: true,
configurable: true
});

Альтернатива: создание кастомного класса:

class MyArray extends Array {
myFilter(callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (callback(this[i], i, this)) {
result.push(this[i]);
}
}
return result;
}

myMap(callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
result.push(callback(this[i], i, this));
}
return result;
}
}

const arr = new MyArray(1, 2, 3, 4, 5);
console.log(arr.myFilter(x => x > 2)); // MyArray [3, 4, 5]

Цепочка прототипов:

myArray → Array.prototype → Object.prototype → null

Когда вы вызываете myArray.push(), JavaScript:

  1. Ищет push в самом объекте myArray
  2. Не находит — ищет в Array.prototype
  3. Находит и вызывает

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

  • Понимание прототипов объясняет, как работают встроенные методы
  • Помогает отлаживать проблемы с this
  • Необходимо для понимания классов (которые являются синтаксическим сахаром над прототипами)
  • Используется при создании полифиллов для старых браузеров

Незнание прототипов — серьёзный пробел, так как это одна из ключевых особенностей JavaScript, отличающая её от классических ООП-языков.

Вопрос 15. Что возвращает хук useState при деструктуризации?

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

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

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

Кандидат использовал useState на практике, но не смог объяснить базовую механику работы хука. Это указывает на поверхностное понимание React.

Что возвращает useState:

useState возвращает массив ровно из двух элементов:

const [state, setState] = useState(initialValue);
  • Первый элемент (state) — текущее значение состояния
  • Второй элемент (setState) — функция для обновления этого состояния

Почему массив, а не объект:

React возвращает массив, а не объект, чтобы позволить разработчику самому выбирать имена переменных при деструктуризации:

// Можно назвать как угодно
const [name, setName] = useState('');
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);

// Если бы возвращался объект, пришлось бы использовать фиксированные имена
// const { state: name, setState: setName } = useState('');

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

import { useState } from 'react';

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

const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}

Функциональное обновление состояния:

Когда новое состояние зависит от предыдущего, лучше передавать функцию:

// Потенциально проблемно — может использовать устаревшее значение
const increment = () => setCount(count + 1);

// Правильно — всегда использует актуальное значение
const increment = () => setCount(prevCount => prevCount + 1);

Ленивая инициализация:

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

// Вычисляется при каждом рендере (даже если значение не используется)
const [state, setState] = useState(expensiveComputation());

// Вычисляется только при первом рендере
const [state, setState] = useState(() => expensiveComputation());

Несколько состояний в компоненте:

function UserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);

// ...
}

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

  • Хуки можно вызывать только на верхнем уровне компонента (не в условиях, циклах, вложенных функциях)
  • Хуки должны вызываться в одном и том же порядке при каждом рендере
  • Каждый вызов useState создаёт независимую ячейку состояния

Аналогия на Go:

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

type Counter struct {
count int
}

func (c *Counter) Increment() {
c.count++
}

func (c *Counter) GetCount() int {
return c.count
}

Важные моменты:

  • useState вызывает ре-рендер компонента при изменении состояния
  • Обновление состояния асинхронно — новое значение будет доступно на следующем рендере
  • Для сложного состояния лучше использовать useReducer
  • Для глобального состояния — Context API или сторонние библиотеки (Redux, Zustand)

Незнание того, что возвращает useState, при заявленном опыте работы с React — это существенный пробел, указывающий на то, что кандидат копировал код, не понимая его механики.

Вопрос 16. Реализовать кастомный хук useCustomArray с методами push и removeByIndex.

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

Ответ собеседника: Неполный. Кандидат никогда не писал кастомные хуки. С помощью подсказок смог создать структуру хука: импортировал useState, создал состояние с начальным массивом, начал возвращать объект с value. Не смог самостоятельно реализовать функции push и removeByIndex, допустил синтаксические ошибки при создании объекта. В итоге с помощью интервьюера реализовал push с использованием спред-оператора для иммутабельного обновления массива.

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

Кандидат продемонстрировал слабое понимание React и неспособность самостоятельно написать базовый кастомный хук. Это серьёзный пробел для позиции, требующей знания React.

Полная реализация useCustomArray:

import { useState } from 'react';

function useCustomArray(initialArray = []) {
const [array, setArray] = useState(initialArray);

const push = (element) => {
setArray(prev => [...prev, element]);
};

const removeByIndex = (index) => {
setArray(prev => prev.filter((_, i) => i !== index));
};

return {
value: array,
push,
removeByIndex
};
}

export default useCustomArray;

Использование хука:

import useCustomArray from './useCustomArray';

function TodoList() {
const { value: todos, push, removeByIndex } = useCustomArray([
'Learn React',
'Build a project'
]);

const addTodo = () => {
push('New todo item');
};

const removeTodo = (index) => {
removeByIndex(index);
};

return (
<div>
<button onClick={addTodo}>Add Todo</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>
{todo}
<button onClick={() => removeTodo(index)}>
Remove
</button>
</li>
))}
</ul>
</div>
);
}

Разбор реализации:

Иммутабельность — ключевой принцип:

// ПРАВИЛЬНО — создаём новый массив
setArray(prev => [...prev, element]);

// НЕПРАВИЛЬНО — мутируем существующий массив
array.push(element);
setArray(array); // React не увидит изменение

React сравнивает ссылки на объекты. Если ссылка не изменилась, ре-рендер не произойдёт.

Функциональное обновление:

// Используем prev для гарантии актуального состояния
setArray(prev => [...prev, element]);

// Вместо замыкания на array, которое может быть устаревшим
setArray([...array, element]); // Может привести к багам

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

import { useState } from 'react';

function useCustomArray(initialArray = []) {
const [array, setArray] = useState(initialArray);

const push = (element) => {
setArray(prev => [...prev, element]);
};

const pop = () => {
setArray(prev => prev.slice(0, -1));
};

const removeByIndex = (index) => {
setArray(prev => prev.filter((_, i) => i !== index));
};

const removeByValue = (value) => {
setArray(prev => prev.filter(item => item !== value));
};

const updateByIndex = (index, newValue) => {
setArray(prev => prev.map((item, i) =>
i === index ? newValue : item
));
};

const clear = () => {
setArray([]);
};

const reset = () => {
setArray(initialArray);
};

return {
value: array,
length: array.length,
isEmpty: array.length === 0,
push,
pop,
removeByIndex,
removeByValue,
updateByIndex,
clear,
reset
};
}

Кастомные хуки — основные принципы:

1. Название должно начинаться с use:

// Правильно
function useCustomArray() { ... }

// Неправильно — React не распознает как хук
function customArray() { ... }

2. Хук может использовать другие хуки:

function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});

useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);

return [value, setValue];
}

3. Логика переиспользуется, состояние — нет:

// Каждый компонент получает своё независимое состояние
function ComponentA() {
const { value, push } = useCustomArray([1, 2, 3]);
// value независим от ComponentB
}

function ComponentB() {
const { value, push } = useCustomArray(['a', 'b']);
// value независим от ComponentA
}

Аналогия на Go:

В Go нет хуков, но аналогом может быть структура с методами:

package main

import "fmt"

type CustomArray[T any] struct {
items []T
}

func NewCustomArray[T any](initial []T) *CustomArray[T] {
return &CustomArray[T]{items: initial}
}

func (ca *CustomArray[T]) Push(item T) {
ca.items = append(ca.items, item)
}

func (ca *CustomArray[T]) RemoveByIndex(index int) {
if index >= 0 && index < len(ca.items) {
ca.items = append(ca.items[:index], ca.items[index+1:]...)
}
}

func (ca *CustomArray[T]) Value() []T {
return ca.items
}

func main() {
arr := NewCustomArray([]int{1, 2, 3})
arr.Push(4)
arr.RemoveByIndex(1)
fmt.Println(arr.Value()) // [1, 3, 4]
}

Важные моменты:

  • Кастомные хуки — это способ извлечь и переиспользовать логику с состоянием
  • Каждый вызова хука создаёт независимое состояние
  • Хуки могут вызывать другие хуки (useState, useEffect и т.д.)
  • Именование с префиксом use обязательно для корректной работы линтеров

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