Перейти к содержимому

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
// Декоратор класса
@sealed
class BankAccount { ... }
// Декоратор метода
class API {
@log
@retry(3)
async fetchUser(id) { ... }
}
// Декоратор поля
class User {
@required
@minLength(2)
name = ''
}
// Функция-декоратор (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.prototype
for (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 = 5
math.multiply(4, 5) // → multiply(4, 5) ← multiply = 20
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)) // 102334155
console.timeEnd('fib') // < 1мс (без кэша — секунды)
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мс
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('Алексей', 37, '[email protected]') // ✅
// service.createUser('A', -1, 'not-email') // ❌ TypeError
// 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 }
}
  1. Реализуйте @once — декоратор, позволяющий вызвать метод только один раз.
  2. Создайте @retry(n) — декоратор, повторяющий асинхронный вызов n раз при ошибке.
  3. Напишите @time — логирует время выполнения метода.
  4. Реализуйте @deprecated(message) — выводит предупреждение при вызове метода.