当前位置: 首页 > news >正文

装饰器原理

基础问答

问:什么是装饰器?有什么作用?

答:装饰器是一种​元编程语法,可以在不修改原有代码的前提下,动态地为类、方法、属性等添加一些能力,本质上还是一个函数,它接收目标对象、属性名、属性描述符(或类本身)作为参数,返回修改后的目标对象或属性描述符。

在使用的时候,是声明式的使用,在装饰器函数前加上一个 @ 符号,在需要使用的函数、类、方法、属性上一行的位置添加,如下:

// 定义类装饰器:为类添加版本信息

function addVersion(version) {

// 装饰器函数接收类作为参数

return function (target) {

// 为类添加静态属性

target.version = version;

// 为类添加静态方法

target.logVersion = function () {

console.log(`版本号:${this.version}`);

};

// 返回修改后的类(也可返回新类)

return target;

};

}

// 使用装饰器修饰类

@addVersion('1.0.0')

class MyClass {

constructor(name) {

this.name = name;

}

}

// 测试效果

console.log(MyClass.version); // 输出:1.0.0

MyClass.logVersion(); // 输出:版本号:1.0.0

如果你不想在本地运行,可以在 Typescript Playground 编译运行。

扩展延伸

先说个题外话,你需要知道的是,装饰器现在还是在提案阶段(TC39 Stage 3),自 2015 年提出以来,到现在依然没有成为规范的一部分,但是目前已经广泛的应用在前端的很多库中,如 MobX 、Angular 依赖注入等,只是我们在使用上略微复杂,需要通过一些编译工具(如 Babel)或 Typescript 来进行转换。

基本使用

装饰器可用于修饰​类​、​类方法​、​类属性​、访问器(getter/setter) 等,不同修饰的场景下的语法和参数略有一些差异。

类装饰器:一般用于修饰整个类,可以为类添加静态属性和方法(参考基础问答部分代码),也可以修改类的构造函数,示例如下:

// 定义类装饰器:为实例添加默认属性

function addDefaultProps(props: Record<string, any>) {

return function <T extends { new (...args: any[]): {} }>(target: T) {

// 返回一个新的类,继承自原类

return class extends target {

constructor(...args: any[]) {

super(...args);

// 添加默认属性

Object.assign(this, props);

}

};

};

}

// 使用装饰器

@addDefaultProps({ type: 'base', status: 'active' })

class MyClass {

name: string;

constructor(name: string) {

this.name = name;

}

}

// 测试

const instance = new MyClass('test');

console.log(instance); // 输出:{ name: 'test', type: 'base', status: 'active' }

方法装饰器:方法装饰器用于修饰类的方法,接收三个参数

​target:类的原型对象(静态方法则为类本身)

​propertyKey:方法名

​descriptor​:方法的属性描述符({ value, writable, enumerable, configurable })

// 定义方法装饰器:记录方法调用日志

function log(target, propertyKey, descriptor) {

// 保存原方法

const originalMethod = descriptor.value;

// 重写方法

descriptor.value = function (...args) {

console.log(`[日志] 调用方法 ${propertyKey},参数:`, args);

// 调用原方法并获取返回值

const result = originalMethod.apply(this, args);

console.log(`[日志] 方法 ${propertyKey} 返回:`, result);

return result;

};

// 返回修改后的描述符

return descriptor;

}

class Calculator {

// 使用装饰器修饰方法

@log

add(a, b) {

return a + b;

}

}

// 测试

const calc = new Calculator();

calc.add(2, 3);

// 输出:

// [日志] 调用方法 add,参数: [2, 3]

// [日志] 方法 add 返回: 5

属性装饰器:用于修饰类的属性,接收两个参数

​target:类的原型对象(静态属性则为类本身)

​propertyKey:属性名

// 定义属性装饰器:限制属性值范围

function range(min, max) {

return function (target, propertyKey) {

// 定义私有属性存储值(避免命名冲突)

const privateKey = `_${propertyKey}`;

// 通过Object.defineProperty定义属性

Object.defineProperty(target, propertyKey, {

get() {

return this[privateKey];

},

set(value) {

if (value < min || value > max) {

throw new Error(`${propertyKey} 必须在 ${min}-${max} 范围内`);

}

this[privateKey] = value;

}

});

};

}

class User {

@range(0, 120)

age;

constructor(age) {

this.age = age;

}

}

// 测试

const user1 = new User(25);

console.log(user1.age); // 输出:25

const user2 = new User(150);

// 抛出错误:age 必须在 0-120 范围内

访问器装饰器:用于修饰类的 getter​ 或setter​,参数与方法装饰器相同(target​、propertyKey​、descriptor),返回修改后的描述符

// 定义访问器装饰器:过滤敏感字符

function sanitize(target, propertyKey, descriptor) {

// 判断是getter还是setter

if (descriptor.get) {

const originalGet = descriptor.get;

descriptor.get = function () {

const value = originalGet.apply(this);

// 过滤HTML标签

return value.replace(/<[^>]+>/g, '');

};

}

return descriptor;

}

class Message {

constructor(content) {

this._content = content;

}

// 使用装饰器修饰getter

@sanitize

get content() {

return this._content;

}

}

// 测试

const msg = new Message('<script>恶意代码</script> 正常内容');

console.log(msg.content); // 输出:恶意代码 正常内容(已过滤<script>标签)

需要注意的是,JavaScript 没有支持装饰器,前面说了装饰器还在提案阶段,要使用这个特性,需要通过 Typescript 或 babel 等编译器,而且随着版本的更迭,有些写法会有不同,如果你运行了这个表格

工作原理

和 JavaScript 中的 class、async/await 类似,装饰器也是一个语法糖,底层还是通过函数调用实现。

编译过程

对于类装饰器:当使用装饰器函数修饰一个类的时候,顺序是,先定义这个类,然后在这个使用装饰器函数包裹这个类(作为参数传递),最后用函数返回值覆盖原来的类。

你可以简单的视为:

// 源码

@decorator

class MyClass {}

// 编译后(近似)

class MyClass {}

MyClass = decorator(MyClass) || MyClass;

更详细的编译结果,可以在自己运行一次 TypeScript 的编译得到。

对于方法装饰器,很容易根据上面的想到,其编译过程是在类定义之后,将方法作为参数传递

执行时机

装饰器在​类定义阶段执行(而非实例化阶段),这意味着:

装饰器的逻辑在类被定义时就会执行,而非调用方法或创建实例时;

装饰器内部无法访问类的实例(this指向原型对象或类本身,而非实例)。

function logWhenDefined(target) {

console.log('类被定义了!');

return target;

}

@logWhenDefined

class MyClass {}

// 输出:类被定义了!(此时还未创建实例)

面试追问

装饰器和高阶函数的区别是什么?

相同点:两者都可实现功能扩展,本质都是函数

​不同点:

装饰器是​语法糖​,有明确的语法规范(@符号),仅用于修饰类或类成员;

高阶函数是​函数式编程概念,指接收函数作为参数或返回函数的函数,适用范围更广(可修饰任何函数,不限于类方法);

装饰器在类定义阶段执行,高阶函数在函数调用阶段执行。

这里给出一个高阶函数示例:

// 高阶函数实现日志功能(与方法装饰器效果类似)

function withLog(fn) {

return function (...args) {

console.log('调用函数,参数:', args);

const result = fn.apply(this, args);

console.log('函数返回:', result);

return result;

};

}

// 用高阶函数修饰普通函数

function add(a, b) {

return a + b;

}

const addWithLog = withLog(add);

addWithLog(1, 2)

装饰器,可以装饰函数和对象吗?

装饰器仅支持类和类成员(方法、属性、访问器),不支持普通函数或对象,是因为函数存在函数(变量)提升,装饰器执行时机(定义阶段)与函数提升可能冲突,导致逻辑混乱,如果想实现类似的效果,建议是通过高阶函数来实现,参考上一问。

实际开发过程中,你在什么场景下使用装饰器?

​日志与监控​:为方法添加调用日志、性能统计(如上述log​和measureTime装饰器);

​权限控制​:在需要权限的方法前添加权限校验(如requirePermission);

​缓存处理:为耗时方法添加结果缓存(避免重复计算);

​框架集成:

Angular:用装饰器定义组件(@Component​)、服务(@Injectable);

MobX:用@observable​、@action装饰器管理状态;

Vue Class Component:用@Component​、@Prop装饰器定义 Vue 组件;

数据校验:为类属性添加类型或范围校验(如@range装饰器)。

使用装饰器的时候,遇到过什么问题?

​兼容性:装饰器仍为提案,需通过 Babel/TypeScript 转译,不同转译工具可能有语法差异;

​执行时机:装饰器在类定义时执行,避免在装饰器中编写依赖实例的逻辑;

​原型链影响:修改类或方法时需注意保持原型链完整(如单例装饰器中继承原类原型);

性能开销:装饰器会增加函数调用层级,复杂装饰器可能影响性能(需适度使用)。

你写的这个方法装饰器,为什么我运行报错?

这个就是一个踩坑的地方,由于装饰器并没有正式的落地标准,所以你会发现有一些网上的装饰器代码你运行不起来,注意切换Typescript或babel的插件版本去解决。

如本文中的代码,在 Typescript 3.x 版本中都可以正常使用,但是升级版本后有些就不兼容了。

http://www.jsqmd.com/news/87325/

相关文章:

  • 超简单AI绘画神器:Stable Diffusion-NCNN让文字秒变精美图片
  • 29、Ubuntu系统安全加密与日志缓存管理全攻略
  • mac 笔记本如何切换中英文输入
  • 实用指南:Rust 并发实战:从零构建一个内存安全的“番茄时钟”
  • BIOS/UEFI 与其分别使用的磁盘分区形式 MBR/GPT
  • 2025年专业的工业制氮机设备厂家推荐及选购参考榜 - 品牌宣传支持者
  • Linux磁盘调度算法实战指南:从原理到性能优化的完整解决方案
  • 32、Ubuntu 网络代理配置与安全应用全解析
  • matlab实现时间相位展开算法
  • 免费终极指南:快速上手MinerU实现PDF到Markdown完美转换
  • 28、Ubuntu系统安全与加密全攻略
  • 3步搞定Serverless Offline多容器网络通信终极配置指南
  • 2025年有实力的钢质艺术楼梯/艺术楼梯加工厂家最新推荐权威榜 - 品牌宣传支持者
  • 如何用30亿参数实现专业级AI音乐创作:腾讯SongGeneration技术解析与实践指南
  • ipympl 终极指南:在 Jupyter 中实现 Matplotlib 交互式绘图
  • 2025年无线信号测量仪表十大品牌权威排行榜,雷达干扰模拟器/电子对抗设备/光纤熔接机/无线信号测量仪表无线信号测量仪表品牌排行 - 品牌推荐师
  • Unity教学 项目4 3D求生枪手
  • OpenCV全景拼接终极指南:从零开始快速上手全景图像制作
  • 基于vue的酒店宾馆客房管理系统_6u85gvj9_springboot php python nodejs
  • 云电脑深度玩转CANN:从环境适配到工业级应用落地全指南
  • 150亿参数挑战千亿模型:ServiceNow颠覆企业AI部署范式
  • Nsight Compute精准定位CUDA矩阵乘法性能瓶颈
  • 为什么说Loco+Tauri是2025年桌面应用开发的最佳选择
  • 基于准PR控制的LCL三相并网逆变器仿真模型(带报告) 参考资料:附带自己写的一份报告
  • Web前端入门第 90 问:JavaScript 也能无中生有的创建音频
  • OpenPLC Editor:工业自动化编程的5大核心优势解析
  • Apache Cassandra版本升级:从3.x到4.x的完整迁移实战指南
  • 5分钟搞定AWR1843毫米波雷达:Python实时数据读取与可视化终极指南
  • Dify.AI完整教程:零代码构建专业级AI应用的最佳实践
  • 腾讯开源Hunyuan-4B-Instruct-AWQ-Int4:轻量级大模型开启边缘智能新纪元