Задачи с кодом
Задачи на собеседовании frontend: 7 классических с решениями
Содержание
Задачи на собеседовании frontend почти всегда из одного набора: debounce, throttle, Promise.all, deepEqual, EventEmitter, flatten и простой хук на React. Их дают не ради самого кода, а чтобы увидеть замыкания, асинхронность и рекурсию в деле. Ниже у каждой задачи рабочее решение, что в нём проверяют и какой вопрос прозвучит следом.
Как дают задачи и что в них оценивают
На frontend-собеседовании задачу обычно дают в общем редакторе или в песочнице и просят решать вслух. Время на одну задачу от 15 до 40 минут. Интервьюеру важен не только итоговый код. Он смотрит, как вы уточняете условие, с чего начинаете и как проверяете себя.
| Задача | Что проверяют | Типичное продолжение |
|---|---|---|
| debounce | Замыкание, таймеры, this и аргументы | Добавить cancel и вызов в начале паузы |
| throttle | Работу со временем, крайние вызовы | Не потерять последний вызов |
| Promise.all | Промисы, порядок результатов, ошибки | Написать allSettled или ограничить параллельность |
| deepEqual | Рекурсию, типы, краевые случаи | Циклические ссылки, Date, Map |
| EventEmitter | Классы, хранение подписок, отписку | Метод once, отписка во время emit |
| flatten | Рекурсию и глубину | Решить без рекурсии, через стек |
| useDebounce | Эффекты React и их очистку | Отменить запрос, который уже ушёл |
Общий ход решения для любой задачи описан в разборе лайвкодинга: перескажите условие, разберите пример руками, назовите идею, напишите код и сами прогоните краевые случаи.
1. debounce
Условие. Напишите функцию debounce(fn, ms). Она возвращает обёртку, которая вызывает fn только после того, как вызовы прекратились на ms миллисекунд. Пример из жизни: поиск, который не шлёт запрос на каждую букву.
function debounce(fn, ms) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), ms);
};
}
const search = debounce((q) => console.log('поиск:', q), 100);
search('р');
search('ре');
search('реа');
setTimeout(() => search('react'), 200);поиск: реа
поиск: reactЧто проверяют. Таймер хранится в замыкании, а не в глобальной переменной. Стрелочная функция внутри setTimeout сохраняет this обёртки, поэтому метод объекта не потеряет контекст. Аргументы берутся из последнего вызова.
Уточнения, которые стоит задать. Вызывать в конце паузы или в начале? Нужен ли метод cancel? Что вернуть из обёртки? Полная версия с cancel и flush есть в задаче debounce с отменой.
2. throttle
Условие. throttle(fn, ms) вызывает fn не чаще раза в ms миллисекунд. Первый вызов проходит сразу, а последний вызов в окне не теряется: он выполнится в конце окна. Так обрабатывают прокрутку и изменение размера окна.
function throttle(fn, ms) {
let last = 0;
let timer = null;
let lastArgs;
return function (...args) {
lastArgs = args;
const wait = ms - (Date.now() - last);
if (wait <= 0) {
clearTimeout(timer);
timer = null;
last = Date.now();
fn.apply(this, args);
} else if (!timer) {
timer = setTimeout(() => {
last = Date.now();
timer = null;
fn.apply(this, lastArgs);
}, wait);
}
};
}
const onScroll = throttle((n) => console.log('scroll', n), 100);
onScroll(1);
onScroll(2);
onScroll(3);
setTimeout(() => onScroll(4), 150);scroll 1
scroll 3
scroll 4Что проверяют. Отличаете ли вы throttle от debounce. debounce ждёт тишины, throttle держит ровный темп. Частая ошибка: забыть последний вызов, и тогда интерфейс показывает устаревшее положение прокрутки.
Вариант с настройками leading и trailing разобран в задаче throttle с краями.
3. Promise.all
Условие. Реализуйте свой Promise.all. На вход массив промисов и обычных значений. На выходе промис с массивом результатов в исходном порядке. Первая ошибка сразу отклоняет общий промис.
function promiseAll(items) {
return new Promise((resolve, reject) => {
const list = Array.from(items);
const results = new Array(list.length);
let done = 0;
if (list.length === 0) return resolve(results);
list.forEach((item, i) => {
Promise.resolve(item).then((value) => {
results[i] = value;
done += 1;
if (done === list.length) resolve(results);
}, reject);
});
});
}
const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));
promiseAll([wait(30, 'a'), 'b', wait(10, 'c')]).then((r) => console.log(r));
promiseAll([wait(10, 1), Promise.reject(new Error('упал'))]).catch((e) => console.log('ошибка:', e.message));
promiseAll([]).then((r) => console.log(r));[]
ошибка: упал
[ 'a', 'b', 'c' ]Что проверяют. Результат кладут по индексу, а не через push: иначе порядок зависит от того, кто ответил быстрее. Счётчик done нужен, потому что длина массива с дырками не говорит, сколько промисов завершилось. Обычные значения оборачивают в Promise.resolve. Пустой массив сразу даёт пустой результат.
Обратите внимание на порядок вывода. Пустой массив и ошибка печатаются раньше, чем результат с таймерами, потому что они обрабатываются в микрозадачах, а таймеры ждут своей очереди. Это повод заговорить про event loop, если интервьюер спросит. Потренировать задачу можно в promiseAll.
4. deepEqual
Условие. Сравните два значения глубоко: объекты и массивы равны, если равны все их поля.
function deepEqual(a, b) {
if (Object.is(a, b)) return true;
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every((k) => Object.hasOwn(b, k) && deepEqual(a[k], b[k]));
}
console.log(deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }));
console.log(deepEqual({ a: 1 }, { a: 1, b: undefined }));
console.log(deepEqual([1, 2], { 0: 1, 1: 2 }));
console.log(deepEqual(NaN, NaN));true
false
false
trueЧто проверяют. null тоже имеет тип object, его отсекают отдельно. Массив и объект с теми же ключами не равны. Поле со значением undefined и отсутствующее поле различаются. Object.is считает NaN равным NaN, а обычное сравнение нет.
Дальше спросят про циклические ссылки. Ответ: храните пары уже сравниваемых объектов в WeakMap и не заходите в них повторно. Похожая логика нужна в deepClone с циклами. Базовую версию сравнения можно сдать в задаче deepEqual.
5. EventEmitter
Условие. Сделайте класс с методами on, off, once и emit. Подписчики события вызываются в порядке подписки.
class EventEmitter {
#handlers = new Map();
on(event, fn) {
if (!this.#handlers.has(event)) this.#handlers.set(event, new Set());
this.#handlers.get(event).add(fn);
return () => this.off(event, fn);
}
off(event, fn) {
this.#handlers.get(event)?.delete(fn);
}
once(event, fn) {
const off = this.on(event, (...args) => {
off();
fn(...args);
});
return off;
}
emit(event, ...args) {
for (const fn of [...(this.#handlers.get(event) ?? [])]) fn(...args);
}
}
const bus = new EventEmitter();
const stop = bus.on('msg', (t) => console.log('a:', t));
bus.once('msg', (t) => console.log('b:', t));
bus.emit('msg', 1);
stop();
bus.emit('msg', 2);
console.log('конец');a: 1
b: 1
конецЧто проверяют. emit перебирает копию списка. Без копии once отписывается прямо во время обхода, и следующий подписчик может пропуститься. on возвращает функцию отписки: так удобнее в React-эффектах. Set не даст подписать одну и ту же функцию дважды, об этом стоит сказать вслух, если интервьюер ждёт повторов.
Задача целиком с тестами: EventEmitter.
6. flatten
Условие. Разверните вложенный массив на заданную глубину. Без глубины разворачивайте полностью.
function flatten(arr, depth = Infinity) {
const out = [];
for (const x of arr) {
if (Array.isArray(x) && depth > 0) out.push(...flatten(x, depth - 1));
else out.push(x);
}
return out;
}
console.log(flatten([1, [2, [3, [4]]], 5]));
console.log(flatten([1, [2, [3, [4]]]], 1));[ 1, 2, 3, 4, 5 ]
[ 1, 2, [ 3, [ 4 ] ] ]Что проверяют. Рекурсию и параметр глубины. Встроенный arr.flat(depth) знать нужно, но писать его просят именно руками. Частое продолжение: решить без рекурсии через стек, чтобы очень глубокий массив не переполнил стек вызовов. Версии с глубиной: flattenDepth и flattenDeep.
7. useDebounce на React
Условие. Напишите хук, который возвращает значение с задержкой: поле поиска меняется сразу, а запрос уходит, когда пользователь перестал печатать.
function useDebounce<T>(value: T, ms: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), ms);
return () => clearTimeout(id);
}, [value, ms]);
return debounced;
}
function Search() {
const [query, setQuery] = useState('');
const q = useDebounce(query, 300);
useEffect(() => {
if (q) loadResults(q);
}, [q]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}Что проверяют. Очистку эффекта: при каждом новом значении старый таймер отменяется, поэтому срабатывает только последний. Список зависимостей полный. Следующий вопрос обычно про гонку ответов: запрос на «rea» может вернуться позже запроса на «react». Ответ: отменять старый запрос через AbortController в очистке эффекта или игнорировать устаревший ответ. Хук целиком: useDebounce.
Как готовиться к задачам
- Напишите каждую задачу из списка сами, без подсказок, и прогоните тесты. Чтение готового решения почти ничего не даёт.
- Через два-три дня напишите ещё раз с чистого листа. Если застряли, значит, решение было заучено, а не понято.
- Решайте вслух и с таймером: 20 минут на задачу. На собеседовании молчание пугает интервьюера сильнее, чем ошибка.
- После каждой задачи ответьте себе на вопрос «что спросят дальше» и допишите это продолжение.
Частые ошибки
- Сразу писать код, не уточнив условие: вызов в начале или в конце, что делать с ошибкой, какой порядок результатов
- Хранить таймер или результаты в глобальной переменной, а не в замыкании
- В Promise.all складывать результаты через push и терять порядок
- Забыть про null, NaN и пустой массив
- Молчать, пока пишете. Интервьюер не видит ход мысли и не может подсказать
- Не проверить код на своём же примере перед тем, как сказать «готово»
Чек-лист
- debounce и throttle пишутся за 10 минут и вы можете объяснить разницу одной фразой
- Promise.all, а следом allSettled и any, без подглядывания
- deepEqual с объяснением, почему null и NaN требуют отдельной проверки
- EventEmitter с once и отпиской во время emit
- flatten рекурсией и через стек
- Один кастомный хук React с очисткой эффекта
Вопросы из базы Техпрогона
Частые вопросы по теме с ответом и разбором. Откройте любой и попробуйте ответить сами.
- Отмена запросаJavaScript
- async/awaitJavaScript
- Промис: состояния и гарантииJavaScript
- Комбинаторы промисовJavaScript
- Гонки в интерфейсеJavaScript
- Колбэки, промисы и async/awaitJavaScript
- Как вы решаете задачу на live codingJavaScript
- Реализуйте Promise.allJavaScript
- Что возвращает async-функцияJavaScript
- Состояния промисаJavaScript
Как тренировать в Техпрогоне
- Все задачи из статьи есть в каталоге JavaScript с автопроверкой: решение запускается на тестах прямо в браузере.
- Лестница стресса: сначала задача «Наедине» с подсказками, потом «Вслух» с записью и «Под давлением» с таймером и уточняющими вопросами.
- Билеты с зачётом по JavaScript: 20 вопросов на замыкания, промисы и event loop, зачёт при двух ошибках и меньше.
- Бесплатно доступны 3 задачи с кодом в день и мок «Наедине». Остальные ступени и задачи без лимита в тарифе «Рывок».
Частые вопросы
Какие задачи чаще всего дают на собеседовании frontend?
Можно ли пользоваться документацией во время задачи?
Что делать, если не помню, как решается задача?
Нужно ли писать на TypeScript?
Сколько задач нужно решить перед собеседованием?
Билеты с зачётом вместо чтения статей
20 вопросов в билете, зачёт при двух ошибках и меньше, разбор каждой ошибки. Бесплатный тариф без ограничения по времени.
Читайте также
Что выведет код: задачи JavaScript
Задачи «что выведет консоль» с frontend-собеседований: var и let в цикле, hoisting, this, event loop, приведение типов и ссылки. Вывод и разбор к каждой.
Код-ревью на собеседовании
Как проходит код-ревью на собеседовании frontend: компонент с восемью проблемами, порядок ответа, вежливые формулировки замечаний и исправленная версия.
План подготовки на 30 дней
План на 30 дней для junior и middle frontend: JavaScript, браузер и сеть, React, TypeScript и вёрстка, задачи с кодом, рассказ о себе и моки. По дням.
Лайвкодинг на собеседовании
Как проходит и что делать, если завис
Вопросы по JavaScript
Каталог вопросов и задач с ответами
Все статьи
Первая работа, подготовка к собесу и задачи с кодом