Лайвкодинг проверяет не память, а то, как вы работаете с задачей под давлением: уточняете требования, выбираете структуру данных, пишете код и проверяете его. Ниже - четыре задачи, которые регулярно встречаются на собеседованиях frontend- и backend-разработчиков на JavaScript. Для каждой - контракт, решение, граничные случаи и то, что стоит сказать вслух.
Как начинать любую задачу
- Повторите условие своими словами и спросите про вход: может ли массив быть пустым, бывают ли null, какой размер данных.
- Договоритесь о выходе: что вернуть при ошибке, важен ли порядок, можно ли мутировать аргументы.
- Напишите простой рабочий вариант, затем улучшайте. Оптимизация до работающего кода - частая ловушка.
- Прогоните решение на двух-трёх примерах вслух, включая граничный. Так ошибки находите вы, а не интервьюер.
Задача 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.