JS中的装饰器
一、基本概念
装饰器理解
装饰器在其他语言比如Python、Java中早就存在了。而在JavaScript中,直到目前仍处于stage2阶段的提案,这表示虽然未来应该会成为语言的一部分,但现在浏览器或Node都还不支持该特性,必须依赖于转译器。
修饰器(Decorator)是一个函数,用来修改类的行为。这是ES7的一个提案,目前Babel转码器已经支持。
在不改变原对象的基础上,为对象动态地增加职责的方式称为装饰者模式。
出现装饰器的原因:
- 传统的面向对象,给对象添加功能,往往使用继承方式,这样会有很多问题,一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变
- 继承这种功能复用方式通常被称为"白箱复用", "白箱"是相对可见性而言的,在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。完成一些功能复用的同时,有可能创建出大量的子类,使子类的数量呈爆炸性增长
装饰器优点:
- 装饰者模式允许用户在不引起子类数量暴增的前提下动态地修饰对象,添加功能,装饰者和被装饰者之间松耦合,可维护性好。
- 被装饰者可以在运行时选择不同的装饰器,实现不同的功能,灵活性好。
- 装饰者模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,可以给一个对象增加多个同样的装饰器,也可以把一个装饰器用来装饰不同的对象,有利于装饰器功能的复用;
- 可以通过选择不同的装饰者的组合,创造不同行为和功能的结合体,原有对象的代码无须改变,就可以使得原有对象的功能变得更强大和更多样化,符合开闭原则;
应用场景
我们如果要设计一个穿搭的应用程序。主题以人这个类为基础。
春夏秋冬我们的衣服穿搭是不一样的。
夏天:在人物类型上,搭配短裤短袖。
秋天:在人物类型上,搭配长裤、长袖、外套
冬天:在人物类型上,搭配羽绒服、冲锋衣、毛衣等等
搭配的服饰肯定不属于人物类型身体的一部分。我可以根据天气随意给人物类型搭配服饰。并且同一件服饰还可以给不同的人物搭配。或者同一个人物类型还可以搭配多种服饰。可以随意搭配、随意脱掉。
人物类型确定过后,服饰搭配就是我们需要给人物进行装饰。这个过程映射到系统程序,就是装饰器模式。不断的在原有的代码基础上扩展也的业务和功能。不影响原来程序本身的属性和行为。
JavaScript装饰器呢,就是对类、类属性、类方法之类的一种装饰,可以理解为在原有代码外层又包装了一层处理逻辑。这样就可以做到不直接修改代码,就实现某些功能。
在JavaScript代码中高阶函数就可以实现装饰器的效果。通过调用一个函数来包装另外一个函数。
代码如下:
js
function people(name) {
console.log('Hello, iam ' + name);
}
function peopleDecorator(wrapped) {
return function () {
console.log('穿搭衣服');
const result = wrapped.apply(this, arguments);
console.log('穿搭结束');
return result;
}
}
const result = peopleDecorator(people);
result()
效果如下:
js
调用:people()
//Hello, iam undefined
调用:result()
//穿搭衣服
//Hello, iam undefined
//穿搭结束
二、设计装饰器
(1)类的装饰器
类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
在TypeScript代码中配置TS编译选项
js
{
"compilerOptions":{
"experimentaDecorators":true
}
}
代码如下:
js
const decoratorHat = (targetClass:any) => {
//类属性
targetClass.hat = "鸭舌帽"
//成员属性
targetClass.prototype.hotColor = "red"
}
@decoratorHat
class People {
constructor(){}
}
const people = new People()
console.log(People.hat);
console.log(people.hotColor);
不管是new一个实例出来,还是直接获取类的属性,都可以打印出对应的值。
修饰器函数的第一个参数,就是所要修饰的目标类。
还可以让装饰器接受参数,这就等于可以修改装饰器的行为了,这也叫做装饰器工厂。装饰器工厂是通过在装饰器外面再封装一层函数来实现。
js
const decoratorHat = (value:any) => {
return (targetClass:any)=>{
console.log("-----",targetClass);
//类属性
targetClass.hat = "鸭舌帽"
//成员属性
targetClass.prototype.hotColor = value
}
}
@decoratorHat("red")
class People {
constructor(){}
}
const people = new People()
// console.log(People.hat);
console.log(people.hotColor);
装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数
(2)类属性装饰器
类属性装饰器可以用在类的单个成员上,无论是类的属性、方法、get/set函数。该装饰器函数有3个参数:
- target:成员所在的类
- name:类成员的名字
- descriptor:属性描述符。
比如在一个类中,制定的某个属性属于只读属性,无法修改值。我们可以设计一个@readonly
js
//不传递参数
function readonly(target: any, name: any, descriptor: any) {
descriptor.writable = false
return descriptor;
}
//传递参数
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class People {
@readonly
get username() {
return "xiaofeifei"
}
@enumerable(false)
get password(){
return "123"
}
}
const people = new People()
console.log("输出people.username", people.username);
people.username = "xiaowang"
people.password = "666"
定义类的属性的时候,使用get方法来定义,这样我们可以在readonly函数中接受三个参数
(3)方法装饰器
可以给函数的方法添加装饰器。
实际案列:记录函数的调用日志记录
js
//不传递参数
function readonly(target: any, name: any, descriptor: PropertyDescriptor) {
descriptor.enumerable = false
return descriptor;
}
//传递参数
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
function log(target:any, name:any, descriptor:any) {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function (...args:any[]) {
console.log(`${name} Arguments: ${args}`);
try {
const result = original.apply(this, args);
console.log(`${name} Result: ${result}`);
return result;
} catch (e) {
console.log(`${name} Error: ${e}`);
throw e;
}
}
}
return descriptor;
}
class People {
@readonly
get username() {
return "xiaofeifei"
}
@enumerable(false)
get password() {
return "123"
}
@log
play(params1:number,params2:number){
return params1 + params2
}
}
const people = new People()
console.log("输出people.username", people.username);
// people.username = "xiaowang"
// people.password = "666"
console.log(people.play(100,200));
(4)参数装饰器
同样使用@符号给属性添加装饰器,他会返回三个参数
- 成员所在的类
- 属性的名字
- 参数的位置
代码如下
js
const currency: ParameterDecorator = (targer: any, key: string | symbol, index: number) => {
console.log(targer, key, index)
}
class People {
get username() {
return "xiaofeifei"
}
get password() {
return "123"
}
play(@currency params1:number,params2:number){
return params1 + params2
}
}
const people = new People()
people.play(100,200)