Are you an LLM? You can read better optimized documentation at /tw/fe/typescript/decorator-metadata-guide.md for this page in Markdown format
TypeScript 裝飾器與元數據編程實戰 2026 | Decorator 完全指南

裝飾器(Decorator)是 TypeScript 中最強大也最令人困惑的特性之一。它讓我們能在不修改原始代碼的情況下,動態地給類、方法、屬性添加功能。本文將系統講解裝飾器的原理、5 類裝飾器用法、元數據編程、依賴注入及 AOP 實戰。
一、裝飾器基礎
1.1 什麼是裝飾器
裝飾器本質上是一個高階函數,接收目標對象,返回增強後的對象:
typescript
// 裝飾器的基本模式
function decorator(target) {
// 修改或增強 target
return target
}
// 使用裝飾器
@decorator
class MyClass {}1.2 啟用裝飾器
json
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true, // 啟用裝飾器
"emitDecoratorMetadata": true // 啟用元數據(需 reflect-metadata)
}
}安裝元數據支持:
bash
npm install reflect-metadata1.3 裝飾器執行順序
裝飾器的執行遵循 由內到外、由下到上 的原則:
typescript
function f1() {
console.log('f1: 求值')
return function (target: any) {
console.log('f1: 執行')
}
}
function f2() {
console.log('f2: 求值')
return function (target: any) {
console.log('f2: 執行')
}
}
@f1()
@f2()
class MyClass {}
// 輸出:
// f1: 求值(先求值,從上到下)
// f2: 求值
// f2: 執行(後執行,從下到上)
// f1: 執行二、5 類裝飾器詳解
2.1 類裝飾器(Class Decorator)
類裝飾器接收類的構造函數作為參數:
typescript
function LogClass(target: Function) {
console.log(`類 ${target.name} 被裝飾了`)
// 保存原始構造函數
const original = target
// 返回新的構造函數
return class extends original {
constructor(...args: any[]) {
super(...args)
console.log(`實例化 ${original.name},參數:${args}`)
}
}
}
@LogClass
class User {
constructor(public name: string) {}
}
const user = new User('Alice')
// 類 User 被裝飾了
// 實例化 User,參數:Alice實用示例:單例模式裝飾器
typescript
function Singleton<T extends new (...args: any[]) => any>(constructor: T) {
let instance: InstanceType<T>
return class extends constructor {
constructor(...args: any[]) {
if (instance) return instance
super(...args)
instance = this as InstanceType<T>
}
}
}
@Singleton
class Database {
constructor(public url: string) {
console.log('連接數據庫...')
}
}
const db1 = new Database('mysql://localhost')
const db2 = new Database('mysql://remote')
// 連接數據庫...(只輸出一次)
console.log(db1 === db2) // true2.2 方法裝飾器(Method Decorator)
方法裝飾器接收 3 個參數:
target:對於靜態方法是類的構造函數,對於實例方法是原型對象propertyKey:方法名descriptor:屬性描述符(PropertyDescriptor)
typescript
function LogMethod(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`調用 ${propertyKey},參數:${JSON.stringify(args)}`)
const result = originalMethod.apply(this, args)
console.log(`返回值:${JSON.stringify(result)}`)
return result
}
return descriptor
}
class Calculator {
@LogMethod
add(a: number, b: number): number {
return a + b
}
}
const calc = new Calculator()
calc.add(2, 3)
// 調用 add,參數:[2,3]
// 返回值:5實用示例:防抖裝飾器
typescript
function Debounce(delay: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value
let timer: NodeJS.Timeout
descriptor.value = function (...args: any[]) {
clearTimeout(timer)
timer = setTimeout(() => originalMethod.apply(this, args), delay)
}
return descriptor
}
}
class SearchComponent {
@Debounce(300)
onSearch(keyword: string) {
console.log(`搜索:${keyword}`)
}
}2.3 屬性裝飾器(Property Decorator)
屬性裝飾器接收 2 個參數:
target:原型對象或構造函數propertyKey:屬性名
typescript
function Required(target: any, propertyKey: string) {
// 在原型上存儲元數據
const requiredProps = Reflect.getMetadata('required', target) || []
requiredProps.push(propertyKey)
Reflect.defineMetadata('required', requiredProps, target)
}
function validate(instance: any) {
const target = Object.getPrototypeOf(instance)
const requiredProps: string[] = Reflect.getMetadata('required', target) || []
for (const prop of requiredProps) {
if (instance[prop] === undefined || instance[prop] === null) {
throw new Error(`屬性 ${prop} 是必填的`)
}
}
}
class UserForm {
@Required
name!: string
@Required
email!: string
age?: number
}
const form = new UserForm()
form.name = 'Alice'
// validate(form) // Error: 屬性 email 是必填的2.4 訪問器裝飾器(Accessor Decorator)
訪問器裝飾器與方法裝飾器參數相同,但只能裝飾 getter 或 setter 中的一個:
typescript
function Enumerable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value
return descriptor
}
}
class Person {
private _name: string = ''
@Enumerable(false)
get name(): string {
return this._name
}
set name(value: string) {
this._name = value
}
}2.5 參數裝飾器(Parameter Decorator)
參數裝飾器接收 3 個參數:
target:原型對象或構造函數propertyKey:方法名(構造函數參數為undefined)parameterIndex:參數索引
typescript
function Inject(token: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
// 將依賴注入信息存入元數據
const existingTokens = Reflect.getMetadata('inject:tokens', target, propertyKey || 'constructor') || []
existingTokens[parameterIndex] = token
Reflect.defineMetadata('inject:tokens', existingTokens, target, propertyKey || 'constructor')
}
}
class UserService {
constructor(@Inject('Database') private db: any) {}
getUser(@Inject('Logger') logger: any, id: number) {
logger.log(`獲取用戶 ${id}`)
}
}三、reflect-metadata 與元數據編程
3.1 核心概念
reflect-metadata 允許在對象上添加和讀取元數據,這是依賴注入和 AOP 的基石:
typescript
import 'reflect-metadata'
// 定義元數據
Reflect.defineMetadata('role', 'admin', MyClass)
Reflect.defineMetadata('version', '1.0', MyClass.prototype, 'method')
// 讀取元數據
const role = Reflect.getMetadata('role', MyClass) // 'admin'
const version = Reflect.getMetadata('version', MyClass.prototype, 'method') // '1.0'
// 檢查元數據是否存在
Reflect.hasMetadata('role', MyClass) // true
// 刪除元數據
Reflect.deleteMetadata('role', MyClass)3.2 內置元數據鍵
typescript
// 類型元數據(需 emitDecoratorMetadata: true)
class Example {
greet(@Reflect.metadata('design:type', String) name: string) {}
method(
@Reflect.metadata('design:paramtypes', [String, Number])
input: string
) {}
}
// 自動記錄的類型信息
// design:type —— 屬性的類型
// design:paramtypes —— 方法參數的類型數組
// design:returntype —— 方法的返回類型3.3 裝飾器工廠 + 元數據
typescript
function Route(method: string, path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 將路由信息存入元數據
Reflect.defineMetadata('route:method', method, target, propertyKey)
Reflect.defineMetadata('route:path', path, target, propertyKey)
}
}
function Middleware(middleware: Function) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const middlewares = Reflect.getMetadata('route:middlewares', target, propertyKey) || []
middlewares.push(middleware)
Reflect.defineMetadata('route:middlewares', middlewares, target, propertyKey)
}
}
class UserController {
@Route('GET', '/users/:id')
@Middleware(authMiddleware)
getUser() {}
@Route('POST', '/users')
@Middleware(authMiddleware)
@Middleware(validateUser)
createUser() {}
}四、實戰:手寫輕量級依賴注入容器
4.1 容器實現
typescript
import 'reflect-metadata'
type Constructor<T = any> = new (...args: any[]) => T
// 依賴注入容器
class DIContainer {
private bindings = new Map<string, any>()
private instances = new Map<string, any>()
// 註冊綁定
bind<T>(token: string, constructor: Constructor<T>) {
this.bindings.set(token, constructor)
return this
}
// 註冊單例
singleton<T>(token: string, constructor: Constructor<T>) {
this.bindings.set(token, { constructor, singleton: true })
return this
}
// 註冊常量
value<T>(token: string, value: T) {
this.instances.set(token, value)
return this
}
// 解析依賴
resolve<T>(token: string): T {
// 先檢查已實例化的單例
if (this.instances.has(token)) {
return this.instances.get(token)
}
const binding = this.bindings.get(token)
if (!binding) {
throw new Error(`未找到綁定:${token}`)
}
const Constructor = binding.singleton ? binding.constructor : binding
const isSingleton = binding.singleton
// 讀取構造函數參數的類型信息
const paramTypes: Constructor[] = Reflect.getMetadata('design:paramtypes', Constructor) || []
// 遞歸解析依賴
const dependencies = paramTypes.map((paramType, index) => {
const injectToken = Reflect.getMetadata('inject:token', Constructor, `constructor:${index}`)
return this.resolve(injectToken || paramType.name)
})
const instance = new Constructor(...dependencies)
if (isSingleton) {
this.instances.set(token, instance)
}
return instance
}
}
// Injectable 裝飾器
function Injectable() {
return function (target: any) {
// 標記該類可被注入
Reflect.defineMetadata('injectable', true, target)
}
}
// Inject 裝飾器
function Inject(token: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
Reflect.defineMetadata(`inject:token`, token, target, `constructor:${parameterIndex}`)
}
}4.2 使用容器
typescript
@Injectable()
class Logger {
log(msg: string) {
console.log(`[LOG] ${new Date().toISOString()} - ${msg}`)
}
}
@Injectable()
class Database {
constructor(private logger: Logger) {}
query(sql: string) {
this.logger.log(`執行查詢:${sql}`)
return [{ id: 1, name: 'Alice' }]
}
}
@Injectable()
class UserService {
constructor(
@Inject('Database') private db: Database,
@Inject('Logger') private logger: Logger
) {}
getUser(id: number) {
this.logger.log(`獲取用戶 ${id}`)
return this.db.query(`SELECT * FROM users WHERE id = ${id}`)
}
}
// 配置容器
const container = new DIContainer()
container.bind('Logger', Logger)
container.singleton('Database', Database)
container.bind('UserService', UserService)
// 解析使用
const userService = container.resolve<UserService>('UserService')
userService.getUser(1)五、AOP(面向切面編程)實戰
5.1 AOP 核心概念
- 切面(Aspect):橫切關注點的模塊化(如日誌、權限、緩存)
- 切點(Pointcut):定義在哪些方法上應用切面
- 通知(Advice):切面的具體邏輯(前置、後置、環繞)
- 織入(Weaving):將切面應用到目標對象的過程
5.2 用裝飾器實現 AOP
typescript
// 前置通知
function Before(advice: (...args: any[]) => void) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
advice(...args)
return original.apply(this, args)
}
}
}
// 後置通知
function After(advice: (result: any) => void) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
const result = original.apply(this, args)
advice(result)
return result
}
}
}
// 環繞通知
function Around(advice: (proceed: () => any, ...args: any[]) => any) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
const proceed = () => original.apply(this, args)
return advice(proceed, ...args)
}
}
}
// 異常通知
function Catch(errorHandler: (error: Error) => void) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
try {
return original.apply(this, args)
} catch (error) {
errorHandler(error as Error)
return null
}
}
}
}5.3 實戰:緩存切面
typescript
function Cache(ttl: number = 60000) {
const cache = new Map<string, { value: any; expire: number }>()
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
const key = `${propertyKey}:${JSON.stringify(args)}`
const cached = cache.get(key)
if (cached && Date.now() < cached.expire) {
console.log(`緩存命中:${key}`)
return cached.value
}
const result = original.apply(this, args)
cache.set(key, { value: result, expire: Date.now() + ttl })
console.log(`緩存寫入:${key}`)
return result
}
}
}
class APIService {
@Cache(30000)
async getUser(id: number) {
console.log(`請求 API:GET /users/${id}`)
return { id, name: 'Alice' }
}
}六、TC39 Stage 3 裝飾器新語法
2026 年,TC39 裝飾器提案已進入 Stage 3,語法與舊版有所不同:
6.1 新語法差異
typescript
// 舊語法(TypeScript 實驗性裝飾器)
function logged(target: any, key: string, descriptor: PropertyDescriptor) {
// ...
}
// 新語法(TC39 Stage 3)
function logged(value: Function, context: ClassMethodDecoratorContext) {
return function (...args: any[]) {
console.log(`調用 ${String(context.name)}`)
return value.apply(this, args)
}
}
class MyClass {
@logged
greet(name: string) {
return `Hello, ${name}!`
}
}6.2 Context 對象
typescript
// 類裝飾器上下文
interface ClassDecoratorContext {
kind: 'class'
name: string
addInitializer(initializer: () => void): void
}
// 方法裝飾器上下文
interface ClassMethodDecoratorContext {
kind: 'method'
name: string
static: boolean
private: boolean
access: { has(object: any): boolean; get(object: any): unknown }
addInitializer(initializer: () => void): void
}
// 屬性裝飾器上下文
interface ClassFieldDecoratorContext {
kind: 'field'
name: string
static: boolean
private: boolean
access: { has(object: any): boolean; get(object: any): unknown }
addInitializer(initializer: () => void): void
}6.3 新語法示例
typescript
// 使用新語法的裝飾器
function bound(value: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name)
if (context.kind !== 'method') {
throw new Error(`@bound 只能用於方法,不能用於 ${context.kind}`)
}
return function (this: any, ...args: any[]) {
return value.apply(this, args)
}
}
function tracked(_: undefined, context: ClassFieldDecoratorContext) {
const fieldName = String(context.name)
return function (this: any, initialValue: unknown) {
console.log(`${fieldName} 初始化為 ${initialValue}`)
return initialValue
}
}
class Button {
@tracked
clicks = 0
@bound
onClick() {
this.clicks++
console.log(`點擊次數:${this.clicks}`)
}
}
const btn = new Button()
const handler = btn.onClick // this 不會丟失
handler() // 點擊次數:1七、NestJS 中的裝飾器應用
NestJS 是裝飾器與依賴注入的集大成者:
typescript
import { Controller, Get, Post, Body, Param, UseGuards, SetMetadata } from '@nestjs/common'
// 自定義裝飾器
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)
@Controller('users')
export class UserController {
constructor(private userService: UserService) {}
@Get()
@UseGuards(AuthGuard)
@Roles('admin')
findAll() {
return this.userService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(+id)
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto)
}
}八、最佳實踐與注意事項
- 保持裝飾器純粹:裝飾器應該只添加元數據或修改行為,不要包含複雜業務邏輯
- 組合優於繼承:用裝飾器組合功能比繼承更靈活
- 注意執行順序:多個裝飾器的執行順序很重要,文檔中要明確說明
- 類型安全:儘量給裝飾器添加泛型約束,避免
any - 性能考量:裝飾器在類定義時執行一次,運行時開銷來自 descriptor 代理
- 兼容性:TC39 新舊語法不兼容,新項目優先使用 Stage 3 語法
- 調試困難:裝飾器會隱藏調用棧,關鍵路徑加日誌
九、總結
- ✅ 掌握了 5 類裝飾器的用法與參數(類、方法、屬性、訪問器、參數)
- ✅ 理解 reflect-metadata 元數據編程原理
- ✅ 實現了輕量級依賴注入容器
- ✅ 用裝飾器實現了 AOP(日誌、緩存、權限等切面)
- ✅ 瞭解 TC39 Stage 3 裝飾器新語法
- ✅ 在 NestJS 中實戰應用裝飾器
裝飾器是 TypeScript 高級編程的核心工具,熟練掌握後可以大幅提升代碼的可維護性和可擴展性。
相關閱讀: