想象你经营着一家快递分拣中心。每天清晨,成千上万的包裹涌入分拣台,每个包裹都需要贴上不同的标签:「易碎」「加急」「冷链」「偏远」。没有这些标签,分拣员根本不知道该怎么处理它们——是轻拿轻放,还是立刻装车,抑或送进冷库。有了标签系统,分拣工作从「凭经验猜测」变成了「看标签行动」,效率翻了几倍。 在 TypeScript 的世界里,装饰器(Decorator)就是那台「标签机」。它让你在不修改类、方法、属性内部实现的前提下,给它们贴上额外的标签——「这个类是可注入的服务」「这个方法需要记录日志」「这个属性是某个数据库字段」。而元数据反射(Reflect Metadata)则是那本「标签登记簿」,让程序在运行时随时查阅某个对象曾被贴过哪些标签。像 NestJS 这样的巨型框架,本质上就是一座自动化分拣中心:框架读取你代码上的「标签」,自动完成依赖注入、路由注册、请求拦截——你只需专注写业务逻辑,剩下的交给标签和分拣系统。
掌握装饰器与反射元数据,你就看懂了 NestJS 等主流框架的底层魔法。作为 AI 从业者,这意味着你能在构建 AI 应用的 API 服务时,轻松驾驭依赖注入与模块化架构,写出结构清晰、易维护的工程代码。
目录
可视化演示
展示Transformer自注意力计算流程:Q/K/V矩阵映射、注意力分数矩阵、Softmax归一化与加权求和。支持单头注意力与多头并行模式切换。
本节配有交互动效(进入视口后自动加载,约 3KB)。
自注意力计算流程 Q与K点积计算注意力分数 Softmax归一化后加权聚合V Attn(Q,K,V)=softmax(QK^T/√d)V 缩放点积注意力机制
装饰器概述:给代码贴「快递标签」
快递分拣中心建成的第一天,你发现分拣员们总是在争吵:「这个包裹要不要轻拿轻放?」你决定引入标准标签制度:每个包裹进入分拣台,先经过一台「标签机」,它根据包裹的特点贴上一张标准标签。标签机不拆开包裹,不改变包裹里的东西,只是在外层贴一张纸。这就是装饰器的本质——一种不改变代码内部实现、只附加额外信息的「贴标签」机制。
在 TypeScript 中,装饰器是一种特殊的声明,以 @ 符号开头,紧跟一个表达式。这个表达式求值后必须是一个函数,函数会在运行时接收被装饰的目标,并可以返回一个新目标来替换它。装饰器可以作用于类、方法、属性、参数四类目标,分别对应「整箱」「箱内物品」「物品属性」「搬运口令」四种标签。
从编程范式看,装饰器是「声明式元编程」的典型代表。命令式编程要自己写代码告诉程序每一步怎么做;而声明式编程只需要声明「这个方法是只读的」,剩下的由框架完成。这就像你告诉分拣员「这个包裹易碎」,而不必手把手教他怎么轻拿轻放。
装饰器是编程中「曲线救国」的智慧:我们无法直接修改语言底层,但可以给代码贴上标签,让框架替我们完成那些重复、机械的工作。标签制度在分拣中心推广开后,每个分拣员都开始依赖这些标签——接下来,我们看看不同类型的包裹适合什么标签。
- @decorator@ 符号与装饰器函数名,相当于标签机上写着标签类型的按钮。
- Target被装饰的目标,可以是类、方法、属性或参数,相当于等待贴标签的快递包裹。
- decorator(Target)调用装饰器函数并传入目标,相当于标签机在包裹上「按下」标签。
- 赋值装饰器的返回值替换原目标,相当于贴好标签的包裹重新回到流水线上继续流转。
类装饰器:给整箱包裹贴总标签
分拣中心开始处理整箱货物。你发现,有些箱子需要整体标注「整车加急」,而不是给箱子里每一件商品单独贴标签。类装饰器就是这个思路——它作用在整个「箱子」(类)上,而不是箱子里的单个物件。
类装饰器是作用于类声明上的装饰器。当 TypeScript 编译时,类装饰器接收的唯一参数是类的构造函数。它可以在类的层面做手脚:给类添加静态属性、修改原型、甚至用一个新的类完全替换旧的类。注意类装饰器不接收「属性名」——因为类这个层级还没有到具体成员。
在 NestJS 中,@Controller('/users') 和 @Injectable() 都是类装饰器。@Controller 在类的构造函数上记录一个路由前缀,@Injectable 标记该类可以被依赖注入容器管理。这些装饰器都不需要修改类内部的任何代码——它们只是「绕着类转了一圈」,在标签登记簿上写了几笔。
为什么类装饰器能「换掉」一个类?因为在 JavaScript 里,类本质上就是构造函数加原型,「替换构造函数」在运行时完全可行。不过在真实工程中,大部分类装饰器并不会真的换类,而是像 @Injectable 那样默默登记「这个类可以用」。真正「换类」的场景,通常出现在高阶组件或依赖注入容器的框架代码里。
- classDecorator类装饰器函数名,相当于「整箱加急」的标签机。
- constructor被装饰类的构造函数,相当于箱子的「总清单」。
- Function类型标注,说明总清单本质上是一个函数。
- 返回值新的构造函数。如果装饰器有返回值,它会替换掉原来的构造函数,相当于换了一张总清单贴在箱子上。
方法装饰器:给易碎品贴特殊标签
箱子里总有一些特别脆弱的物品需要单独标识。比如一个玻璃花瓶,整箱标签只能写「轻放」,但它需要的是「绝对防震」。这时候分拣员会在花瓶上单独贴一张红色「易碎」标签。方法装饰器就像这张红色标签——它贴在类中的某个具体方法上,告诉框架「这个方法需要特殊处理」。
方法装饰器在方法声明之前定义。它接收三个参数:第一个是类的原型对象(如果是静态方法则是构造函数),第二个是方法名,第三个是方法的属性描述符(Property Descriptor)。属性描述符是 JavaScript 底层的「方法配置单」,包含 value(函数本身)、writable(能否重新赋值)、enumerable(能否被遍历)、configurable(能否删除)四个字段。
方法装饰器最实用的能力是「拦截」:通过改写描述符中的 value,可以在原方法执行前后插入自己的逻辑。日志记录、权限校验、缓存、事务管理,这些横切关注点都能用一个方法装饰器干净利落地解决。这正是 AOP(面向切面编程)的思想——把日志、权限这些「横切关注点」从业务逻辑中剥离,用装饰器优雅织入。
借助 d.value 的改写,方法装饰器可以给方法「裹上一层保护壳」。这层壳在每次方法调用时先执行额外逻辑,再调用原始方法。你的业务方法依然专心做自己的事,「警戒线」由装饰器来拉。这也是 @Get('/list') 这类路由装饰器的底层原理——它把路径信息挂在方法上,让框架在扫描时发现它。
d.value 里放着原方法。- t目标对象的原型(或构造函数),相当于箱子的「箱壁」。
- k方法的名称,相当于玻璃花瓶的名字。
- d属性描述符,相当于花瓶的「易碎」配置单,记录方法的各种属性。
- d'新的描述符,相当于分拣员在配置单上追加一句「必须立放」。
属性装饰器:只登记不改动的标签
分拣中心还有一种更细的标签:贴在单个物品上的「重量」「产地」标签。这些标签不改变物品本身的功能,只是登记信息。属性装饰器就是这样——它作用于类的属性,但不能修改属性的值,只能登记属性相关的元信息。
为什么不能修改值?因为属性装饰器在类定义阶段就被调用了,这时候实例还没创建,根本没有「值」可以改。它接收的参数只有两个:目标原型和属性名(对比方法装饰器的三个参数)。它的主要用途是:把属性的设计时类型信息记录到元数据里,供依赖注入、序列化等框架功能使用。
TypeScript 在编译开启了 emitDecoratorMetadata 的代码时,会自动调用 Reflect.metadata 装饰器,把属性的类型(design:type)等信息写入元数据。这也是类型信息在「编译后即抹除」的 JavaScript 世界里的重要保留手段。没有这步登记,NestJS 就无法在运行时知道构造函数要注入什么类型的依赖。
「不能改值」不是缺陷,而是安全边界。分拣员只需记录「这个箱子高 30 厘米、重 5 公斤」,没必要推倒箱子去量。属性装饰器只做登记,这让框架可以安全地扫描并收集类的结构信息,无需担心副作用。少了这份克制,依赖注入就会变成一场不可控的「拆箱游戏」。
- t类的原型,相当于标签要贴的那个「货架层」。
- k属性名,相当于货架上的具体商品名。
- void无返回值,属性装饰器「贴完就走」,不改变货架上的任何东西。
- designtype:反射读取的类型信息,相当于事后去登记簿上查「这个商品当初标记的类型是什么」。
执行机制:先贴里层,再贴外层
分拣中心墙上贴着一张《标签作业顺序规范》:如果要给同一个箱子贴两张标签,必须先贴里面的,再贴外面的。装饰器的执行机制与此惊人一致——它遵循一条看似反直觉的规则:求值从上到下,调用从下到上。
对于 @A @B class C,执行分两步:第一步,从上到下求值 @A 和 @B 两个表达式,得到装饰器函数的引用;第二步,从下到上进行函数调用——先执行 B(C),再把结果传给 A。多个装饰器组合时,最下方的装饰器最贴近目标,最上方的装饰器在最外层。这就像穿衣服:先穿内衣,再穿外套。
如果装饰器带参数,比如 @Controller('/users'),还会多一层「工厂函数」:先调用 Controller 工厂拿到真正的装饰器函数,再用它去装饰类。理解这个执行顺序是写复杂装饰器的前提——如果把依赖顺序弄反,装饰器拿到的可能是尚未被初始化的对象,程序会在运行时悄悄出错。
这个「洋葱模型」不仅决定装饰器的包裹层次,也决定了嵌套顺序所带来的视觉效果。在 NestJS 中,你可能会看到 @Controller 与 @UseGuards 同时出现——守卫包在控制器外层,请求先经过守卫再进入控制器。理解执行顺序,你就理解了框架的「标签读取」流程:机器沿着标签从外往里读,直到最核心的业务逻辑。
- @A @B C书写形式,最上方是 A,最下方是 B。
- C被装饰的类,最内层的「身体」。
- B(C)先调用 B,最下方的装饰器最先作用在 C 上。
- A(B(C))再调用 A,最上方的装饰器作用在 B 的结果上。
元数据反射:标签登记簿
分拣中心越做越大,你发现光贴标签还不够,需要一本「标签登记簿」:每次贴标签时顺手记一笔,后续查询「这个箱子贴过什么标签」时,不用翻箱倒柜,直接查登记簿。Reflect Metadata 就是 TypeScript 世界里的这本登记簿。
Reflect Metadata 是 ECMAScript 的一个提案(当时处于 Stage 2,现已被装饰器 Stage 3 提案吸收),它为 Reflect 全局对象扩展了一套元数据读写 API。核心方法有四组:defineMetadata 写入、getMetadata 读取(包含原型链)、getOwnMetadata 读取(不包含原型链)、hasMetadata 判断是否存在。在 TypeScript 中开启 emitDecoratorMetadata 后,编译器会自动生成 design:type、design:paramtypes、design:returntype 三类设计时类型元数据。
这套机制的价值在于:JavaScript 是一门「运行时不保留类型信息」的语言,但通过装饰器 + 元数据,我们可以在运行时重建一套类型系统。这正是 NestJS 依赖注入的前提——容器需要知道构造函数期望的参数类型,才能把正确的依赖实例注入进去。
很多初学者混淆「装饰器」与「元数据」:装饰器是「贴标签」的动作,元数据是「标签上的内容」。两者配合,才构成「声明式编程」的完整拼图——你声明意图(写元数据),框架负责执行(读元数据)。可以说,没有 Reflect Metadata,NestJS 的依赖注入就只能靠「魔法字符串」硬编码,而优雅的自动装配也无从谈起。
- k元数据的键,相当于登记簿的「标签类型」列。
- v元数据的值,相当于「标签内容」。
- t目标对象,相当于登记簿上记录的「箱子编号」。
- defineMetadata写入操作,盖一个章进登记簿。
- getMetadata读取操作,翻登记簿查「箱子 t 的标签 k 是什么」。
框架应用:NestJS 的智能分拣系统
分拣中心迎来了智能分拣系统。货物贴上标签后,传送带自动识别标签:「加急」送快速通道,「冷链」推进冷库,「偏远」流向物流车——没有任何人指挥,全靠标签驱动。NestJS 之于 JavaScript 生态,就是这套智能分拣系统。
NestJS 的架构核心是「装饰器驱动」:@Module 声明模块,@Controller 声明路由控制器,@Injectable 声明可注入的服务,@Inject 声明依赖注入的 token。这些装饰器执行时,会把各种元数据写入 Reflect Metadata。NestJS 启动时,框架扫描所有类,读取元数据,构建依赖关系图、注册路由表。
依赖注入的流程是:容器维护一个「注册表」,把每个服务和 token 对应起来。当一个类需要依赖时,容器查看这个类构造函数的设计时参数类型元数据(design:paramtypes),到注册表里找到对应的服务实例,自动注入进去。路由注册同理:@Controller('users') 写下前缀,@Get('list') 写下路径,框架读取元数据后,自动把方法挂在对应的 HTTP 路径上。
理解了这套机制,你就读懂了 NestJS 的「底层魔法」。那些看似神奇的 @Injectable() 并不是框架的晦涩黑科技,而是装饰器 + 元数据反射在工程中的经典应用。更进一步地,当你自己设计一套「标签系统」时——例如给 AI Agent 的工具函数贴上「何时可用」的标签,让调度器自动选择工具——你正在复刻 NestJS 的分拣中心架构。
- token依赖的唯一标识,相当于快递单号。
- container.get(token)从容器中取实例,相当于扫描单号在货架上取出对应包裹。
- prefix / path装饰器写入的路由前缀和路径,相当于包裹上的收货地址。
- route table框架根据元数据生成的映射,相当于分拣案上的地址归类表。
标准演进:从方言到国家标准
分拣中心的标签制度推行一段时间后,你发现各家快递公司都有自己的体系:A 家红色代表易碎,B 家蓝色代表易碎,外来的分拣员经常搞混。你决定推动「标签国家标准」——统一颜色、统一位置、统一含义。装饰器的标准演进,正是这样一条从「各家方言」走向「国际标准」的路。
装饰器最初是 TypeScript 的实验特性,需要在 tsconfig.json 里开启 experimentalDecorators 和 emitDecoratorMetadata 才能使用。这套「旧语法」(legacy decorators)基于 ES2016 装饰器草案,功能强大但存在问题:编译器要生成大量辅助代码(如 __decorate 函数);对装饰器返回值类型校验宽松;元数据发射依赖私有 API,不利于标准化。
2022 年,ECMAScript 装饰器提案进入 Stage 3,带来全新语法。新签名统一为 (value, context):value 是被装饰的值(类、方法、属性等),context 是包含 kind、name、access、addInitializer 等信息的上下文对象。新语法还移除了旧版的参数装饰器,不再自动发射 design:type 元数据——这导致 reflect-metadata 这类库在新语法下存在兼容性问题。
标准演进对开发者意味着什么?新语法更统一、更安全,浏览器原生支持也在路上(Chrome 已默认开启)。但 NestJS 等大型框架的生态目前仍深度依赖旧语法和反射元数据,所以 TypeScript 官方短期内会继续支持旧语法,只是不再推动它走向标准。作为开发者,「新旧语法都要看得懂」是很有用的能力——就像分拣员既要会解读老标签,也要会用新国标。
- legacy(t, k, d)旧版签名——原型、名字、描述符三个参数。
- v新世界里的「物品本身」,被装饰的值。
- c新世界里的「标签说明」上下文对象,包含 kind、name 等字段。
- 箭头从旧到新的演进,就像从各贴各的标签到统一国标。
AI 应用场景
NestJS 中构建 AI API 服务
阶段:后端服务架构 / API 层
解决的问题:AI 应用要接入大模型客户端、向量数据库、缓存等多个外部依赖。如果处处手写 new 实例,代码会重度耦合、难以单元测试和更换实现。
具体案例:用 NestJS 构建 RAG 问答服务,@Injectable() 标注 LlmService 和 VectorStoreService,控制器构造函数自动注入这两个依赖;@Controller('/rag') 声明路由前缀,@Post('query') 注册提问接口。框架启动时扫描元数据完成装配,你只需专注提示词工程与检索逻辑。
AI Agent 工具注册与调度
阶段:Agent 工具调用 / 函数调度
解决的问题:AI Agent 需要动态发现「有哪些工具可用、参数是什么」。手动维护工具清单既繁琐又容易漏配,且工具增删时易出错。
具体案例:定义一个 @Tool(name, description) 装饰器标记工具函数,并利用反射元数据在 Agent 启动时扫描所有被标记的函数,自动生成 OpenAI function calling 所需的 tools 参数列表。新增工具只需加一行装饰器,调度器自动感知。
LLM 输出结构化校验
阶段:数据处理 / 输入输出校验
解决的问题:LLM 输出的 JSON 结构不稳定,字段类型可能漂移。手写校验逻辑冗长且难以复用。
具体案例:在 DTO 类中用属性装饰器标注字段类型(如 @Field('temperature', 'number')),框架读取元数据后对 LLM 返回的结构化内容做自动校验,类型不符时触发重试或告警。装饰器让校验规则与数据结构定义天然地放在一起。
Python 演示
"""用 Python 模拟 TypeScript 装饰器与 Reflect Metadata演示依赖注入 + 路由注册的底层机制"""import functools # 模拟 Reflect Metadata 的存储层_META = {} def define_metadata(key, value, target): # 写入元数据: 相当于 Reflect.defineMetadata _META.setdefault(id(target), {})[key] = value def get_metadata(key, target, default=None): # 读取元数据: 相当于 Reflect.getMetadata return _META.get(id(target), {}).get(key, default) class Injectable: # 类装饰器: 标记可注入依赖 def __call__(self, cls): define_metadata('injectable', True, cls) return cls class Controller: # 类装饰器: 注册路由前缀 def __init__(self, prefix): self.prefix = prefix def __call__(self, cls): define_metadata('prefix', self.prefix, cls) routes = [] for name, method in cls.__dict__.items(): path = getattr(method, '_path', None) if path is not None: full = self.prefix + path routes.append((name, full, method)) define_metadata('routes', routes, cls) return cls class Get: # 方法装饰器: 标记 HTTP GET 路径 def __init__(self, path): self.path = path def __call__(self, method): method._path = self.path @functools.wraps(method) def wrapper(*args, **kwargs): print(f"[路由] GET {self.path} -> {method.__name__}") return method(*args, **kwargs) wrapper._path = self.path return wrapper class Log: # 方法装饰器: 记录调用日志 def __call__(self, method): @functools.wraps(method) def wrapper(*args, **kwargs): print(f"[日志] 调用 {method.__name__}") result = method(*args, **kwargs) print(f"[日志] 完成 {method.__name__}") return result return wrapper # 模拟框架启动: 扫描 + 注册@Injectable@Controller('/api/users')class UserService: def __init__(self, db="内存库"): self.db = db @Get('/list') @Log() def list_users(self): return f"[{self.db}] 用户列表: Alice, Bob" if __name__ == '__main__': print("=== 启动时扫描元数据 ===") print("injectable:", get_metadata('injectable', UserService)) print("路由前缀:", get_metadata('prefix', UserService)) print("路由表:") for name, path, fn in get_metadata('routes', UserService): print(f" GET {path} -> {name}") print("\n=== 调用业务方法 ===") svc = UserService() print(svc.list_users())
_META.setdefault(id(target), {})[key] = value用目标对象的 id 作为键,把元数据存入全局「标签登记簿」。class Injectable:模拟@Injectable()类装饰器:只在元数据中标记 injectable=True,不改变类行为。class Controller:模拟@Controller(prefix):扫描类方法,把带_path标签的方法收集成路由表。method._path = self.path给原方法打上「路由路径」标签,供外层装饰器和 Controller 扫描读取。@functools.wraps(method)保留原函数的__name__和__dict__,确保路由标签不因包装而丢失。wrapper._path = self.path包装函数也要接力传递路由标签,否则 Controller 会找不到它。if __name__ == '__main__':入口:像 NestJS 一样在启动时扫描元数据并打印路由表,然后调用业务方法。
常见误区
误区「装饰器会修改类或方法的内部逻辑,所以用了装饰器就要小心原有功能被破坏。」
正解装饰器确实可以返回一个新对象替换原目标,但绝大多数框架装饰器(如 @Controller、@Injectable)只是「登记信息」,并不碰原类的方法实现。方法装饰器可以改写属性描述符来「包装」原方法,但包装后的函数调用的仍是原逻辑。装饰器的核心是附加元信息,而非重写业务。
为何常见:装饰器语法看起来像是「包裹」了代码,初学者容易把「包裹」误解为「修改」——就像有人看到快递标签,以为标签会改变包裹里的商品。
误区「装饰器写在上面的先执行,因为它的位置靠前,代码通常都是从上往下读的。」
正解恰恰相反。@A @B C 等价于 A(B(C))——先执行下方的 B,再执行上方的 A。求值顺序是从上到下(先拿到 A 和 B 的引用),但函数调用是从下到上(先让 B 贴在最里面)。这就像穿衣服,内衣永远先于外套。
为何常见:几乎所有编程语言的语句都从上到下执行,这种直觉被生搬硬套到了装饰器上。
误区「Reflect.defineMetadata 是 JS 标准内置的,直接就能用,不用装任何库。」
正解Reflect.defineMetadata 并不是 ECMAScript 标准的一部分,它来自 reflect-metadata 这个第三方库,通常需要在入口文件 import 'reflect-metadata' 手动引入。且它是为旧版装饰器设计的;新版 Stage 3 装饰器提案中,反射元数据属于「可选附加提案」,需要单独支持。
为何常见:TypeScript 编译后的代码里确实调用了 Reflect.metadata,而且很多框架都悄悄引入了这个库,让人误以为它是语言原生的。
误区「属性装饰器接收目标原型和属性名,那直接 `target[propName] = 'xxx'` 就能修改属性了吧?」
正解虽然技术上能在 target 上设置属性,但这是修改「类的原型」而非「实例的值」,且与设计意图相悖。属性装饰器没有属性描述符参数,无法控制实例属性的读写,它只应该用于记录元信息——比如登记 design:type 供依赖注入使用。
为何常见:方法装饰器能修改属性描述符,「方法能改,属性也应该能改」的类比形成了误导,忽略了参数个数的差异。
误区「装饰器是 TypeScript 的专利,JavaScript 完全用不了。」
正解装饰器不是 TS 的独创。新的 ECMAScript 装饰器提案(Stage 3)已进入 JavaScript 标准轨道,Babel 等编译器也支持转译。TypeScript 的 experimentalDecorators 只是基于旧草案的先行实现。类比:Python、Java 也有各自的注解与装饰器机制,这是语言生态的通用思想。
为何常见:大量教学资料以 TypeScript 为例讲解装饰器,容易让人误以为它是 TS 的语言特性——就像很多人以为「泛型是 Java 的专利」一样。
面试题
Q1. 装饰器在 TypeScript 中的核心作用是什么?
- A.修改代码的编译速度
- B.为类、方法、属性附加元信息← 正确答案
- C.替代接口实现类型检查
- D.生成运行时类型校验工具
答案:B。装饰器的本质是在不修改代码内部实现的前提下附加声明式元信息,就像给快递包裹贴标签。A 与编译速度无关,C 由类型系统而非装饰器完成,D 只是装饰器的应用之一而非核心作用。
Q2. 类装饰器接收的唯一参数是什么?
- A.类的原型对象
- B.类的实例
- C.类的构造函数← 正确答案
- D.类的属性描述符
答案:C。类装饰器作用于「整箱包裹」,接收的唯一参数是构造函数。原型对象是方法装饰器才接触的,实例在类定义阶段尚不存在,属性描述符是方法装饰器的参数。
Q3. 对于 @A @B class C {},装饰器函数调用顺序是?
- A.先调用 A 再调用 B
- B.先调用 B 再调用 A← 正确答案
- C.按书写顺序同时调用
- D.由编译器随机决定顺序
答案:B。装饰器等价于 A(B(C)),先执行下方的 B,再执行上方的 A——像穿衣服,先穿内衣再穿外套,而不是按书写顺序「从上到下」。
Q4. 方法装饰器默认接收哪三个参数?
- A.构造函数、方法名、参数列表
- B.原型对象、方法名、属性描述符← 正确答案
- C.实例、方法名、返回值
- D.类、方法名、this 上下文
答案:B。方法装饰器接收目标原型(静态方法时为构造函数)、方法名、属性描述符。描述符里装着 value、writable 等「方法配置单」,修改它可实现拦截逻辑。
Q5. Reflect Metadata 中,哪个方法用于读取对象上的元数据?
- A.Reflect.readMetadata
- B.Reflect.getMetadata← 正确答案
- C.Reflect.loadMetadata
- D.Reflect.findMetadata
答案:B。Reflect.getMetadata 是标准 API,用于从「标签登记簿」读取元数据。其他三个命名并不存在,属于干扰项。
Q6. NestJS 中,@Injectable() 类装饰器的主要作用是?
- A.给类添加静态方法
- B.声明该类可被依赖注入容器管理← 正确答案
- C.将类转换为 JSON 序列化格式
- D.自动生成类的构造函数
答案:B。@Injectable() 向容器登记「这个类是可用依赖」,相当于给包裹贴上「可入库」标签。它不生成构造函数,也不改序列化行为。
Q7. 关于属性装饰器,下列说法正确的是?
- A.它接收原型对象和属性名作为参数← 正确答案
- B.它可以修改属性的值
- C.它通常用于记录属性的元信息← 正确答案
- D.它接收属性描述符作为第三个参数
答案:AC。属性装饰器只有两个参数(原型、属性名),没有描述符参数,所以不能修改属性值——它只登记元信息。因此 B 和 D 错误,A 和 C 正确。
Q8. 装饰器标准演进(从实验版到 Stage 3)的主要变化包括?
- A.新签名统一为 (value, context)← 正确答案
- B.Stage 3 移除了参数装饰器← 正确答案
- C.Stage 3 仍自动发射 design:type 元数据
- D.新提案仍然需要 experimentalDecorators 开关
答案:AB。Stage 3 的新签名是 (value, context),且移除了参数装饰器。C 错误——新提案不再自动发射 design:type;D 错误——experimentalDecorators 是旧版 TypeScript 实验开关。
Q9. 在 NestJS 中,路由前缀和路径是如何注册到路由表的?
- A.编译器自动推断方法名
- B.装饰器写入元数据,框架启动时扫描读取← 正确答案
- C.开发者手动在路由配置文件中声明
- D.运行时监听 HTTP 请求自动生成
答案:B。@Controller 和 @Get 执行时把前缀、路径写入元数据,NestJS 启动时像分拣员扫描标签一样读取元数据,自动组装路由表。
Q10. 关于装饰器执行机制,以下哪些说法正确?
- A.装饰器表达式求值从上到下← 正确答案
- B.装饰器函数调用从下到上← 正确答案
- C.带参装饰器需先经过工厂函数求值← 正确答案
- D.类装饰器永远先于方法装饰器执行
答案:ABC。A、B 是装饰器执行机制的核心——求值自上而下、调用自下而上。C 正确,@Controller('/x') 需要先调用工厂拿到真正的装饰器。D 错误,执行顺序取决于装饰器在语法结构中的位置,并非类装饰器一定先执行。