前言
@decorator 装饰器是 es7 更新的提案,是一种与类相关的语法,用来注释或修改类和类的方法,是在装饰器模式的基础上产生的。装饰器是过去几年中js最大的成就之一,已是ES7的标准特性之一。
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,属性或参数上,可以修改类的行为。 通俗的讲装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能。
常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器
装饰器的写法:普通装饰器(无法传参) 、 装饰器工厂(可传参,下面的内容都将基于工厂模式)
先来了解一下普通装饰器的写法,工厂模式的写法在后面会看到很多,就不在这里介绍了
function Decorator(targetClass: any) { console.log(targetClass); targetClass.prototype.msg = "装饰器注入的实例属性"; targetClass.func = function () { console.log("装饰器注入的方法"); }; } @Decorator class Animals { constructor() {} } const animals: any = new Animals(); // @ts-ignore Animals.func(); console.log(animals.msg); 复制代码
可以直接再类上添加静态属性和静态方法,也可以通过prototype来添加实例方法
运行结果如下(如果使用TS来写代码,Node环境下需要配置typescript环境,这里我直接使用deno运行)
类装饰器
上面的普通装饰器的示例就是一个类装饰器,这里我们将它重构一下,使用工厂模式来实现
function Decorator(params: any) { return function (targetClass: any) { targetClass.prototype.msg = `来自装饰器注入的消息${params}`; }; } @Decorator("args") class Animals { constructor() {} } const animals: any = new Animals(); console.log(animals.msg); 复制代码
实现方式类似于柯里化
使用装饰器的时候传递参数,在使用一些Node框架的时候经常遇到,类似于@Controller('/users')
,通过装饰器指定controller的路由,这种情境下使用普通的装饰器函数已经无法满足,而工厂模式则可以轻而易举的完成这个任务。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数; 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明;所以装饰器除了注入新的属性,也可以用来重载类的属性和方法,但是必须重载所有的属性和方法
属性装饰器
顾名思义,属性装饰器用来修饰类的属性,可以用在类的属性、方法、get/set
函数中,属性装饰器接收三个参数,目标类、被装饰的属性key、被装饰的属性描述,例如我们可以通过装饰器来将属性变为只读
function readonly() { return function ( target: unknown, key: string, descriptor: PropertyDescriptor ) { console.log(target, key, descriptor); console.log(descriptor.value.toString()); descriptor.writable = false; return descriptor; }; } class Person { constructor() {} @readonly() sayHi() { console.log("Hi"); } } const person = new Person(); person.sayHi(); person.sayHi = function () {}; 复制代码
除了修改方法描述,还可以使用target[key]
的形式来获取属性值或者修改属性值。(修饰方法时可以通过descriptor.value来获取函数体,后面会用到)
通过只读修饰之后的属性再修改属性值的时候就会报错。属性装饰器装饰方法的时候可以用来实现日志操作以及方法的拦截
修饰方法的装饰器最后必须返回属性描述descriptor
参数装饰器
参数装饰器用来装饰方法中的形参,装饰参数时接收三个参数:目标类、方法名、参数在arguments
中的索引
function LogParams(params: string) { return function (target: unknown, methodName: string, index: number) { console.log(target, methodName, index); console.log(`监听${params}`); }; } class Person { constructor() {} sayHi(@LogParams("user") user: string) { console.log("Hi", user); } } const person = new Person(); person.sayHi("king"); 复制代码
参数装饰器只能用来监视一个方法的参数是否被传入,并不能做太多的处理,所以并不常用
装饰器的妙用
防抖&节流
节流防抖使我们日常开发中经常使用的性能优化的手段,之前的使用都需要封装一层函数,看起来也不舒服,现在有了装饰器,我们可以非常“爽”地进行防抖和节流的优化
// 节流 const throttle = (time: number) => { let prev = new Date().getTime(); return (target: unknown, name: string, descriptor: PropertyDescriptor) => { // 前面的示例中说到过,通过descriptor.value获取函数体 const func = descriptor.value; if (typeof func === "function") { descriptor.value = function (...args: any[]) { const now = new Date().getTime(); if (now - prev > time) { func.apply(this, args); prev = new Date().getTime(); } }; } }; }; // 防抖 const debounce = (time: number) => { let timer: number; return (target: unknown, name: string, descriptor: PropertyDescriptor) => { const func = descriptor.value; if (typeof func === "function") { descriptor.value = function (...args: any[]) { if (timer) clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, time); }; } }; }; 复制代码
使用起来也非常的舒服,比如组件要监听滚动事件,我们就可以直接在绑定的函数上使用装饰器
class App extends React.Component { componentDidMount() { window.addEveneListener('scroll', this.scroll); } componentWillUnmount() { window.removeEveneListener('scroll', this.scroll); } @throttle(50) scroll() {} } 复制代码
类型校验
类型校验主要应用于JavaScript,typescript自带类型检验,所以不太有必要使用
const validate = (type) => (target, name) => { if (typeof target[name] !== type) { throw new Error(`TypeError: attribute ${name} must be ${type} type`) } } class Form { @validate('string') static name = 111 // TypeError: attribute name must be ${type} type }