«Напишите debounce», «реализуйте Promise.all», «сделайте deepClone»: такие задачи дают на live coding почти в каждой фронтенд-компании. Проверяют не память, а понимание замыканий, this, промисов и крайних случаев. Здесь порядок работы и разбор ключевых реализаций.
Как вести задачу
- Уточнить требования. Что с крайними случаями: пустой массив,
null, циклические ссылки? Нужен лиthis? Нужна ли отмена? - Проговорить сигнатуру и пример.
debounce(fn, ms)возвращает функцию;promiseAll([])выполняется пустым массивом. - Сначала простое рабочее решение, потом улучшения. Пустой экран хуже неидеального кода.
- Прогнать вслух на примере, проверить крайние случаи.
- Сказать про сложность и ограничения: что не поддержано и как бы вы это доделали.
debounce
Откладывает вызов, пока вызовы продолжаются; срабатывает через ms после последнего.
function debounce(fn, ms) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), ms);
};
}
Ключевые моменты: таймер в замыкании, function а не стрелка у обёртки, чтобы сохранить this, передача последних аргументов. Расширения: leading (вызов на первом событии), методы cancel и flush.
throttle
Вызывает не чаще одного раза за ms.
function throttle(fn, ms) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= ms) {
last = now;
fn.apply(this, args);
}
};
}
Это вариант «leading»: последний вызов в окне теряется. Полная версия с trailing запоминает последние аргументы и вызывает функцию по таймеру в конце окна.
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; // по индексу, а не push: порядок важен
if (++done === list.length) resolve(results);
}, reject);
});
});
}
Частые ошибки: push вместо записи по индексу (порядок по времени завершения), проверка results.length вместо счётчика, забытый пустой массив (промис зависнет), отсутствие Promise.resolve для не-промисов.
allSettled отличается тем, что никогда не отклоняется и пишет { status, value } или { status, reason }. race резолвит первый завершившийся. any копит ошибки и отклоняется AggregateError, когда упали все.
deepClone
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== 'object') return value;
if (seen.has(value)) return seen.get(value); // циклы
if (value instanceof Date) return new Date(value);
if (value instanceof Map) {
const copy = new Map();
seen.set(value, copy);
value.forEach((v, k) => copy.set(deepClone(k, seen), deepClone(v, seen)));
return copy;
}
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
seen.set(value, copy); // до рекурсии
for (const key of Reflect.ownKeys(value)) copy[key] = deepClone(value[key], seen);
return copy;
}
Важно: регистрировать копию в seen до рекурсии, иначе цикл уйдёт в бесконечность. Обсудите, что делать с Set, RegExp, функциями, геттерами. В реальном коде почти всегда хватает structuredClone.
deepEqual
Сравнить примитивы через Object.is или ===, отсечь разные типы и null, сравнить количество ключей, рекурсивно сравнить значения. Не забыть массивы против объектов ([] и {} с нулём ключей не равны) и, по желанию, даты и циклы.
curry, memoize, compose
const curry = (fn) => function curried(...args) {
return args.length >= fn.length
? fn.apply(this, args)
: (...more) => curried.apply(this, [...args, ...more]);
};
memoize хранит Map в замыкании, ключ строит по всем аргументам. compose это reduceRight по функциям.
EventEmitter
Методы on, off, emit, once. Хранилище Map<string, Set<Function>> или объект массивов. Тонкости: once должен удалить обёртку, а off уметь снять её по исходной функции; при emit перебирать копию списка, чтобы отписка внутри обработчика не ломала цикл.
Свой bind и call
call реализуется трюком «положить функцию в свойство объекта под уникальным Symbol и вызвать как метод». bind возвращает функцию, склеивает аргументы и учитывает вызов через new (new.target).
Что спросят на собесе
- Реализуйте
debounce, затем добавьтеcancelиleading. - Реализуйте
Promise.all(илиallSettled,race,any). - Напишите
deepCloneс поддержкой циклических ссылок. - Напишите
curry,memoize,EventEmitterили свойbind. - Какие крайние случаи вы учли и какая сложность у решения.