Собеседования

Задачи на лайвкодинге по JavaScript: debounce, Promise.all и пул запросов

Четыре частые задачи лайвкодинга: debounce, свой Promise.all, ограничение параллельных запросов и deepEqual. Решения, граничные случаи и проверки.

Лайвкодинг проверяет не память, а то, как вы работаете с задачей под давлением: уточняете требования, выбираете структуру данных, пишете код и проверяете его. Ниже - четыре задачи, которые регулярно встречаются на собеседованиях frontend- и backend-разработчиков на JavaScript. Для каждой - контракт, решение, граничные случаи и то, что стоит сказать вслух.

Как начинать любую задачу

  1. Повторите условие своими словами и спросите про вход: может ли массив быть пустым, бывают ли null, какой размер данных.
  2. Договоритесь о выходе: что вернуть при ошибке, важен ли порядок, можно ли мутировать аргументы.
  3. Напишите простой рабочий вариант, затем улучшайте. Оптимизация до работающего кода - частая ловушка.
  4. Прогоните решение на двух-трёх примерах вслух, включая граничный. Так ошибки находите вы, а не интервьюер.

Задача 1. debounce

Условие: функция поиска вызывается на каждый ввод символа, а запрос нужно отправлять, только когда пользователь перестал печатать на ms миллисекунд. Это и есть debounce: каждый новый вызов сбрасывает таймер. Важные детали - сохранить this и аргументы последнего вызова.

JavaScript// debounce: вызвать функцию после паузы в ms миллисекунд export function debounce(fn, ms) { let timer return function (...args) { clearTimeout(timer) timer = setTimeout(() => fn.apply(this, args), ms) } }

Стрелочная функция внутри setTimeout берёт this из обёртки, поэтому метод объекта, обёрнутый в debounce, продолжит работать. Частый дополнительный вопрос - чем debounce отличается от throttle. Throttle не ждёт паузы, а пропускает не больше одного вызова за интервал: так ограничивают обработчики scroll и resize.

JavaScript// throttle: не чаще одного вызова за ms миллисекунд export function throttle(fn, ms) { let last = -Infinity return function (...args) { const now = Date.now() if (now - last >= ms) { last = now fn.apply(this, args) } } }

Что проговорить: эта версия throttle вызывает функцию сразу и отбрасывает промежуточные вызовы. Если нужен ещё и финальный вызов после серии, это отдельное требование - уточните его, а не угадывайте.

Задача 2. Свой Promise.all

Контракт Promise.all: принимает итерируемый набор значений или промисов, возвращает промис массива результатов в исходном порядке, отклоняется первой ошибкой. Пустой набор сразу даёт пустой массив. Не-промисы оборачиваются через Promise.resolve.

JavaScript// Promise.all своими руками: порядок результатов и первая ошибка export function promiseAll(values) { return new Promise((resolve, reject) => { const items = Array.from(values) const results = new Array(items.length) let left = items.length if (left === 0) { resolve(results) return } items.forEach((value, i) => { Promise.resolve(value).then((result) => { results[i] = result left -= 1 if (left === 0) resolve(results) }, reject) }) }) }

Типичная ошибка - складывать результаты через push: тогда порядок зависит от того, какой промис завершился раньше. Вторая - считать готовность по results.length, хотя массив создан нужной длины заранее. Счётчик left решает обе проблемы. Повторный вызов reject после первой ошибки безопасен: промис уже перешёл в итоговое состояние.

Задача 3. Не больше N запросов одновременно

Условие: есть 100 адресов, но сервер выдерживает только 3 параллельных запроса. Promise.all запустит всё сразу, поэтому нужен пул. Идея - запустить limit «рабочих», каждый в цикле забирает следующий индекс из общего счётчика. JavaScript выполняет код между await в одном потоке, поэтому next++ не создаёт гонки.

JavaScript// Пул: не больше limit задач одновременно, порядок результатов сохраняется export async function mapLimit(items, limit, task) { if (!Number.isInteger(limit) || limit < 1) throw new RangeError('limit должен быть целым числом больше 0') const results = new Array(items.length) let next = 0 async function worker() { while (next < items.length) { const i = next++ results[i] = await task(items[i], i) } } const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker()) await Promise.all(workers) return results }

Что проговорить: при ошибке одной задачи mapLimit отклонится, но уже запущенные запросы продолжат выполняться. Если их нужно остановить, передайте в task AbortSignal. Если нужно собрать все результаты вместе с ошибками, сделайте аналог Promise.allSettled.

Задача 4. Глубокое сравнение

deepEqual сравнивает структуры, а не ссылки. Начните с Object.is: он корректно считает NaN равным NaN. Затем отсеките примитивы и null, сравните «массив или объект» и количество ключей, после чего рекурсивно сравните значения.

JavaScript// deepEqual для простых объектов и массивов export 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((key) => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key])) }

Ограничения, которые стоит назвать самому

  • Date, Map, Set и классы с приватным состоянием здесь сравниваются как обычные объекты - для них нужны отдельные ветки.
  • Циклические ссылки приведут к бесконечной рекурсии. Решение - WeakMap уже сравненных пар.
  • Порядок ключей не важен, а порядок элементов массива - важен. Это стоит подтвердить с интервьюером.

Как тренироваться

Решайте задачу вслух с таймером, затем перепишите её через день без подсказок. Записывайте вопросы интервьюера, на которых споткнулись, и закрывайте их отдельно. Порядок выполнения промисов и таймеров подробно разобран в статье про event loop и микрозадачи, а формат собеседования - в материале про лайвкодинг. Тренировки на реальных задачах входят в менторство.

Источники

Контракт Promise.all и Promise.allSettled: MDN. Сравнение значений и Object.is: MDN. Таймеры: HTML Living Standard.

Вопросы по теме

Можно ли на лайвкодинге пользоваться документацией?
Зависит от компании, поэтому спросите в начале. Даже если нельзя, проговорите, что именно вы бы уточнили в документации: это показывает понимание, а не зубрёжку.
Что делать, если не знаю, как решить задачу?
Сформулируйте вход и выход, решите самый простой случай и двигайтесь к общему. Интервьюеру важнее видеть рассуждение и проверки, чем мгновенный правильный ответ.
Нужно ли писать тесты на лайвкодинге?
Полноценный набор тестов обычно не нужен, но несколько вызовов с ожидаемым результатом и разбор граничных случаев сильно повышают доверие к решению.

Читайте также

Все статьи

Обсудим вашу подготовку?

Напишите, что уже умеете и с чем возникли трудности. Если есть проект, пришлите ссылку на него.

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

Что не получается, что уже пробовали. От 100 знаков, сейчас 0.
Где работали или учились, сколько лет, какой стек, сколько откликов и собеседований. От 100 знаков, сейчас 0.
PDF или DOCX, до 5 МБ. Файл видит только администратор.
GitHub, hh, LinkedIn, сайт. Можно несколько, через пробел или с новой строки.