Урок по треку JavaScript 8 мин

Реализация с нуля за 8 минут

debounce, throttle, Promise.all, deepClone и другие классические задачи на live coding.

«Напишите debounce», «реализуйте Promise.all», «сделайте deepClone»: такие задачи дают на live coding почти в каждой фронтенд-компании. Проверяют не память, а понимание замыканий, this, промисов и крайних случаев. Здесь порядок работы и разбор ключевых реализаций.

Как вести задачу

  1. Уточнить требования. Что с крайними случаями: пустой массив, null, циклические ссылки? Нужен ли this? Нужна ли отмена?
  2. Проговорить сигнатуру и пример. debounce(fn, ms) возвращает функцию; promiseAll([]) выполняется пустым массивом.
  3. Сначала простое рабочее решение, потом улучшения. Пустой экран хуже неидеального кода.
  4. Прогнать вслух на примере, проверить крайние случаи.
  5. Сказать про сложность и ограничения: что не поддержано и как бы вы это доделали.

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.
  • Какие крайние случаи вы учли и какая сложность у решения.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.

Вопросы модуля (124)