В продакшн-коде часто нужно добавить поведение к существующей функции, не меняя её код: кэшировать результат, логировать вызовы, ограничить частоту. Паттерн «декоратор» решает это через обёртку. call и apply — инструменты, которые делают обёртку прозрачной: декорируемая функция получает правильный this и все аргументы.
...args для передачи произвольного числа аргументовcall(ctx, ...args) и apply(ctx, args)call вызывает функцию, явно задавая this и аргументы по отдельности:
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const user = { name: 'Мария' }
greet.call(user, 'Привет', '!') // 'Привет, Мария!'apply работает так же, но принимает аргументы массивом:
greet.apply(user, ['Здравствуйте', '.']) // 'Здравствуйте, Мария.'
// Удобно, когда аргументы уже в массиве:
const args = ['Добрый день', '?']
greet.apply(user, args) // 'Добрый день, Мария?'| | call | apply |
|--|------|-------|
| Аргументы | По одному | Массивом |
| Когда удобно | Фиксированное число аргументов | Аргументы уже в массиве |
| Аналог через spread | fn.call(ctx, ...arr) | fn.apply(ctx, arr) |
Декоратор — функция, принимающая другую функцию и возвращающая новую с расширенным поведением. Исходная функция не изменяется.
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) // мгновенно из кэша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))function delay(fn, ms) {
return function(...args) {
setTimeout(() => fn.apply(this, args), ms)
}
}
const logLater = delay(console.log, 1000)
logLater('Это сообщение появится через 1 секунду')Внутри декоратора 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 с кэшированием по аргументам и декоратор 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 — инструменты, которые делают обёртку прозрачной: декорируемая функция получает правильный this и все аргументы.
...args для передачи произвольного числа аргументовcall(ctx, ...args) и apply(ctx, args)call вызывает функцию, явно задавая this и аргументы по отдельности:
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`
}
const user = { name: 'Мария' }
greet.call(user, 'Привет', '!') // 'Привет, Мария!'apply работает так же, но принимает аргументы массивом:
greet.apply(user, ['Здравствуйте', '.']) // 'Здравствуйте, Мария.'
// Удобно, когда аргументы уже в массиве:
const args = ['Добрый день', '?']
greet.apply(user, args) // 'Добрый день, Мария?'| | call | apply |
|--|------|-------|
| Аргументы | По одному | Массивом |
| Когда удобно | Фиксированное число аргументов | Аргументы уже в массиве |
| Аналог через spread | fn.call(ctx, ...arr) | fn.apply(ctx, arr) |
Декоратор — функция, принимающая другую функцию и возвращающая новую с расширенным поведением. Исходная функция не изменяется.
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) // мгновенно из кэша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))function delay(fn, ms) {
return function(...args) {
setTimeout(() => fn.apply(this, args), ms)
}
}
const logLater = delay(console.log, 1000)
logLater('Это сообщение появится через 1 секунду')Внутри декоратора 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 с кэшированием по аргументам и декоратор 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 и передать аргументы из массива.