一文搞懂 ts 装饰器
最近在开发一个 web component 组件库,使用的是 lit 库,由于 lit 使用 ts 开发时定义 state 和 props 都是使用的装饰器语法,又想起前几个月跳槽面试时有被问到装饰器,由于没用过所以这个问题答得不是很好,因特意梳理一下装饰器相关的知识点。
装饰器概念
装饰器是 typescript 中的一种特殊的类型声明,实际上 js 装饰器目前也已经提案了,目前处于 stage2 的阶段。顾名思义,装饰器起到了对数据的装饰(加工的)作用,可以被附加到类、方法、访问器、属性、参数上。
装饰器使用 @expression
的形式使用,expression
是一个函数,会在运行时被调用,被装饰的数据会作为参数传入到这个函数中。
如下述代码中的 decorator
就是一个装饰器函数,接收一个 target 参数,decorator
装饰器修饰了 Animal
这个类,那么 Animal
类就被作为 target 参数传入到了 decorator
函数中。
function decorator(target: any) {
target.say = function () {
console.log('hello!')
}
}
@decorator
class Animal {
static say: Function;
constructor() {
}
}
Animal.say() // hello!
装饰器工厂
上面装饰器其实有一种缺点,就是除了传入要装饰的数据之外,装饰器本身的功能不能通过传参去自定义,想要通过传参自定义装饰器的功能,我们可以使用装饰器工厂。
装饰器工厂通过 @expression(args)
形式使用,装饰器工厂中的 expression
会返回一个装饰器函数,args
是用户想自定义传入的参数。
如下述代码中 giveSay
就是一个装饰器工厂,接收一个参数 name,通过这个参数用户可以传入自定义想传入的数据。返回的装饰器函数接收 target 参数,使用装饰器工厂所修饰的数据(如下面代码中的 Animal1
和 Animal2
) 会被作为 target 参数传入。
function giveSay(name: string) {
return function(target: any) {
target.say = function () {
console.log('hello! My name is ' + name)
}
}
}
@giveSay('Yuanbao')
class Animal1 {
static say: Function;
constructor() {
}
}
Animal1.say() // hello! My name is Yuanbao
@giveSay('Facai')
class Animal2 {
static say: Function;
constructor() {
}
}
Animal2.say() // hello! My name is Facai
装饰器类型
类装饰器
类装饰器在类声明之前被声明(紧靠着类声明),它应用于类构造函数,可以用来监视,修改或替换类定义。单纯通过文字可能比较难理解,下面我们通过几个例子来看类装饰器的应用。
监视及修改类
如下代码,我们可以打印一下当 decorator
装饰器装饰一个类 Animal
时,接收到的参数:
function decorator(...args) {
console.log(args)
}
@decorator
class Animal {
name = 'cat';
}
通过打印结果我们可以得知,装饰器作用于类时,接收到的参数就是类的构造函数,从而我们可以拿到类构造函数里面的参数及方法,起到监视类的作用。
同理,装饰器的第一个参数是类的构造函数,那么也可以对类进行修改,覆盖、添加或者删除类里面的属性及方法:
function decorator(target: any) {
target.say = function () {
console.log('hello!')
}
target.run = function () {
console.log('I am running.')
}
}
@decorator
class Animal {
static say: Function;
constructor() {
}
}
Animal.say() // hello!
Animal.run() // I am running.
替换类定义
我们看一下当类装饰器有返回值的情况下会发生什么:
function returnStr(target) {
return 'hello world~';
}
function returnClass(target) {
return target;
}
@returnStr
class ClassA { }
@returnClass
class ClassB { }
console.log('ClassA:', ClassA); // ClassA: hello world~
console.log('ClassB:', ClassB); // ClassB: ƒ ClassB()
从打印结果可知,当类装饰器有返回值时,返回的值会替换原有的类的定义。
方法/访问器装饰器
将类方法和访问器装饰器放在一起讲,是因为这俩的装饰器用法相同,类方法/访问器装饰器接收三个参数:
- 参数1:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 参数2:成员的名字。
- 参数3:成员的属性描述符,详见 descriptor
JS 基础比较好的同学,看到这三个参数,可能立马会联想到 Object.defineProperty()
这个方法,接收的参数是一模一样的。
function readonly(target, name, descriptor){
descriptor.writable = false
return descriptor;
}
class Animal {
@readonly
name() { return 'PeiQi' }
}
console.log(Object.getOwnPropertyDescriptor(Animal, 'name'))
// { value:"Animal", writable:false, enumerable:false, configurable:true}
如果一个类方法装饰器有返回值,则返回值会被用作方法的属性描述符:
function readonly(target, name, descriptor){
return { writable: false }
}
class Animal {
@readonly
name() { return 'PeiQi' }
}
console.log(Object.getOwnPropertyDescriptor(Animal, 'name'))
// { value:"Animal", writable:false, enumerable:false, configurable:true}
属性装饰器
属性装饰器用于装饰类属性,接收 2 个参数:
- 参数1:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 参数2:成员的名字。
属性描述符不会做为参数传入属性装饰器,这与 TypeScript 是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
const keys = []
function recordProperty(target, name){
keys.push(name)
}
class Animal {
@recordProperty
name = 'zlx'
@recordProperty
age = 25
}参数装饰器用于修饰类方法的参数
console.log(keys)
// ["name", "age"]
参数装饰器
参数装饰器用于修饰类方法的参数,接收 3 个参数:
- 参数1:对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 参数2:成员的名字。
- 参数3:参数在函数参数列表中的索引。
参数装饰器只能用来监视一个方法的参数是否被传入,返回值会被忽略。
function ageDecorator(target, name, index){
console.log(name, index)
}
class Animal {
say(name: string, @ageDecorator age?: number) {
}
}
// say 1
装饰器执行顺序
- 首先执行实例相关:参数装饰器 > 方法装饰器 > 访问器装饰器 > 属性装饰器
- 然后执行静态相关:参数装饰器 > 方法装饰器 > 访问器装饰器 > 属性装饰器
- 然后执行构造函数的参数装饰器
- 最后是类装饰器
- 多个装饰器装饰同一个数据时,从下往上依次执行
function staticParamsDecorator(target, name, index) {
console.log('static params decorator')
}
function staticFuncDecorator(target, name, descriptor) {
console.log('static func decorator')
}
function staticPropertyDecorator(target, name) {
console.log('static property decorator')
}
function instanceParamsDecorator(target, name, index) {
console.log('instance params decorator')
}
function instanceFuncDecorator(target, name, descriptor) {
console.log('instance func decorator')
}
function instancePropertyDecorator(target, name) {
console.log('instance property decorator')
}
function constructorParamsDecorator(target, name, index) {
console.log('constructor params decorator')
}
function classDecorator1(target) {
console.log('class decorator1')
}
function classDecorator2(target) {
console.log('class decorator2')
}
@classDecorator1
@classDecorator2
class Animal {
constructor(@constructorParamsDecorator options) {
}
@staticPropertyDecorator
static Name = 'zlx'
@staticFuncDecorator
static Say(@staticParamsDecorator name: string) {
}
@instancePropertyDecorator
age = 11
@instanceFuncDecorator
run(@instanceParamsDecorator time: number) {
}
}
// instance property decorator
// instance params decorator
// instance func decorator
// static property decorator
// static params decorator
// static func decorator
// constructor params decorator
// class decorator2
// class decorator1
转载自:https://juejin.cn/post/7194643388262514745