85. Декораторы (Stage 3)
Декораторы — специальный синтаксис @decorator для модификации классов, методов, свойств и параметров. В 2024 году декораторы достигли Stage 3 в TC39 и поддерживаются через Babel/TypeScript. Понимание декораторов важно для Angular, NestJS и MobX.
ℹ️ Статус: Stage 3 (почти стандарт). Требует компилятора (Babel/TypeScript) или Node.js с флагом.
Декоратор как функция высшего порядка
Заголовок раздела «Декоратор как функция высшего порядка»До официального синтаксиса — декораторы как обычные функции:
// Декоратор метода — обёрткаfunction readonly(target, key, descriptor) { descriptor.writable = false return descriptor}
function log(target, key, descriptor) { const original = descriptor.value descriptor.value = function(...args) { console.log(`Вызов ${key}(${args.join(', ')})`) const result = original.apply(this, args) console.log(`${key} → ${result}`) return result } return descriptor}
// Применяем вручную (без синтаксиса @)class Calculator { add(a, b) { return a + b } multiply(a, b) { return a * b }}
const desc = Object.getOwnPropertyDescriptor(Calculator.prototype, 'add')Object.defineProperty(Calculator.prototype, 'add', log(Calculator.prototype, 'add', desc))
const calc = new Calculator()calc.add(3, 4) // Вызов add(3, 4) → 7Официальный синтаксис (Stage 3, 2024)
Заголовок раздела «Официальный синтаксис (Stage 3, 2024)»// Декоратор класса@sealedclass BankAccount { ... }
// Декоратор методаclass API { @log @retry(3) async fetchUser(id) { ... }}
// Декоратор поляclass User { @required @minLength(2) name = ''}Реализация популярных декораторов
Заголовок раздела «Реализация популярных декораторов»@log — логирование вызовов
Заголовок раздела «@log — логирование вызовов»// Функция-декоратор (Stage 3 API)function log(originalMethod, context) { const methodName = context.name
return function(...args) { console.log(`→ ${methodName}(${args.map(a => JSON.stringify(a)).join(', ')})`) const result = originalMethod.apply(this, args) console.log(`← ${methodName} = ${JSON.stringify(result)}`) return result }}
class MathService { add(a, b) { return a + b } multiply(a, b) { return a * b }}
// Применяем вручную (пока Stage 3 не везде поддерживается)const proto = MathService.prototypefor (const method of ['add', 'multiply']) { const desc = Object.getOwnPropertyDescriptor(proto, method) desc.value = log(desc.value, { name: method }) Object.defineProperty(proto, method, desc)}
const math = new MathService()math.add(2, 3) // → add(2, 3) ← add = 5math.multiply(4, 5) // → multiply(4, 5) ← multiply = 20@memoize — кэширование результата
Заголовок раздела «@memoize — кэширование результата»function memoize(fn) { const cache = new Map() return function(...args) { const key = JSON.stringify(args) if (cache.has(key)) { console.log(`[cache hit] ${fn.name}(${key})`) return cache.get(key) } const result = fn.apply(this, args) cache.set(key, result) return result }}
function fibonacci(n) { if (n <= 1) return n return fibonacci(n - 1) + fibonacci(n - 2)}
// Без мемоизации: 2^n вызовов// С мемоизацией:const fastFib = memoize(function fib(n) { if (n <= 1) return n return fastFib(n - 1) + fastFib(n - 2)})
console.time('fib')console.log(fastFib(40)) // 102334155console.timeEnd('fib') // < 1мс (без кэша — секунды)@debounce — декоратор для методов
Заголовок раздела «@debounce — декоратор для методов»function debounce(delay) { return function(originalMethod, context) { let timer
return function(...args) { clearTimeout(timer) timer = setTimeout(() => originalMethod.apply(this, args), delay) } }}
class SearchComponent { constructor() { // Применяем декоратор вручную this.search = debounce(300)(this.search.bind(this), { name: 'search' }) }
search(query) { console.log(`Поиск: "${query}"`) // fetch(`/api/search?q=${query}`) }}
const search = new SearchComponent()search.search('J') // отменяетсяsearch.search('JS') // отменяетсяsearch.search('JavaScript') // выполнится через 300мс@validate — валидация аргументов
Заголовок раздела «@validate — валидация аргументов»function validate(rules) { return function(originalMethod) { return function(...args) { rules.forEach(({ index, check, message }) => { if (!check(args[index])) { throw new TypeError(`Аргумент ${index}: ${message}`) } }) return originalMethod.apply(this, args) } }}
class UserService { createUser(name, age, email) { return { name, age, email } }}
// Применяем валидациюconst service = new UserService()const originalCreate = service.createUser.bind(service)service.createUser = validate([ { index: 0, check: (n) => typeof n === 'string' && n.length >= 2, message: 'имя мин. 2 символа' }, { index: 1, check: (a) => typeof a === 'number' && a >= 0 && a <= 150, message: 'возраст 0-150' }, { index: 2, check: (e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e), message: 'некорректный email' },])(originalCreate)
// service.createUser('A', -1, 'not-email') // ❌ TypeErrorTypeScript декораторы в реальных фреймворках
Заголовок раздела «TypeScript декораторы в реальных фреймворках»// NestJS — серверный фреймворк@Controller('/users')@UseGuards(AuthGuard)class UsersController { @Get(':id') @UseInterceptors(LogInterceptor) async getUser(@Param('id') id: string) { return this.usersService.findOne(id) }}
// MobX — управление состояниемclass AppStore { @observable count = 0 @observable user = null
@action increment() { this.count++ }
@computed get doubleCount() { return this.count * 2 }}Задания для практики
Заголовок раздела «Задания для практики»- Реализуйте
@once— декоратор, позволяющий вызвать метод только один раз. - Создайте
@retry(n)— декоратор, повторяющий асинхронный вызовnраз при ошибке. - Напишите
@time— логирует время выполнения метода. - Реализуйте
@deprecated(message)— выводит предупреждение при вызове метода.