← Назад к вопросам

Реализуйте функцию debounce

2.0 Middle🔥 171 комментариев
#Node.js и JavaScript#Алгоритмы и структуры данных

Условие

Напишите функцию debounce(fn, delay), которая возвращает новую функцию, откладывающую вызов fn до тех пор, пока не пройдет delay миллисекунд с момента последнего вызова.

function debounce(fn, delay) {
  // Ваш код
}

// Пример использования:
const debouncedLog = debounce((x) => console.log(x), 300);
debouncedLog(1);
debouncedLog(2);
debouncedLog(3); // Только это значение будет выведено через 300мс

Что проверяется

  • Работа с таймерами (setTimeout, clearTimeout)
  • Замыкания в JavaScript
  • Практическое понимание паттерна debounce

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Базовая реализация

function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
  let timerId: NodeJS.Timeout | null = null;

  return function (...args: Parameters<T>) {
    // Отменяем предыдущий таймер
    if (timerId !== null) {
      clearTimeout(timerId);
    }

    // Устанавливаем новый таймер
    timerId = setTimeout(() => {
      fn(...args);
      timerId = null;
    }, delay);
  };
}

Как это работает

Ключевой механизм — замыкание (Closure):

  • Переменная timerId сохраняется в замыкании возвращаемой функции
  • Каждый вызов debounced функции имеет доступ к одному и тому же timerId
  • Это позволяет отменять предыдущие таймеры

Порядок выполнения:

  1. debouncedLog(1)timerId = setTimeout(..., 300)
  2. debouncedLog(2)clearTimeout(timerId) → новый таймер на 300мс
  3. debouncedLog(3)clearTimeout(timerId) → новый таймер на 300мс
  4. Через 300мс: fn(3) → вывод 3

Пример использования

const debouncedLog = debounce((x: number) => console.log(x), 300);

debouncedLog(1);
debouncedLog(2);
debouncedLog(3);
// Через 300мс выведет: 3

Реальный пример: поиск в API

const searchAPI = (query: string) => {
  console.log(`Поиск: ${query}`);
  // API запрос
};

const debouncedSearch = debounce(searchAPI, 500);

input.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
  // Запрос отправится только через 500мс после последнего символа
});

Версия с возвращаемым значением

function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => Promise<ReturnType<T>> {
  let timerId: NodeJS.Timeout | null = null;

  return function (...args: Parameters<T>): Promise<ReturnType<T>> {
    return new Promise((resolve) => {
      if (timerId !== null) {
        clearTimeout(timerId);
      }

      timerId = setTimeout(() => {
        const result = fn(...args);
        resolve(result);
        timerId = null;
      }, delay);
    });
  };
}

Версия с методом cancel

function debounce<T extends (...args: any[]) => any>(fn: T, delay: number) {
  let timerId: NodeJS.Timeout | null = null;

  const debounced = function (...args: Parameters<T>) {
    if (timerId !== null) {
      clearTimeout(timerId);
    }

    timerId = setTimeout(() => {
      fn(...args);
      timerId = null;
    }, delay);
  };

  debounced.cancel = () => {
    if (timerId !== null) {
      clearTimeout(timerId);
      timerId = null;
    }
  };

  return debounced;
}

const debouncedSearch = debounce(searchAPI, 500);
window.addEventListener('beforeunload', () => debouncedSearch.cancel());

Тестирование

import { describe, it, expect, vi } from 'vitest';

describe('debounce', () => {
  it('должен задержать вызов функции', () => {
    vi.useFakeTimers();
    const fn = vi.fn();
    const debounced = debounce(fn, 300);

    debounced('hello');
    expect(fn).not.toHaveBeenCalled();

    vi.advanceTimersByTime(299);
    expect(fn).not.toHaveBeenCalled();

    vi.advanceTimersByTime(1);
    expect(fn).toHaveBeenCalledWith('hello');
    expect(fn).toHaveBeenCalledTimes(1);
  });

  it('должен сбросить таймер при повторном вызове', () => {
    vi.useFakeTimers();
    const fn = vi.fn();
    const debounced = debounce(fn, 300);

    debounced(1);
    vi.advanceTimersByTime(100);
    debounced(2);
    vi.advanceTimersByTime(100);
    debounced(3);

    vi.advanceTimersByTime(300);
    expect(fn).toHaveBeenCalledWith(3);
    expect(fn).toHaveBeenCalledTimes(1);
  });
});

Debounce vs Throttle

AspektDebounceThrottle
СутьОткладывает вызов до затишьяОграничивает частоту вызовов
ИспользуетсяSearch input, resize eventsScroll, mouse move
РезультатВызов только после паузыМаксимум N раз в секунду

Практические применения

  • Поиск с автодополнением — не отправлять запрос на каждый символ
  • Проверка уникальности username — проверять только после паузы
  • Сохранение черновика — сохранять через 5 сек после последнего изменения
  • Resize/Scroll обработчики — пересчитывать размеры после окончания resizing
  • Валидация формы — проверять поле только после паузы в вводе
Реализуйте функцию debounce | PrepBro