前言
@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
}