← JavaScript/Декораторы и переадресация вызова: call/apply#114 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Декораторы и переадресация вызова: call/apply

В продакшн-коде часто нужно добавить поведение к существующей функции, не меняя её код: кэшировать результат, логировать вызовы, ограничить частоту. Паттерн «декоратор» решает это через обёртку. call и apply — инструменты, которые делают обёртку прозрачной: декорируемая функция получает правильный this и все аргументы.

На основе предыдущих уроков

  • «Функции» — функции как значения, замыкания
  • «Замыкания» — замкнутые переменные (кэш, lastCallTime) живут в обёртке
  • «Rest/Spread» — ...args для передачи произвольного числа аргументов
  • «bind/call/apply» — call(ctx, ...args) и apply(ctx, args)
  • func.call(context, arg1, arg2, ...)

    call вызывает функцию, явно задавая this и аргументы по отдельности:

    function greet(greeting, punctuation) {
      return `${greeting}, ${this.name}${punctuation}`
    }
    
    const user = { name: 'Мария' }
    greet.call(user, 'Привет', '!')  // 'Привет, Мария!'

    func.apply(context, [args])

    apply работает так же, но принимает аргументы массивом:

    greet.apply(user, ['Здравствуйте', '.'])  // 'Здравствуйте, Мария.'
    
    // Удобно, когда аргументы уже в массиве:
    const args = ['Добрый день', '?']
    greet.apply(user, args)  // 'Добрый день, Мария?'

    Отличие call от apply

    | | call | apply |

    |--|------|-------|

    | Аргументы | По одному | Массивом |

    | Когда удобно | Фиксированное число аргументов | Аргументы уже в массиве |

    | Аналог через spread | fn.call(ctx, ...arr) | fn.apply(ctx, arr) |

    Декораторы — функции-обёртки

    Декоратор — функция, принимающая другую функцию и возвращающая новую с расширенным поведением. Исходная функция не изменяется.

    memoize — кэширование результатов

    function memoize(fn) {
      const cache = new Map()
    
      return function(...args) {
        const key = JSON.stringify(args)
        if (cache.has(key)) {
          return cache.get(key)
        }
        const result = fn.apply(this, args)  // сохраняем контекст!
        cache.set(key, result)
        return result
      }
    }
    
    function slowFactorial(n) {
      return n <= 1 ? 1 : n * slowFactorial(n - 1)
    }
    
    const factorial = memoize(slowFactorial)
    factorial(10)  // вычислено
    factorial(10)  // мгновенно из кэша

    throttle — ограничение частоты вызовов

    function throttle(fn, ms) {
      let lastCallTime = 0
    
      return function(...args) {
        const now = Date.now()
        if (now - lastCallTime >= ms) {
          lastCallTime = now
          fn.apply(this, args)
        }
      }
    }
    
    // Обработчик прокрутки срабатывает не чаще раза в 200мс:
    // window.addEventListener('scroll', throttle(updateScrollIndicator, 200))

    delay — задержка вызова

    function delay(fn, ms) {
      return function(...args) {
        setTimeout(() => fn.apply(this, args), ms)
      }
    }
    
    const logLater = delay(console.log, 1000)
    logLater('Это сообщение появится через 1 секунду')

    Почему важен fn.apply(this, args)

    Внутри декоратора this — это контекст вызова обёртки, а не оригинальной функции. Если функция — метод объекта, нужно передать контекст правильно:

    class UserService {
      constructor(prefix) { this.prefix = prefix }
    
      greet(name) { return `[${this.prefix}] Привет, ${name}!` }
    }
    
    const service = new UserService('API')
    const memoGreet = memoize(service.greet.bind(service))
    
    console.log(memoGreet('Алиса'))  // '[API] Привет, Алиса!'
    console.log(memoGreet('Алиса'))  // из кэша

    Типичные ошибки

    1. Потеря контекста — использование fn(...args) вместо fn.apply(this, args):

    // Плохо: this внутри оригинальной функции будет undefined (strict) или global
    function badDecorator(fn) {
      return function(...args) {
        return fn(...args)  // this потерян!
      }
    }
    
    // Хорошо: передаём this явно
    function goodDecorator(fn) {
      return function(...args) {
        return fn.apply(this, args)  // this сохранён
      }
    }

    2. Стрелочная функция в декораторе не имеет своего this:

    // Плохо: стрелочная функция захватывает this из внешней области
    function badMemoize(fn) {
      const cache = new Map()
      return (...args) => {  // стрелочная — this из лексического окружения
        const key = JSON.stringify(args)
        if (cache.has(key)) return cache.get(key)
        const result = fn.apply(this, args)  // this здесь — не то что нужно!
        cache.set(key, result)
        return result
      }
    }
    
    // Хорошо: обычная function expression
    function goodMemoize(fn) {
      const cache = new Map()
      return function(...args) {  // обычная — this приходит от вызывающего
        const key = JSON.stringify(args)
        if (cache.has(key)) return cache.get(key)
        const result = fn.apply(this, args)
        cache.set(key, result)
        return result
      }
    }

    3. Кэш не учитывает контекст — разные объекты получают один результат:

    // Проблема: ключ кэша только по args, не по this
    // Если memoize применить к методу без bind — два разных объекта
    // с одинаковыми аргументами получат одинаковый кэшированный результат
    // Решение: bind перед memoize
    const memoMethod = memoize(obj.method.bind(obj))

    В реальных проектах

  • memoize — кэширование API-запросов, тяжёлых вычислений (парсинг, трансформации)
  • throttle — обработчики scroll, resize, mousemove
  • debounce — поиск по мере ввода, автосохранение форм
  • logging decorator — автоматическое логирование вызовов в аналитику
  • retry decorator — повтор упавших HTTP-запросов
  • Примеры

    Декоратор memoize с кэшированием по аргументам и декоратор delay для отложенного уведомления

    function memoize(fn) {
      const cache = new Map()
    
      return function(...args) {
        const key = JSON.stringify(args)
    
        if (cache.has(key)) {
          return { result: cache.get(key), fromCache: true }
        }
    
        const result = fn.apply(this, args)
        cache.set(key, result)
        return { result, fromCache: false }
      }
    }
    
    // Тяжёлая функция: подсчёт числа Фибоначчи (экспоненциальная сложность без кэша)
    function fib(n) {
      if (n <= 1) return n
      return fib(n - 1) + fib(n - 2)
    }
    
    const memoFib = memoize(fib)
    
    console.time('первый вызов')
    console.log(memoFib(40))  // { result: 102334155, fromCache: false }
    console.timeEnd('первый вызов')
    
    console.time('второй вызов')
    console.log(memoFib(40))  // { result: 102334155, fromCache: true }
    console.timeEnd('второй вызов')  // в тысячи раз быстрее!
    
    // Декоратор delay — откладывает уведомление
    function delay(fn, ms) {
      return function(...args) {
        setTimeout(() => fn.apply(this, args), ms)
      }
    }
    
    function notifyUser(name, message) {
      console.log(`[${new Date().toLocaleTimeString()}] ${name}: ${message}`)
    }
    
    const delayedNotify = delay(notifyUser, 2000)
    delayedNotify('Иван', 'Ваш заказ готов')
    // Через 2 секунды: '[12:34:56] Иван: Ваш заказ готов'

    Декораторы и переадресация вызова: call/apply

    В продакшн-коде часто нужно добавить поведение к существующей функции, не меняя её код: кэшировать результат, логировать вызовы, ограничить частоту. Паттерн «декоратор» решает это через обёртку. call и apply — инструменты, которые делают обёртку прозрачной: декорируемая функция получает правильный this и все аргументы.

    На основе предыдущих уроков

  • «Функции» — функции как значения, замыкания
  • «Замыкания» — замкнутые переменные (кэш, lastCallTime) живут в обёртке
  • «Rest/Spread» — ...args для передачи произвольного числа аргументов
  • «bind/call/apply» — call(ctx, ...args) и apply(ctx, args)
  • func.call(context, arg1, arg2, ...)

    call вызывает функцию, явно задавая this и аргументы по отдельности:

    function greet(greeting, punctuation) {
      return `${greeting}, ${this.name}${punctuation}`
    }
    
    const user = { name: 'Мария' }
    greet.call(user, 'Привет', '!')  // 'Привет, Мария!'

    func.apply(context, [args])

    apply работает так же, но принимает аргументы массивом:

    greet.apply(user, ['Здравствуйте', '.'])  // 'Здравствуйте, Мария.'
    
    // Удобно, когда аргументы уже в массиве:
    const args = ['Добрый день', '?']
    greet.apply(user, args)  // 'Добрый день, Мария?'

    Отличие call от apply

    | | call | apply |

    |--|------|-------|

    | Аргументы | По одному | Массивом |

    | Когда удобно | Фиксированное число аргументов | Аргументы уже в массиве |

    | Аналог через spread | fn.call(ctx, ...arr) | fn.apply(ctx, arr) |

    Декораторы — функции-обёртки

    Декоратор — функция, принимающая другую функцию и возвращающая новую с расширенным поведением. Исходная функция не изменяется.

    memoize — кэширование результатов

    function memoize(fn) {
      const cache = new Map()
    
      return function(...args) {
        const key = JSON.stringify(args)
        if (cache.has(key)) {
          return cache.get(key)
        }
        const result = fn.apply(this, args)  // сохраняем контекст!
        cache.set(key, result)
        return result
      }
    }
    
    function slowFactorial(n) {
      return n <= 1 ? 1 : n * slowFactorial(n - 1)
    }
    
    const factorial = memoize(slowFactorial)
    factorial(10)  // вычислено
    factorial(10)  // мгновенно из кэша

    throttle — ограничение частоты вызовов

    function throttle(fn, ms) {
      let lastCallTime = 0
    
      return function(...args) {
        const now = Date.now()
        if (now - lastCallTime >= ms) {
          lastCallTime = now
          fn.apply(this, args)
        }
      }
    }
    
    // Обработчик прокрутки срабатывает не чаще раза в 200мс:
    // window.addEventListener('scroll', throttle(updateScrollIndicator, 200))

    delay — задержка вызова

    function delay(fn, ms) {
      return function(...args) {
        setTimeout(() => fn.apply(this, args), ms)
      }
    }
    
    const logLater = delay(console.log, 1000)
    logLater('Это сообщение появится через 1 секунду')

    Почему важен fn.apply(this, args)

    Внутри декоратора this — это контекст вызова обёртки, а не оригинальной функции. Если функция — метод объекта, нужно передать контекст правильно:

    class UserService {
      constructor(prefix) { this.prefix = prefix }
    
      greet(name) { return `[${this.prefix}] Привет, ${name}!` }
    }
    
    const service = new UserService('API')
    const memoGreet = memoize(service.greet.bind(service))
    
    console.log(memoGreet('Алиса'))  // '[API] Привет, Алиса!'
    console.log(memoGreet('Алиса'))  // из кэша

    Типичные ошибки

    1. Потеря контекста — использование fn(...args) вместо fn.apply(this, args):

    // Плохо: this внутри оригинальной функции будет undefined (strict) или global
    function badDecorator(fn) {
      return function(...args) {
        return fn(...args)  // this потерян!
      }
    }
    
    // Хорошо: передаём this явно
    function goodDecorator(fn) {
      return function(...args) {
        return fn.apply(this, args)  // this сохранён
      }
    }

    2. Стрелочная функция в декораторе не имеет своего this:

    // Плохо: стрелочная функция захватывает this из внешней области
    function badMemoize(fn) {
      const cache = new Map()
      return (...args) => {  // стрелочная — this из лексического окружения
        const key = JSON.stringify(args)
        if (cache.has(key)) return cache.get(key)
        const result = fn.apply(this, args)  // this здесь — не то что нужно!
        cache.set(key, result)
        return result
      }
    }
    
    // Хорошо: обычная function expression
    function goodMemoize(fn) {
      const cache = new Map()
      return function(...args) {  // обычная — this приходит от вызывающего
        const key = JSON.stringify(args)
        if (cache.has(key)) return cache.get(key)
        const result = fn.apply(this, args)
        cache.set(key, result)
        return result
      }
    }

    3. Кэш не учитывает контекст — разные объекты получают один результат:

    // Проблема: ключ кэша только по args, не по this
    // Если memoize применить к методу без bind — два разных объекта
    // с одинаковыми аргументами получат одинаковый кэшированный результат
    // Решение: bind перед memoize
    const memoMethod = memoize(obj.method.bind(obj))

    В реальных проектах

  • memoize — кэширование API-запросов, тяжёлых вычислений (парсинг, трансформации)
  • throttle — обработчики scroll, resize, mousemove
  • debounce — поиск по мере ввода, автосохранение форм
  • logging decorator — автоматическое логирование вызовов в аналитику
  • retry decorator — повтор упавших HTTP-запросов
  • Примеры

    Декоратор memoize с кэшированием по аргументам и декоратор delay для отложенного уведомления

    function memoize(fn) {
      const cache = new Map()
    
      return function(...args) {
        const key = JSON.stringify(args)
    
        if (cache.has(key)) {
          return { result: cache.get(key), fromCache: true }
        }
    
        const result = fn.apply(this, args)
        cache.set(key, result)
        return { result, fromCache: false }
      }
    }
    
    // Тяжёлая функция: подсчёт числа Фибоначчи (экспоненциальная сложность без кэша)
    function fib(n) {
      if (n <= 1) return n
      return fib(n - 1) + fib(n - 2)
    }
    
    const memoFib = memoize(fib)
    
    console.time('первый вызов')
    console.log(memoFib(40))  // { result: 102334155, fromCache: false }
    console.timeEnd('первый вызов')
    
    console.time('второй вызов')
    console.log(memoFib(40))  // { result: 102334155, fromCache: true }
    console.timeEnd('второй вызов')  // в тысячи раз быстрее!
    
    // Декоратор delay — откладывает уведомление
    function delay(fn, ms) {
      return function(...args) {
        setTimeout(() => fn.apply(this, args), ms)
      }
    }
    
    function notifyUser(name, message) {
      console.log(`[${new Date().toLocaleTimeString()}] ${name}: ${message}`)
    }
    
    const delayedNotify = delay(notifyUser, 2000)
    delayedNotify('Иван', 'Ваш заказ готов')
    // Через 2 секунды: '[12:34:56] Иван: Ваш заказ готов'

    Задание

    На сайте магазина кнопка "Оформить заказ" должна реагировать не чаще одного раза в 300мс (защита от двойного клика). Напиши декоратор throttle(fn, ms), который ограничивает частоту вызовов: после каждого реального вызова должно пройти не менее ms миллисекунд до следующего.

    Подсказка

    lastCallTime = 0 в начале (чтобы первый вызов всегда прошёл). Проверяй Date.now() - lastCallTime >= ms. При успешном вызове: lastCallTime = now, затем fn.apply(this, args). apply нужен, чтобы сохранить контекст this и передать аргументы из массива.

    Загружаем среду выполнения...
    Загружаем AI-помощника...