typescript装饰器(decorator)笔记
介绍:
随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在建议征集的第三阶段,但在TypeScript里已做为一项实验性特性予以支持。
官方描述:
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。 装饰器使用@expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
一、背景:
截至目前2023-4-14,装饰器(decorator)的特性还未正式成为js的规范,所以即便现在ts里面可以使用,但是还是作为实验性特性的功能,给予想用的同学使用的机会,但是不排除js正式引入装饰器后,使用方法会修改。所以本篇文章立足于ts的实验性特性的装饰器来展开讲述。随着时间的推移,本文很可能与未来的装饰器使用方法有所出入。
二、正文
笔者理解的装饰器,就是在类上某些地方(类声明,方法,访问符,属性或参数)使用特定的语法,对它的一些属性或方法等进行修改。装饰器其实就是一个函数,然后使用’@函数‘语法,就变成了装饰器。
(1)类装饰器:作用于类声明上,接受一个参数。所以简单点说类装饰器就是一个函数,上面带有且仅有一个参数。
例子1:
function a(target: any) { console.log("我是a装饰器"); } function b(target: any) { console.log("我是b装饰器"); } @a @b class demo { }
运行结果:
可以看到,装饰器函数是从下到上运行的。
例子2:
function a() { console.log("收集a装饰器"); return function(target: any) { console.log("a装饰器运行") } } function b() { console.log("收集b装饰器"); return function(target: any) { console.log("b装饰器运行") } } @a() @b() class demo { }
运行结果:
例子2可以看到,装饰器虽然是从下到上运行的,为什么说是从下到上呢?因为只有:b装饰器运行,a装饰器运行才算是装饰器运行的结果,
而收集a装饰器,收集b装饰器,其实只是收集装饰器的过程。换句话说,就是@a()返回的函数才是a装饰器,@b()返回的函数才是b装饰器
所以装饰器运行的时候的原则(适用于所有装饰器):
- 由上至下依次对装饰器表达式求值。
- 求值的结果会被当作函数,由下至上依次调用。
例子3:
上面举例了两个例子,似乎没有说到类装饰器里面的参数是谁?官方文档上是说,类装饰器的参数是【类的构造函数】,但是下面的例子,笔者打印的结果却是【类本身】
function testDecorator(target: any) { console.log(target, target === Test) // 输出 [class Test] true } @testDecorator class Test { name: string; constructor(name: string) { this.name =name } } new Test('')
运行结果:
这是笔者在思否社区的提问:
总结:
1.类装饰器其实就是作用于类上的一个函数,使用【@函数名】在类声明上标识。
2.类装饰器有且仅有一个参数。
3.类装饰器参数指的是【类的构造函数】,也可以说其参数指的是类本身。
看过了上面的提问应该知道了,js类的构造函数其实就等于类本身,这可能是js区别于其他一些真正面向对象语言的不同之处。
(2)方法装饰器:
function testDecorator(target: any, key: string, descriptor: PropertyDescriptor) { console.log("key:",key,",target:", target, ",target == Test.prototype:", target == Test.prototype, ",target == Test:", target == Test); descriptor.value = function() { return '6666' } } class Test { name: string; constructor(name: string) { this.name = name; } @testDecorator getName() { return this.name; } @testDecorator static staticGetName() { console.log("静态方法") } } const test = new Test('我是test'); console.log("test.getName():",test.getName());
运行结果:
上面打印结果两点值得关注:
1.前两个打印信息表明:普通方法, target参数对应的是 类的prototype;静态方法, target参数对应的是 类。
2.第三条打印信息表明第三个参数descriptor的作用等同于object.defineProperty参数的descriptor,由于修改了descriptor.value,所以返回的值不是 "我是test”,变成了 “6666”
(3)访问符装饰器
// 访问符装饰器 function testDecorator(target: any, key: string, descriptor: PropertyDescriptor) { console.log("key:",key,",target:", target, ",target == Test.prototype:", target == Test.prototype, ",target == Test:", target == Test); const originalSet:(this: PropertyDescriptor, args_0: string) => void = descriptor.set!; // 保存原始的 set 方法 descriptor.set = function(name: string) { name = "set装饰器:" + name; originalSet.call(this, name); // 使用 .call(this, ...) 调用原始的 set 方法 } // descriptor.get = function() { // 和set一样,也可以修改get的值 // console.log("this:",this, "this==Test:", this == Test) // return 'hhghghgh' // } } // 类 class Test { private _name: string; static staticName: string = 'static'; constructor(name: string) { this._name = name; } get name() { return this._name; } @testDecorator // 希望通过装饰器,对this._name做一点处理 set name(name: string) { console.log("this:",this, this == test) this._name = name; } @testDecorator static get n() { return this.staticName; } } const test = new Test('111'); test.name = '222'; // 原来值为111,现在修改为222 console.log(test.name); // 由于在装饰器中,descriptor.set做了处理,所以输出: set装饰器:222 console.log(Test.n);
可以看到,访问符装饰器参数与方法一样。
但是需要注意两点。
1.同一个访问符装饰器只能修饰set与get中的一个,看下面例子:
class Test { private _name: string; static staticName: string = 'static'; constructor(name: string) { this._name = name; } @testDecorator get name() { return this._name; } @testDecorator // 由于上面已经对get name使用了testDecorator,所以set name 不可以再用 set name(name: string) { console.log("this:",this, this == test) this._name = name; } }
具体原因我认为应该是一个装饰器修饰其中一个已经足够了,不需要太多了,
例子:下面testDecorator中已经重写了get 和 set。所以在get name 或者set name上用一个就够了。
// 访问符装饰器 function testDecorator(target: any, key: string, descriptor: PropertyDescriptor) {const originalSet:(this: PropertyDescriptor, args_0: string) => void = descriptor.set!; // 保存原始的 set 方法 descriptor.set = function(name: string) { name = "set装饰器:" + name; originalSet.call(this, name); // 使用 .call(this, ...) 调用原始的 set 方法 } descriptor.get = function() { // 和set一样,也可以修改get的值 console.log("this:",this, "this==Test:", this == Test) return 'hhghghgh' } } // 类 class Test { private _name: string; static staticName: string = 'static'; constructor(name: string) { this._name = name; } get name() { return this._name; } @testDecorator // 希望通过装饰器,对this._name做一点处理 set name(name: string) { console.log("this:",this, this == test) this._name = name; } @testDecorator static get n() { return this.staticName; } }
2.访问符装饰器中不能对 descriptor.writable和descriptor.value进行修改,道理其实很简单:
descriptor.writable是控制对象可不可以写,既然访问符有了set了,默认就是可写了,那么再对descriptor.writable进行修改就冲突了,而且不管descriptor.writable等于true还是false都会报错;
descriptor.value是控制对象的值,既然访问符有了get了,默认就是可读了,那么再对descriptor.value进行修改就冲突了,而且不管descriptor.value等于什么都会报错。
例子:
// 访问符装饰器 function testDecorator(target: any, key: string, descriptor: PropertyDescriptor) { console.log("key:",key,",target:", target, ",target == Test.prototype:", target == Test.prototype, ",target == Test:", target == Test); const originalSet:(this: PropertyDescriptor, args_0: string) => void = descriptor.set!; // 保存原始的 set 方法 descriptor.writable = false; // 不管值为true或者false都会报错 descriptor.value = '666'; // 不管值为多少都会报错 descriptor.set = function(name: string) { name = "set装饰器:" + name; // console.log("this:",this, this == test, target, target.constructor == Test) // this指向当前实例对象 originalSet.call(this, name); // 使用 .call(this, ...) 调用原始的 set 方法 } descriptor.get = function() { // 和set一样,也可以修改get的值 console.log("this:",this, "this==Test:", this == Test) return 'hhghghgh' } } // 类 class Test { private _name: string; static staticName: string = 'static'; constructor(name: string) { this._name = name; } get name() { return this._name; } @testDecorator // 希望通过装饰器,对this._name做一点处理 set name(name: string) { console.log("this:",this, this == test) this._name = name; } @testDecorator static get n() { return this.staticName; } }
运行:
(4)参数装饰器
target参数对应的是 (普通方法:类的prototype,静态方法:类), key:对应方法名 , paramIndex: 参数的下标
// 参数装饰器 function testDecorator(target: any, key: string, paramIndex: number) {
} class Test { private _name: string; constructor(name: string) { this._name = name; } getInfo(@testDecorator name: string) { console.log("name:",name) } static getStaticFn(@testDecorator name: string) { console.log(name) } }
运行结果:
(5)属性装饰器
例子:target参数对应的是 (普通属性:类的prototype,静态属性:类), key:对应方法名 ,
function testDecorator(target: any, key: string) { console.log(",key:",key,",target:", target, ",target == Test.prototype:", target == Test.prototype, ",target == Test:", target == Test); } class Test { @testDecorator name!: string; @testDecorator static staticname: string = '666'; }
运行结果: