Фронтенд собеседование 2025 | Junior Frontend | Реальные вопросы и задачи
Сегодня мы разберём реальное собеседование с кандидатом Иваном, который проходил отбор на позицию фронтенд-разработчика. В ходе интервью подробно обсуждались его резюме, технические знания и практические навыки, а также были даны ценные рекомендации по улучшению портфолио и подготовке к будущим собеседованиям. Мы увидим, как опытные разработчики оценивают уровень джуниора и какие советы они дают для профессионального роста.
Вопрос 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:
new— создание нового объектаcall/apply/bind— явное связывание- Вызов как метода объекта — неявное связывание
- Глобальный контекст (или
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:
- Ищет
pushв самом объектеmyArray - Не находит — ищет в
Array.prototype - Находит и вызывает
Почему это важно знать:
- Понимание прототипов объясняет, как работают встроенные методы
- Помогает отлаживать проблемы с
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.
