Декораторы
Декоратор — обёртка вокруг функции, которая изменяет или расширяет её поведение, не меняя саму функцию. Паттерн основан на замыканиях и функциях высшего порядка.
Зачем нужно
Декораторы позволяют добавлять функциональность (логирование, кэширование, валидацию, ограничение вызовов) к любой функции без изменения её кода. Это реализация принципа Open/Closed — открыт для расширения, закрыт для изменения.
Где используется
Debounce/throttle, логирование, замер производительности, retry-логика, валидация аргументов, кэширование, авторизация, React HOC.
Предпосылки
Замыкания (Closures), Функции высшего порядка, Мемоизация
Базовый паттерн
function decorator(originalFn) {
return function(...args) {
// Действие ДО вызова
console.log('Вызов с аргументами:', args);
const result = originalFn.apply(this, args); // вызов оригинала
// Действие ПОСЛЕ вызова
console.log('Результат:', result);
return result;
};
}
function add(a, b) {
return a + b;
}
const decoratedAdd = decorator(add);
decoratedAdd(2, 3);
// "Вызов с аргументами: [2, 3]"
// "Результат: 5"
Практические декораторы
Debounce — задержка до стабилизации
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Поиск с задержкой — вызов только после паузы в 300ms
const searchInput = document.querySelector('#search');
const search = debounce((query) => {
console.log('Поиск:', query);
fetch(`/api/search?q=${query}`);
}, 300);
searchInput.addEventListener('input', (e) => search(e.target.value));
Throttle — ограничение частоты вызовов
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
return fn.apply(this, args);
}
};
}
// Обработка скролла — максимум раз в 100ms
const onScroll = throttle(() => {
console.log('Позиция:', window.scrollY);
}, 100);
window.addEventListener('scroll', onScroll);
Throttle с trailing call
function throttle(fn, interval) {
let lastTime = 0;
let timeoutId = null;
return function(...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
clearTimeout(timeoutId);
timeoutId = null;
lastTime = now;
fn.apply(this, args);
} else if (!timeoutId) {
timeoutId = setTimeout(() => {
lastTime = Date.now();
timeoutId = null;
fn.apply(this, args);
}, remaining);
}
};
}
Spy — отслеживание вызовов
function spy(fn) {
function wrapper(...args) {
wrapper.calls.push({
args: [...args],
timestamp: Date.now()
});
return fn.apply(this, args);
}
wrapper.calls = ;
return wrapper;
}
const sum = spy((a, b) => a + b);
sum(1, 2);
sum(3, 4);
console.log(sum.calls);
// [
// { args: [1, 2], timestamp: ... },
// { args: [3, 4], timestamp: ... }
// ]
Once — однократный вызов
function once(fn) {
let called = false;
let result;
return function(...args) {
if (called) return result;
called = true;
result = fn.apply(this, args);
return result;
};
}
const initialize = once(() => {
console.log('Инициализация...');
return { ready: true };
});
initialize; // "Инициализация..." → { ready: true }
initialize; // → { ready: true } (без повторного вызова)
initialize; // → { ready: true }
Delay — отложенный вызов
function delay(fn, ms) {
return function(...args) {
return new Promise(resolve => {
setTimeout(() => {
resolve(fn.apply(this, args));
}, ms);
});
};
}
const delayedLog = delay(console.log, 1000);
delayedLog('Через секунду'); // выведет через 1с
Retry — повтор при ошибке
function retry(fn, attempts = 3, delay = 1000) {
return async function(...args) {
for (let i = 0; i < attempts; i++) {
try {
return await fn.apply(this, args);
} catch (err) {
if (i === attempts - 1) throw err;
console.log(`Попытка ${i + 1} не удалась, повтор через ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
};
}
const fetchWithRetry = retry(
async (url) => {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
3,
2000
);
Логирование и замер времени
function withLogging(fn, label) {
return function(...args) {
console.log(`[${label}] Вызов с:`, args);
console.time(label);
try {
const result = fn.apply(this, args);
console.log(`[${label}] Результат:`, result);
return result;
} catch (err) {
console.error(`[${label}] Ошибка:`, err.message);
throw err;
} finally {
console.timeEnd(label);
}
};
}
const processData = withLogging(
(data) => data.map(x => x * 2),
'processData'
);
processData([1, 2, 3]);
// [processData] Вызов с: [[1, 2, 3]]
// [processData] Результат: [2, 4, 6]
// processData: 0.05ms
Валидация аргументов
function validateArgs(...validators) {
return function(fn) {
return function(...args) {
args.forEach((arg, i) => {
if (validators[i] && !validators[i](arg)) {
throw new TypeError(
`Аргумент ${i} не прошёл валидацию: ${arg}`
);
}
});
return fn.apply(this, args);
};
};
}
const isNumber = (v) => typeof v === 'number' && !isNaN(v);
const isPositive = (v) => isNumber(v) && v > 0;
const safeDivide = validateArgs(isNumber, isPositive)(
(a, b) => a / b
);
console.log(safeDivide(10, 2)); // 5
// safeDivide(10, 0); // TypeError: Аргумент 1 не прошёл валидацию
// safeDivide('a', 2); // TypeError: Аргумент 0 не прошёл валидацию
Композиция декораторов
function compose(...decorators) {
return function(fn) {
return decorators.reduceRight((decorated, decorator) => {
return decorator(decorated);
}, fn);
};
}
// Применяем несколько декораторов
const enhancedFetch = compose(
(fn) => retry(fn, 3),
(fn) => withLogging(fn, 'fetch'),
(fn) => throttle(fn, 1000)
)(fetch);
TC39 Decorators (Stage 3)
Синтаксический сахар для классов (пока не в стандарте):
// Будущий синтаксис (Stage 3)
function log(target, context) {
return function(...args) {
console.log(`Вызов ${context.name}`);
return target.apply(this, args);
};
}
class API {
@log
fetchData(url) {
return fetch(url);
}
}
Частые ошибки
1. Потеря this
// Плохо — this не передаётся
function broken(fn) {
return function(...args) {
return fn(...args); // this потерян!
};
}
// Хорошо — передаём this через apply
function correct(fn) {
return function(...args) {
return fn.apply(this, args);
};
}
2. Потеря свойств оригинальной функции
function myFn() {}
myFn.customProp = 'важно';
const decorated = once(myFn);
console.log(decorated.customProp); // undefined!
// Решение: копировать свойства
function preserveProps(wrapper, original) {
Object.assign(wrapper, original);
Object.defineProperty(wrapper, 'name', {
value: original.name
});
return wrapper;
}
3. Debounce без отмены
// Добавь возможность отмены
function debounce(fn, delay) {
let timeoutId;
function debounced(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout( => fn.apply(this, args), delay);
}
debounced.cancel() = () => clearTimeout(timeoutId);
return debounced;
}
const debouncedSearch = debounce(search, 300);
// При размонтировании компонента:
debouncedSearch.cancel();
Практика
- Напиши декоратор
delay(fn, ms), откладывающий вызов - Реализуй
debounceс опциейimmediate(вызов на переднем фронте) - Создай декоратор
limit(fn, maxCalls)— ограничение числа вызовов - Напиши
withCache(fn, ttl)— кэш с временем жизни - Реализуй композицию декораторов и примени 3 декоратора к одной функции