想象你走进一家手机美容店。老板不拆你的手机芯片,不换主板,只是熟练地贴钢化膜、套保护壳、再挂一个小挂件。手机原本的通话、刷视频功能一点没变,但屏幕更耐摔、机身更好看,你还知道这台手机被精心养护过。TypeScript装饰器做的就是这件事——在不动类和方法内部逻辑的前提下,给代码贴上“保护膜”“功能壳”和“信息标签”。 装饰器是TypeScript中一种用@符号标记的特殊机制。它像一个“代码美容师”,在类定义的那一刻,悄悄地对类、方法、属性、参数做增强或记录。NestJS里的@Controller、TypeORM里的@Entity,底层都是装饰器在起作用。这篇教程会带你从零认识类装饰器、方法装饰器、属性装饰器、参数装饰器,搞懂它们的参数、执行顺序、工厂模式,以及如何配合reflect-metadata读写元数据。学完你就能看懂框架源码里的装饰器魔法,也能自己动手写一个。

岗位关联

学会装饰器,你就能看懂 NestJS、TypeORM 等主流框架的核心机制,并独立封装日志、权限、校验等通用能力,让代码复用和开发效率上一个台阶。

目录
  1. 可视化演示
  2. 装饰器入门:认识手机美容店
  3. 类装饰器:整机包膜服务
  4. 方法装饰器:给摄像头贴镜片
  5. 属性装饰器:屏幕钢化膜
  6. 参数装饰器:充电口防尘塞
  7. 装饰器工厂:定制套餐工坊
  8. 执行顺序:先贴内层再贴外层
  9. reflect-metadata:手机档案标签
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示类/方法/属性/参数四种装饰器的应用、执行顺序与装饰器工厂模式,切换模式查看装饰器核心机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

四种装饰器类型 青色为类装饰器,绿色为方法,橙色为属性,粉色为参数 @decorator target → 修改行为 TypeScript装饰器用@语法给类、方法、属性、参数添加元数据和修改行为

装饰器入门:认识手机美容店

小明的手机美容店开张了。第一位顾客拿来一部手机,要求“屏幕更耐磨、机身更好看”。小明没有拆开后盖改硬件,而是贴了一张钢化膜、套了一个渐变壳。手机还是那部手机,打电话、刷视频的体验却升级了。这个思路放到TypeScript里,就是装饰器:不修改类内部实现,在外部“贴一层”新能力。

装饰器是TypeScript提供的一种特殊声明语法,用@符号放在类、方法、属性或参数前面。它本质上是一个普通函数,只是被“注册”到了目标上。当类定义被求值时,装饰器函数会被自动调用一次。使用前需要在tsconfig.json中开启"experimentalDecorators": true,这也是很多初学者卡住的第一步。

装饰器的典型用途包括日志记录、权限校验、依赖注入、路由注册。比如NestJS里写@Controller('users'),这个@Controller就是一个装饰器,它不改变UserController内部的业务逻辑,只是在类定义时把路由前缀“贴”了上去。

小明给手机贴完膜,顾客满意地走了。装饰器的哲学正是如此——不追求“拆机改造”,而是用一层层“外包”来增强代码,这让代码的原始逻辑保持干净,也让增强能力可以被复用和组合。

decorator=f(target)应用后:  target=f(target)  或原值\displaystyle \text{decorator} = f(\text{target}) \qquad \text{应用后:}\; \text{target} = f(\text{target}) \;\text{或原值}
装饰器是函数,@是语法糖。把目标对象传给装饰器函数,根据返回值决定是否替换目标。
  1. 装饰器是函数每个装饰器本质上是一个接收目标对象的普通函数,就像贴膜师拿到手机本体。
    f(target)f(\text{target})
  2. @语法是糖@符号只是把目标传给装饰器并接收结果的简写,背后还是普通函数调用。
    target=f(target)\text{target} = f(\text{target})
  3. 可替换可保留装饰器返回新对象就替换原目标,返回null则原目标保留不变。
    target=f(target)  或  原target\text{target} = f(\text{target}) \;\text{或}\; \text{原target}

类装饰器:整机包膜服务

今天店里来了一位“整机包膜”的顾客。他要的不是保护某个零件,而是给整个手机套上防摔壳。在TypeScript里,类装饰器就是这个“整机服务”——它作用在整个类上,像给类套了一层外壳。

类装饰器定义在类声明之前,接收唯一一个参数:类的构造函数。它接收的是“类的本体”,也就是那个可以被new的构造函数本身。你可以用它冻结类、扩展原型、添加静态属性,甚至返回一个全新的类来替换原来的类。

当装饰器返回一个新类时,这个新类会完全替换原类,新的对象实例都基于新类创建。通过class extends target的写法,可以给类追加字段或重写方法。这是实现“混入”(Mixin)功能的基础手段。

整机包膜让手机多了一层保护,顾客不必关心壳里是怎么走线的。类装饰器也一样:业务类不用管自己有没有被“包膜”,装饰器在类定义时静默完成一切。

decorator(Constructor)→NewConstructor  或  void\displaystyle \text{decorator}(\text{Constructor}) \rightarrow \text{NewConstructor} \;\text{或}\; \text{void}
类装饰器拿构造函数做文章,返回新类就“换机”,不返回就“原机贴膜”。
  1. 唯一参数是构造函数类装饰器只接收类构造函数一个参数,就像整机包膜只需要手机本体。
    Constructor\text{Constructor}
  2. 可返回新类返回的新类会替换原类,所有后续实例都由新类创建。
    NewConstructor  或  void\text{NewConstructor} \;\text{或}\; \text{void}
  3. 惯用继承扩展用class extends target继承原类再追加能力,是替换类时的常见写法。
    class extends Constructor\text{class extends Constructor}
typescript7 行
function sealed(target: Function) {  Object.freeze(target);  Object.freeze(target.prototype);} @sealedclass User {}

方法装饰器:给摄像头贴镜片

手机美容店里常有一项服务:给摄像头贴蓝宝石镜片。顾客的手机主要功能是拍照,但镜头容易刮花。小明在镜头上加了保护,拍照功能不变,照片却不再模糊。方法装饰器对类里的方法做的正是这件事。

方法装饰器定义在方法声明之前,接收三个参数:第一个是类的原型对象(prototype),第二个是方法名(字符串),第三个是方法的属性描述符(PropertyDescriptor)。描述符里藏着value(函数本身)、writable、enumerable等控制选项,改写descriptor.value就能在方法调用前后插入逻辑。

这段代码展示了方法拦截的核心:先保存原始函数,再替换为一个“包装函数”,包装函数在调用前打印日志、调用后返回结果。apply(this, ...)保证this指向正确。这就是日志装饰器的底层原理,也是AOP(面向切面编程)在TypeScript中的实现方式。

给摄像头贴了镜片,拍照主体没变,照片却更清晰了。方法装饰器也是如此:原方法的逻辑完整保留,新的“镜片”在不打扰业务的前提下为方法增加了日志、校验或缓存能力。

decorator(proto,key,descriptor)→descriptor  或  void\displaystyle \text{decorator}(\text{proto}, \text{key}, \text{descriptor}) \rightarrow \text{descriptor} \;\text{或}\; \text{void}
方法装饰器的核心是拿到属性描述符,替换value实现“原方法 + 额外逻辑”。
  1. 第一参是原型实例方法存在原型上,装饰器拿到的是构造函数的prototype对象。
    proto=Class.prototype\text{proto} = \text{Class.prototype}
  2. 第二参是方法名被装饰方法的名字,是字符串类型,方便后续打印和索引。
    key=String\text{key} = \text{String}
  3. 第三参是描述符属性描述符包含value、writable、enumerable、configurable四个字段。
    descriptor={value,writable,enumerable,configurable}\text{descriptor} = \{\text{value}, \text{writable}, \text{enumerable}, \text{configurable}\}
  4. 改写value实现拦截把descriptor.value换成包装函数,就完成了对原方法的“包膜”。
    descriptor.value=wrapper\text{descriptor.value} = \text{wrapper}
typescript7 行
function log(target: any, key: string, descriptor: PropertyDescriptor) {  const original = descriptor.value;  descriptor.value = function (...args: any[]) {    console.log('[日志] 调用', key, args);    return original.apply(this, args);  };}

属性装饰器:屏幕钢化膜

手机店里最受欢迎的单品是钢化膜。它贴在屏幕表面,不影响触控,却能在摔落时保护屏幕。属性装饰器就像钢化膜,贴在类的属性上,轻轻覆盖,不改变属性本身的读写逻辑。

属性装饰器定义在属性声明之前,接收两个参数:第一个是类的原型对象(静态属性则为构造函数),第二个是属性名。注意,它拿不到第三个descriptor参数,因此在默认的装饰器实现里,属性装饰器无法直接改写属性的value、writable等特性。它的返回值也会被忽略。

既然拿不到描述符,那属性装饰器有什么用?答案是“做标记”。它可以配合reflect-metadata把属性的附加信息(如字段长度、是否必填)记录下来,供框架后续读取。TypeORM的@Entity和@Column、class-validator的@IsEmail,本质都是先靠属性装饰器收集字段信息,再消费这些信息。

钢化膜贴在屏幕上,看不见摸不着改变,但摔手机时保护效果立竿见影。属性装饰器做标记时也是静悄悄的,等到框架需要读取这些标记时,它们才真正“显形”发力。

decorator(proto,key)→void\displaystyle \text{decorator}(\text{proto}, \text{key}) \rightarrow \text{void}
属性装饰器只有两个参数,不能碰描述符,主要职责是“留记号”。
  1. 只有两个参数参数一是原型或构造函数,参数二是属性名字符串。
    (proto,key)(\text{proto}, \text{key})
  2. 拿不到描述符属性装饰器不像方法装饰器那样能拿到descriptor,因此不能改读写特性。
    ∄ descriptor\nexists \text{ descriptor}
  3. 返回void返回值被忽略,所以它只能做“贴标签”这类副作用操作。
    void\text{void}
typescript3 行
function format(target: any, key: string) {  console.log('属性装饰器:', key);}

参数装饰器:充电口防尘塞

手机美容店内还有一件不起眼的小配件——充电口防尘塞。它塞在手机的充电口里,不充电时防止灰尘进入,需要充电时拔掉即可。参数装饰器的作用对象,正是方法的“插孔”:参数位置。

参数装饰器定义在方法参数之前,接收三个参数:第一个是类的原型对象,第二个是方法名(如果是构造函数参数则忽略该参数值),第三个是参数在参数列表中的索引数字。第一个参数索引为0,第二个为1,依此类推。

参数装饰器无法获取参数的值,也无法修改参数内容。它只能在参数上“盖章”,记录这个位置有特殊语义。最常见的使用是配合reflect-metadata记录“参数的类型”或“参数是否必填”,再由方法装饰器读取这些元数据去执行真正的校验逻辑。NestJS的@Param('id')、@Body()就是参数装饰器的经典应用。

防尘塞不参与充电过程,却让充电口保持干净。参数装饰器不参与方法实际运算,却把“这位置有讲究”的信息留给框架,让依赖注入和数据校验变得自动化。

decorator(proto,key,paramIndex)→void\displaystyle \text{decorator}(\text{proto}, \text{key}, \text{paramIndex}) \rightarrow \text{void}
参数装饰器记下“第几个参数有讲究”,但真正校验要在方法装饰器里做。
  1. 前两参与方法装饰器一致第一参是原型对象,第二参是所在方法的名字。
    (proto,key)(\text{proto}, \text{key})
  2. 第三参是索引参数索引从0开始,第0个参数对应paramIndex=0。
    paramIndex∈{0,1,2,… }\text{paramIndex} \in \{0, 1, 2, \dots\}
  3. 不能改参数值装饰器拿不到实参,所以无法拦截或修改传入的值。
    ∄ paramValue\nexists \text{ paramValue}
typescript3 行
function required(target: any, key: string, index: number) {  console.log('第', index, '个参数被标记');}

装饰器工厂:定制套餐工坊

顾客多了,小明发现“统一贴膜”满足不了所有人:有人要磨砂膜,有人要防蓝光膜,有人要带图案的。于是店里推出“定制套餐”——顾客选好款式材质,工坊按配方生产对应的膜。装饰器工厂就是这套“定制工坊”。

装饰器工厂是一个返回装饰器函数的函数。它接收配置参数,在参数里定制装饰器的行为,然后返回真正被@使用的装饰器函数。调用工厂时需要在@后加括号传参,比如@log('用户模块')。

这在手写框架时特别有用:同样的“打日志”装饰器,通过工厂传入不同的前缀、级别、输出目标,就能复用在各个业务模块。NestJS里的@Controller('users')、@Get(':id')、@SetMetadata('role', 'admin'),全是装饰器工厂——先传路由或配置,再返回真正干活的装饰器。

定制套餐让每个顾客拿到自己心仪的膜。装饰器工厂让每个模块拥有专属配置的装饰器——同样的Log装饰器,给订单模块加“订单”前缀,给用户模块加“用户”前缀,代码复用率高且互不干扰。

factory(config)=decorator\displaystyle \text{factory}(\text{config}) = \text{decorator}
装饰器工厂就是“带参数的装饰器生产器”:传配置,返回装饰器函数。
  1. 工厂是外层函数工厂本身不是装饰器,它是生产装饰器的函数,输入配置参数。
    factory:config→decorator\text{factory}: \text{config} \to \text{decorator}
  2. 返回的才是装饰器调用工厂后得到的函数才是真正挂在@后的装饰器函数。
    decorator=factory(config)\text{decorator} = \text{factory}(\text{config})
  3. 用法是函数调用@factory(opt)先执行工厂求得装饰器,再把装饰器应用到目标上。
    @(factory(opt))(target)\text{@}\bigl(\text{factory}(\text{opt})\bigr)(\text{target})
typescript9 行
function log(prefix: string) {  return function (target: any, key: string, descriptor: PropertyDescriptor) {    const fn = descriptor.value;    descriptor.value = function (...args: any[]) {      console.log(prefix + key, args);      return fn.apply(this, args);    };  };}

执行顺序:先贴内层再贴外层

一位顾客同时买了钢化膜、防摔壳和镜头贴。小明操作的顺序是:先贴镜头贴,再贴屏幕膜,最后套外壳。这个顺序很讲究——先做的“内层”,后做的“外层”,外层的壳包裹着内层的膜。装饰器的执行顺序同样是这个“洋葱模型”。

当多个装饰器同时挂在一个目标上时,规则有两条:第一,装饰器表达式(工厂调用)按从上到下的顺序求值;第二,求值得到的装饰器函数按从下到上的顺序调用。也就是说,@dec1 @dec2中dec2先执行,dec1后执行,dec1包裹在dec2外面。

不同类型装饰器应用在同一类上时,顺序也有约定:实例成员的参数装饰器先执行,然后是同成员的属性/方法装饰器(从右到左对每个成员处理),之后是构造器参数装饰器,最后才是类装饰器。属性/参数装饰器最“内层”,类装饰器最“外层”。

小明把最贴身的膜先贴好,再把壳套在外面,保护层层层叠加。装饰器的洋葱模型也一样:内层装饰器先感知原始行为,外层装饰器在它外围做进一步处理,顺序反了,效果就完全不同。

dec1(dec2(target))表达式从上到下求值,函数从下到上调用\displaystyle \text{dec1}(\text{dec2}(\text{target})) \qquad \text{表达式从上到下求值,函数从下到上调用}
装饰器像贴膜:先贴内层再贴外层,外层包着内层,执行时机相反。
  1. 表达式从上到下求值@dec1 @dec2中dec1的工厂/引用先求值,dec2后求值。
    eval order:dec1→dec2\text{eval order}: \text{dec1} \to \text{dec2}
  2. 调用从下到上求值得到的函数按从右到左(从下到上)的顺序被调用,dec2先执行。
    call order:dec2→dec1\text{call order}: \text{dec2} \to \text{dec1}
  3. 外层包裹内层后执行的dec1在外层,先执行的dec2在内层,形成洋葱。
    dec1(dec2(target))\text{dec1}\bigl(\text{dec2}(\text{target})\bigr)
typescript3 行
@dec1@dec2class User {} // dec2先执行,dec1后执行,最终效果 dec1(dec2(User))

reflect-metadata:手机档案标签

手机美容店生意好了,小明给每台手机贴一张小标签,写上“贴膜日期”“保修期”“客户偏好”。标签不改变手机功能,却让店里能快速查到每台手机的服务历史。reflect-metadata就是给代码对象贴“档案标签”的工具包。

reflect-metadata是一个npm库,为JavaScript/TypeScript提供了元数据读写能力。核心API包括Reflect.defineMetadata(key, value, target)写入、Reflect.getMetadata(key, target)读取、Reflect.hasMetadata判断。装饰器是“贴纸”,元数据是“贴纸上的字”——装饰器执行时通过defineMetadata把信息写到类或方法上,其他代码再通过getMetadata读出来。

这个库是NestJS、TypeORM等框架的基石。TypeORM的@Entity('user')会把表名写入元数据;NestJS的@Controller('users')把路由前缀写入元数据;依赖注入容器则通过元数据知道“这个构造参数要注入哪个服务”。装饰器负责收集信息,reflect-metadata负责存储和读取信息,二者配合完成元编程。

标签贴在手机上,手机自己不知道,但店员一查就知道该给这手机用什么保养方案。reflect-metadata把装饰器标记的信息变成可查询的数据,让框架能在运行时“读懂”代码结构,这就是NestJS能自动配路由、TypeORM能自动建表的原因。

Reflect.getMetadata(key,target)→value\displaystyle \text{Reflect.getMetadata}(\text{key}, \text{target}) \rightarrow \text{value}
reflect-metadata像标签系统:装饰器写标签,框架读标签,类本身无感知。
  1. key是元数据键名键通常是字符串或Symbol,用来区分不同类型的元数据。
    key=string∣Symbol\text{key} = \text{string} \mid \text{Symbol}
  2. target是承载对象元数据挂在类构造函数、原型或方法对应的对象上。
    target=Object\text{target} = \text{Object}
  3. 返回元数据值getMetadata读到键对应的值,找不到时返回undefined。
    value=any∣undefined\text{value} = \text{any} \mid \text{undefined}
typescript4 行
import 'reflect-metadata'; Reflect.defineMetadata('role', 'admin', User);const role = Reflect.getMetadata('role', User); // 'admin'

AI 应用场景

NestJS控制器路由注册

阶段:Web框架配置与路由注册

解决的问题:传统Express写路由需要手动app.get('/users/:id', handler),路由分散且容易漏写。NestJS用类装饰器@Controller和方法装饰器@Get,在类定义时自动完成路由映射,路由前缀与方法一一对应,代码集中且声明式。

具体案例:开发用户管理接口时,只需写@Controller('users')和@Get(':id'),NestJS启动时读取装饰器写入的元数据,自动把GET /users/:id绑定到该方法上。新增接口只需加一个方法,不必再找路由文件。

TypeORM实体映射

阶段:数据持久层建模

解决的问题:传统ORM需要手写大量XML映射文件,字段多时极易出错。TypeORM用@Entity、@Column属性装饰器在实体类上直接声明表名、列名、类型,一处定义处处复用。

具体案例:定义一个User实体类,用@Entity('user')标在类上、@Column({name: 'user_name', length: 50})标在属性上,启动时框架读取元数据自动建表或校验表结构,字段变更只需改一处代码。

依赖注入容器

阶段:IoC容器与模块装配

解决的问题:大型项目中手动new依赖、传构造函数参数容易产生耦合。NestJS利用参数装饰器@Inject和构造器参数元数据,让容器自动找到并注入对应服务。

具体案例:在Service构造器参数前写@Inject('REDIS'),容器读到参数装饰器记录的“这个位置需要REDIS”,启动时将已注册的Redis客户端实例自动注入。换实现时只需改注册处的绑定,Service代码不用动。

Python 演示

python57 行
import functoolsimport time  def log_execution(func):    """Print method call log like a TS method decorator."""    @functools.wraps(func)    def wrapper(*args, **kwargs):        print("[log] call {} args={} kwargs={}".format(func.__name__, args[1:], kwargs))        start = time.time()        result = func(*args, **kwargs)        cost = (time.time() - start) * 1000        print("[log] {} returned {} (cost {:.2f}ms)".format(func.__name__, result, cost))        return result    return wrapper  def validate_positive(func):    """Validate amount is positive like a TS param decorator."""    @functools.wraps(func)    def wrapper(self, amount):        if amount <= 0:            raise ValueError("amount must be positive")        return func(self, amount)    return wrapper  class BankAccount:    """A bank account class with decorators as phone stickers."""     def __init__(self, owner, balance=0):        self.owner = owner        self.balance = balance     @log_execution    def deposit(self, amount):        self.balance += amount        return self.balance     @log_execution    @validate_positive    def withdraw(self, amount):        if amount > self.balance:            raise ValueError("insufficient balance")        self.balance -= amount        return self.balance  def main():    account = BankAccount("xiaoming", 1000)    account.deposit(500)    account.withdraw(200)    print("final balance:", account.balance)  if __name__ == "__main__":    main()
目的:用Python装饰器模拟TypeScript装饰器的核心思想——不改动方法内部逻辑,通过@语法在外层叠加日志和参数校验能力。
  1. def log_execution(func):装饰器本质是接收函数的函数,对应TS装饰器的“贴膜师”角色
  2. def wrapper(*args, **kwargs):返回的包装函数,代替原方法执行,对应TS方法装饰器改写descriptor.value
  3. result = func(*args, **kwargs)调用原始方法,保留原逻辑,像贴膜不影响手机功能
  4. @log_executionPython语法糖,等价于deposit = log_execution(deposit),和TS的@写法同源
  5. if amount <= 0:参数校验逻辑,体现TS中“参数装饰器做标记、真正校验靠方法装饰器”的思想

常见误区

误区我一开始以为写了@装饰器,目标的方法逻辑或属性值一定会被改变,比如方法被换掉、属性被重写。如果装饰器没有改变任何东西,就觉得它没用。

正解装饰器完全可以只做“标记”。像给手机贴一张小标签,手机该什么样还是什么样。配合reflect-metadata把信息写进元数据,类定义完成后另一个函数再读取这些信息去干活,这是框架中最常见的用法。装饰器改行为只是其中一个分支,不是全部。

为何常见:教程里最爱展示日志装饰器、拦截装饰器这类“改了行为”的例子,看得多了就以为装饰器非改不可。

误区以为`@outer @inner`就是先执行outer,再执行inner,像剥洋葱一样从外层往里剥。

正解TypeScript的规则是:表达式从上到下求值,函数从下到上调用。@outer @inner中inner先执行,outer后执行。最终outer拿到的是inner处理后的结果,所以看起来是outer包在inner外面。类比贴膜:先贴内层膜,再贴外层壳,壳最后套上却是最外面那层。

为何常见:看到“洋葱模型”这个词就想起“先入后出”,但装饰器的求值和调用分两个阶段,只记住一个阶段就容易弄反。

误区方法装饰器能拿到descriptor改写value,我猜属性装饰器应该也能拿到descriptor,去改属性的可写性、默认值之类的。

正解属性装饰器只有两个参数——原型对象和属性名,没有descriptor,返回值也被忽略(TypeScript 5.0之前的默认装饰器实现里)。它根本摸不到属性描述符,所以不能拦截属性赋值。如果想控制属性的读写行为,应该用访问器装饰器(装饰getter/setter)或方法装饰器。

为何常见:方法装饰器的三个参数太深入人心,初学者容易把方法和属性混为一谈,忽略了属性装饰器是“轻量版”。

误区我以为装饰器会像方法本身一样,每次`new`一个对象、每次调用一次方法,装饰器都重新跑一遍。

正解装饰器只在类被定义时执行且只执行一次。它在类定义求值阶段完成对被装饰目标的包装或标记,之后每次调用方法执行的是“被包装后的函数”,而不是装饰器本身。装饰器相当于贴膜的那一次动作,贴完就结束了,之后每次用手机都是贴着膜用。

为何常见:方法装饰器包装的wrapper在方法调用时运行,让人误以为装饰器本身也在每次调用时运行。

误区我最初在参数装饰器里写if判断,想拦截非法参数,结果发现根本不生效,参数还是原样传进去了。

正解参数装饰器拿不到参数值,也没有descriptor,无法拦截或修改参数。它只能在元数据里记下“这个位置的参数有特殊语义”。真正的参数校验必须通过方法装饰器包装方法体,在wrapper里读取参数并校验,这也是NestJS参数校验管道底层的做法。

为何常见:校验是刚需,直觉上觉得“装饰在参数上就应该能拦住参数”,但装饰器本质上只是注册时的一次函数调用而已。

面试题

Q1. TypeScript中装饰器的本质是什么?

单选题 · easy

  • A.一种特殊的类型注解
  • B.一个普通的函数← 正确答案
  • C.一种内置的数据结构
  • D.一个命令行工具

答案:B。装饰器本质是函数,通过@语法挂到类、方法、属性、参数上,在类定义时被调用。类型注解只是给编译器看的标签,装饰器是真正会执行代码的函数。C和D与装饰器的函数本质无关。

Q2. 在项目中使用装饰器,tsconfig.json需要开启哪个选项?

单选题 · easy

  • A."strict": true
  • B."noImplicitAny": true
  • C."experimentalDecorators": true← 正确答案
  • D."esModuleInterop": true

答案:C。装饰器属于实验性特性,必须在tsconfig.json中设置"experimentalDecorators": true才能启用。strict和noImplicitAny是类型检查选项,esModuleInterop是模块互操作选项,与装饰器是否启用无关。

Q3. 方法装饰器接收的第一个参数是什么?

单选题 · mid

  • A.类的构造函数
  • B.方法的属性描述符
  • C.类的原型对象← 正确答案
  • D.方法的名字字符串

答案:C。实例方法挂在原型上,所以方法装饰器的第一个参数是类的原型对象(静态方法则是构造函数)。构造函数是类装饰器的参数;描述符和名字分别是第二、三个参数。

Q4. 关于类装饰器的返回值,下列说法正确的是?

单选题 · mid

  • A.可以返回一个新类来替换原类← 正确答案
  • B.必须返回undefined否则报错
  • C.只能返回void不能返回类
  • D.返回值会被编译器忽略

答案:A。类装饰器的返回值会被作为替换后的构造函数。返回新类,原来的类就被替换;不返回则保留原类。返回void和返回新类都是合法写法,不存在“必须返回undefined”的限制。

Q5. 以下哪种装饰器可以拿到属性描述符?

单选题 · mid

  • A.属性装饰器
  • B.参数装饰器
  • C.方法装饰器← 正确答案
  • D.类装饰器

答案:C。方法装饰器接收(target, key, descriptor)三个参数,descriptor是属性描述符,可以改写。属性装饰器和参数装饰器都拿不到descriptor,类装饰器只接收构造函数。这是四种装饰器最核心的差异之一。

Q6. 代码@factory(config)中,这种写法依赖什么机制?

单选题 · mid

  • A.装饰器继承
  • B.装饰器工厂← 正确答案
  • C.装饰器重载
  • D.装饰器混入

答案:B。@factory(config)是装饰器工厂的调用形式:先调用工厂函数,传入配置参数,得到真正的装饰器函数,再把这个装饰器应用到目标上。工厂的意义是让装饰器可以通过参数定制行为,而不是写死逻辑。

Q7. 对于代码@A @B class X {},其中A和B是装饰器函数,下列说法正确的是?

单选题 · hard

  • A.先调A再调B,表达式从下到上求值
  • B.先调B再调A,表达式从下到上求值
  • C.先调B再调A,表达式从上到下求值← 正确答案
  • D.先调A再调B,表达式从上到下求值

答案:C。TypeScript规定多个装饰器应用时,表达式从上到下求值(A的表达式先求值),函数调用从下到上(B先被调用,A后调用),最终效果是A包裹B,即A(B(X))。这就像先贴内层膜B再贴外层壳A。

Q8. 以下哪些位置可以合法使用装饰器?

多选题 · mid

  • A.类声明上← 正确答案
  • B.方法定义上← 正确答案
  • C.函数体内的局部变量上
  • D.参数位置上← 正确答案

答案:ABD。装饰器可以用于类声明、方法、属性、参数以及构造器参数,用途由目标类型决定。函数体内部的局部变量不能使用装饰器,因为装饰器只能附着在类结构成员上,不能脱离类上下文。

Q9. 方法装饰器接收的参数包括哪些?

多选题 · hard

  • A.类的原型对象← 正确答案
  • B.方法的名字← 正确答案
  • C.方法的属性描述符← 正确答案
  • D.方法的参数个数

答案:ABC。方法装饰器的三个参数依次是原型对象(静态方法为构造函数)、方法名字符串、属性描述符。参数个数不在其中——参数个数信息需要通过参数装饰器的索引或Function.length获取,不直接传给方法装饰器。

Q10. reflect-metadata库的主要用途是什么?

单选题 · easy

  • A.给装饰器提供运行时类型检查
  • B.在对象上存储和读取元数据← 正确答案
  • C.替代装饰器实现AOP编程
  • D.编译TypeScript到JavaScript

答案:B。reflect-metadata提供Reflect.defineMetadata、Reflect.getMetadata等API,用于在对象上读写键值对元数据。装饰器负责“贴标签”,reflect-metadata负责“存标签、读标签”,二者配合支撑NestJS、TypeORM等框架的元编程能力。