装饰器
# 前言
最近在学习
nest
,学习过程中发现满屏的装饰器
。于是带着好奇心去学习了一下es6
的装饰器语法。
# 什么是装饰器?
装饰器是大多数语言中都有的一种语法,它的作用主要是用来包装
类
,属性
,方法
(在js中由于存在函数提升
的问题所以不能用来包装函数
)。在不改变他们原来作用的情况下扩展
一些其他的功能,目前还没有作为新特性正式发布到js
中,所以使用的时候需要通过Babel
去转译。
# 装饰器API
装饰器本质上是一个函数
,通过@+函数名
的方式将这个函数
作为装饰器使用。其实他就是高阶函数
的变种,使之语法更加简洁,代码结构更加的清晰。
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
private?: boolean;
static?: boolean;
addInitializer?(initializer: () => void): void;
}) => Output | void;
2
3
4
5
6
7
8
9
10
11
装饰器函数接收两个
参数(装饰器作用于代码运行时,这两个参数由js引擎
提供)。
value
:所需要装饰的值,某些情况下可能是undefined
(装饰属性时)context
:上下文信息对象kind
:字符串,表示装饰的目标的类型,可能的取值有class、method、getter、setter、field、accessor
。name
:被装饰的值的名称access
:对象,包含访问这个值的方法,即存值器和取值器。static
: 布尔值,该值是否为静态元素。private
:布尔值,该值是否为私有元素。addInitializer
:函数,允许用户增加初始化逻辑。
装饰器的返回值是一个新的装饰器,也可以不返回任何值。
装饰器的执行步骤如下:
- 先执行各个装饰器,从上到下,从左到右的顺序执行(允许同时使用多个装饰器装饰一个目标)
- 调用方法装饰器
- 调用类装饰器
步骤一比较好理解,就是同时存在多个装饰器的时候会按顺序依次先执行所有装饰器,再执行被装饰的目标。后面两个目前我也没太搞清楚
# 类装饰器
type ClassDecorator = (value: Function, context: {
kind: "class";
name: string | undefined;
addInitializer(initializer: () => void): void;
}) => Function | void;
2
3
4
5
类装饰器的第一个参数就是被装饰的目标类
,第二个参数是装饰器的上下文对象
,如果被装饰的是一个匿名类
,name
属性就是undefined
。
类装饰器可以返回一个新的类
,也可以不返回任何值。
下面通过一个小小的例子来认识一下他
function AddName(target){
target.name = '我叫大黄,被装饰到目标Dog类上啦!'
}
@AddName
class Dog{
}
const dog = new Dog()
dog.name // 我叫大黄,被装饰到目标Dog类上啦!
2
3
4
5
6
7
8
9
10
11
12
上面的例子中我们写了一个添加名称的装饰器,并将它作用于Dog
这个类上。这就是最简单的使用,当然我们可以再改造一下,让这个装饰器支持可扩展的功能
function AddName(name){
return function (target){
target.name = `我叫${name},被装饰到目标Dog类上啦!`
}
}
@AddName('小白')
class Dog{
}
const dog = new Dog()
dog.name // 我叫小白,被装饰到目标Dog类上啦!
2
3
4
5
6
7
8
9
10
11
12
13
还记得上文在介绍装饰器的时候说过,装饰器可以返回一个新的装饰器
吗。在这个例子中,我们将装饰器又进行了一层包装,让她可以接受外界传入的参数,并且在返回新的装饰器
里去使用这个参数,他的执行步骤如下。
首先调用AddName
函数,传入name
参数,然后AddName
函数返回了一个新的装饰器
,这个装饰器会接受js引擎
传入的装饰器专属的两个参数
,通过这样一包装。我们的装饰器就可以干更多的事情
接下来再来试试返回一个新的类
function AddName(target) {
return class hashiqi extends target {
constructor(...arg) {
super(...arg);
console.log(`我是哈士奇类,继承于被装饰的类,我的名字叫${args.join(", ")}`)
}
}
}
@AddName
class Dog {
}
const dog = new Dog('拆家大王')
dog.name // 我是哈士奇类,继承于被装饰的类,我的名字叫拆家大王
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这个例子中,我们在装饰器里返回了一个新的类
,他会代替
掉被装饰的类,也就是说被装饰的那个类就不会
再执行
了,我们执行的是装饰器返回的新的类
。
小结
这一小节中我们通过几个简单的例子认识了类装饰器,并且将他常用的几种写法罗列了出来。包括只装饰目标类,装饰器返回一个全新的类代替目标类,以及装饰器如何接受参数进行更自由的扩展。下面一小节将来认识一下方法装饰器
# 方法装饰器
看到这里是不是觉得有点疑惑?上文明明都说了不能装饰函数
不能装饰函数
,这不就啪啪打脸了吗?别着急,这里说的方法装饰器,装饰的确实是函数,但是他装饰的是类里面的函数啦。本质上来说他可以算作属性装饰器的一种!
type ClassMethodDecorator = (value: Function, context: {
kind: "method";
name: string | symbol;
access: { get(): unknown };
static: boolean;
private: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;
2
3
4
5
6
7
8
方法装饰器和类装饰器一样,可以返回一个新的方法代替
原来的方法,也可以什么都不返回,除此之外返回其他的值会报错。
function log(target,{name}){
console.log('执行了'+name+'方法')
}
class C {
@log
toString() {
return 'C';
}
}
new C().toString() //执行了toString方法
2
3
4
5
6
7
8
9
10
11
12
通过这么一个简单的包装,我们就可以做出一个埋点打印功能。
function log(target,{name}){
return function (name){
console.log('我是替代原来的方法的新方法'+name)
}
}
class C {
@log
toString() {
return 'C';
}
}
new C().toString() //我是替代原来的方法的新方法toString
2
3
4
5
6
7
8
9
10
11
12
13
也可以和类一样返回一个新方法替代原来的。
# 属性装饰器
下面来认识一下类的属性装饰器
type ClassFieldDecorator = (value: undefined, context: {
kind: "field";
name: string | symbol;
access: { get(): unknown, set(value: unknown): void };
static: boolean;
private: boolean;
}) => (initialValue: unknown) => unknown | void;
2
3
4
5
6
7
属性装饰器的第一个参数就不再是属性本身了,他的第一个参数是undefined
。使用者可以通过返回一个初始化函数来去访问当前属性的初始值,当该属性被赋值时 初始化函数会
自动执行
,他会收到属性的初始值
,并且需要返回一个值来代替
当前装饰的属性,或者不返回任何东西。除了这样中情况外返回其余内容都会报错
下面来看一个小例子
function getName(value,{kind,name}){
return function (initVal){
console.log(`获取到${name}属性得初始值:${initVal}`)
return initVal
}
}
class Dog{
@getName name='小黄';
}
new Dog().name = '小白' //获取到name属性得初始值:小白
2
3
4
5
6
7
8
9
10
11
上述例子中,我们通过一个装饰器装饰类得name
属性,并在装饰器中返回了一个新的函数,这个函数得返回值会作为name
得值。
# 为什么不能用在普通函数上?
上面介绍完了几个常见的装饰器的使用,接下来我们聊聊为什么装饰器不能用在普通函数
上。
我们都知道,在js中存在变量提
升的问题,而不单单时变量,就连函数的声明都是会提升的
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
2
3
4
5
6
7
8
9
上述代码在执行过程中实际时这样的
var counter
var add
@add
function foo() {
}
counter = 0
add = function () {
counter++;
};
2
3
4
5
6
7
8
9
10
11
12
由于函数提升的问题,add
装饰器还没有被赋值的时候他就已经作为装饰器被执行了。这会导致我们意想不到的bug
出现。
那细心的小伙伴可能又会问,var
有变量提升,那如果我换成let
或者const
呢?
如果换成let
:
let counter = 0
let add = function () {
counter++;
};
@add
function foo() {
}
2
3
4
5
6
7
8
9
10
他的执行顺序如下
@add
function foo() {
}
let counter = 0
let add = function () {
counter++;
};
2
3
4
5
6
7
8
9
由于提升的问题,函数还是跑到了装饰器被赋值
之前。总的来说,就是因为普通的函数存在函数提升,会被放在最前面执行,而装饰器函数绕不过声明
,赋值
这一步。
也就意味着装饰器的赋值这一步操作永远都会在函数提升
之后,这样一来就会导致普通函数被声明的之后他的装饰器是没值
的。而类不会提升
,所以他不会有这个问题存在。
如果非要装饰函数的话还是通过高阶函数
的方式去完成吧。
# 总结
总的来说装饰器其实可以被看作一个高阶函数
,他通过包装某个函数或者类并且不改变他原来得功能得情况下去扩展一些其他得功能。而装饰器的参数有两个,第一个是被装饰的目标
,第二个是装饰器的上下文
。
这两个参数会根据装饰目标的不同类型有不同的值。装饰器在装饰不同类型的值的时候大同小异,下面把上文中所提到的类型都做一个总结:
装饰类的时候
:- 装饰器的返回值必须是一个
新的类
或者什么都不返回,否则报错。当返回一个新的类的时候这个类会代替
被装饰的类 - 装饰器的第一个参数为被装饰的类
本身
- 装饰器的返回值必须是一个
装饰类的方法的时候
:- 装饰器的返回值必须是一个新的函数或者什么都不返回,否则报错。当返回一个新的函数的时候这个函数会代替被装饰的方法
- 装饰器的第一个参数是被装饰的方法
本身
装饰类的属性的时候
:- 装饰器可以返回一个
初始化函数
或者什么都不返回,当返回一个初始化函数的时候,这个函数必须返回一个值
。初始化函数会在属性被赋值时调用,此时会传入当前属性的值作为参数给初始化函数。 - 装饰器的第一个参数时
undefined
,也就是说属性装饰器要想拿到属性的值必须返回一个初始化函数
- 装饰器可以返回一个