开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情
前言
最近在学习TS,感觉学习TS的最好方法就是对照编译之后的JS源码进行比较学习,这样就容易理解TS语法的功能~~~
这篇文章就讲一讲TS中的装饰器吧,若要启用实验性的装饰器特性,必须tsconfig.json里启用experimentalDecorators编译器选项!!!
装饰器模式
装饰器模式重点在于装饰二字,在我们平时也会经常遇到这样的场景,举一个小例子,你新买一个手机,因为怕手机会被摔坏,这个时候你要去再给他配一个手机壳,那么这个手机壳就是用来装饰手机,也就是手机的装饰器!!!
我们可以通过上面的手机例子得出几个关于装饰器的结论:
- 被装饰的目标就算失去了装饰器也可以正常使用功能;(手机离开手机壳也可以正常使用)
- 被装饰的目标和装饰器是多对多的关系;(手机可以使用手机壳、钢化膜,手机壳也可以用在多个手机上使用)
使用场景
你应该遇到过这样的场景,当你要在旧代码进行添加新功能时,你发现旧代码中定义了Person类中的属性和方法不满足新的功能和需求,你需要对Person类进行扩充
class Person {
name;
getName() {
return this.name
}
}
如果让你来扩充,你会怎样进行修改喃?是对Person类的属性和方法重新定义?
class Person {
name;
sex;
getName() {
return this.name
}
getSex() {
return this.name
}
}
但是如果有一天产品经理说这个新功能要舍去了,你又把Person类再次定义回去嘛!!!
这显然是很糟糕的写法,我们应该遵守开放封闭原则,不影响旧的Person类的情况下添加新的属性和方法!!!
class Person {
name;
getName() {
return this.name
}
}
// 补充代码
Person.prototype.sex = "未知"
Person.prototype.getSex = function () {
return this.sex
}
这样对Person类进行补充,既不影响以前Person类,又对当前Person类进行了补充,这就是装饰器模式
装饰器是对目标补充,它能够作用于类声明、方法、属性或者参数上
TS中装饰器
- 装饰器是一个函数,通过*@函数名*使用
- 根据装饰器传入的参数分类:
- 普通装饰器(无法传参)
- 装饰器工厂(可以传参)
- 根据装饰器的装饰的目标分类:
- 类装饰器
- 方法装饰器
- 属性装饰器
- 访问器装饰器
- 参数装饰器
- 多个装饰器执行顺序按照从下到上
注:如果你对装饰器的基础有了解,也可以直接跳转到源码学习
装饰器参数
普通装饰器
function addRun(target: any) {
target.prototype.run = function () {
console.log("在奔跑!!!");
}
}
@addRun
class Person { }
(new Person() as any).run() // 在奔跑!!!
装饰器工厂
function addRun(area: string) {
// 外部函数用来接受参数,再返回装饰器函数
return function (target: any) {
target.prototype.run = function () {
// 使用外部函数接受的参数
console.log(`在${area}奔跑!!!`);
}
}
}
@addRun("草地")
class Person { }
(new Person() as any).run() // 在草地奔跑!!!
这里主要对比普通装饰器和装饰器工厂装饰器的函数传参,后面会通过编译之后的JS进行分析
TS无法识别问题
在上的代码中之所以会将new Person()对象断言为any,是因为TS无法识别通过装饰器添加的属性和方法,会有报错提示!!!
也可以通过接口去定义Person的类型
function addRun(target: any) {
target.prototype.run = function () {
console.log("在奔跑!!!");
}
}
interface Person {
run(): void
}
@addRun
class Person { }
new Person().run() // 在奔跑!!!
装饰器分类
类装饰器
- 参数:
- target:类的构造函数
- 返回值:使用返回的类的构造函数替换原来的类
function addRun(target: any) {
// 返回一个新的构造函数,替代原来的构造函数
return class extends target {
area: string = "草地"
run() {
console.log(`在${this.area}奔跑!!!`);
}
}
}
@addRun
class Person { }
(new Person() as any).run() // 在奔跑!!!
方法装饰器
- 参数
- target:对于静态方法来说是类的构造函数,对于实例方法是类的原型对象
- fieldName:方法名称
- descriptor:方法的属性描述符
- 返回值:根据返回的属性描述符重新定义方法的属性描述符
修饰实例方法
function addRun(target: any, fieldName: string, descriptor: PropertyDescriptor) {
// target: 类的原型对象
// 最初的run方法
let _run = target[fieldName]
return {
// 给run方法添加补充
value(...arg) {
console.log("开始运动了!!!");
_run.apply(this, arg)
}
}
}
class Person {
@addRun
run() {
console.log("在奔跑!!!");
}
}
new Person().run()
/**
* 开始运动了!!!
* 在奔跑!!!
*/
修饰静态成员
function addRun(target: any, fieldName: string, descriptor: PropertyDescriptor) {
// target: 类的构造函数
// 最初的run方法
let _run = target[fieldName]
return {
// 给run方法添加补充
value(...arg) {
console.log("开始运动了!!!");
_run.apply(this, arg)
}
}
}
class Person {
@addRun
static run() {
console.log("在奔跑!!!");
}
}
Person.run()
/**
* 开始运动了!!!
* 在奔跑!!!
*/
访问器装饰器
访问器本质和方法时一样的,所以参数和返回值也是一样的
function addArea(target: any, fieldName: string, descriptor: PropertyDescriptor) {
// target:类的构造函数
let _value = target[fieldName]
return {
get(...arg): string {
return `在${_value}奔跑!!!`
}
}
}
class Person {
private static _area: string = "草地"
@addArea
static get run() {
return this._area
}
}
console.log(Person.run); // 在草地奔跑!!!
属性修饰器
先了解一下属性和方法的区别
class Person {
name: string = "李白"
run() {
console.log("在奔跑!!!");
}
}
上面代码换成ES5的代码
function Person() {
this.name = "李白"
}
Person.prototype.run = function () {
console.log("在奔跑!!!");
}
属性是在实例化的才会被赋值的,所以在类声明阶段,也就拿不到属性的描述符,修饰器也就没有第三个参数,返回值也就没什么意义了!!!
静态属性虽然会在类声明阶段就赋值,但是TS可能为了同一参数,在第三个参数也没有赋值
-
参数
-
target:对于静态属性来说是类的构造函数,对于实例属性是类的原型对象
-
fieldName:属性名称
-
-
返回值:无
function show(target: any, fieldName: string) { }
class Person {
@show
name: string;
@show
static age: number;
}
参数装饰器
函数参数和属性修饰符是一样的,都在声明阶段没有赋值,所以无法拿到属性的描述符
- 参数
- target:对于静态属性来说是类的构造函数,对于实例属性是类的原型对象
- fieldName:函数的名称
- paramIndex:参数在函数参数列表中的索引
function show(target: any, fieldName: string, paramIndex: number) {
// target:类的原型对象
// fieldName:函数的名称
// paramIndex: 参数在函数参数列表中的索引
}
class Person {
run(@show area: string) { }
}
装饰器执行顺序
function decorator1(target: any) {
console.log("decorator1执行了!!!");
}
function decorator2(target: any) {
console.log("decorator2执行了!!!");
}
@decorator1
@decorator2
class Person { }
/**
* decorator2执行了!!!
* decorator1执行了!!!
*/
执行顺序是decorator2 => decorator1,也就是先执行靠近Person的装饰器,也可以理解为
decorator1(decorator2(Person))
装饰器执行顺序:自下向上
源码学习
以类装饰器为例子吧
function addRun(target: any) { }
function addName(target: any) { }
@addRun
@addName
class Person { }
看一下编译之后的JS的代码:
装饰器是通过调用__decorate函数挂载到被装饰目标上的
注:这里默认Reflect不存在,方便理解__decorate的实现原理
/**
* 装饰器挂载到目标对象上
*
* @param {Array<Function>} decorators // 装饰器数组
* @param {Object} target // 目标对象( 类的构造函数、类的原型对象)
* @param {string} [key] // 名称(属性名称、方法名称)
* @param {(PropertyDescriptor | null)} [desc] // 属性描述符
* @return {(Object | void)}
*/
function __decorate(decorators, target, key, desc) {
let paramLen = arguments.length // 获取参数的长度
let result = null // 定义函数的返回值
if (paramLen < 3) {
result = target
} else {
if (desc === null) {
desc = Object.getOwnPropertyDescriptor(target, key) // 获取属性描述符
}
result = desc
}
/**
* 执行装饰器函数,挂载到目标对象上
*/
let decorator = null
for (var i = decorators.length - 1; i >= 0; i--) {
if (decorator = decorators[i]) {
let _result = null
if (paramLen < 3) {
_result = decorator(result)
} else if (paramLen > 3) {
_result = decorator(target, key, result)
} else {
_result = decorator(target, key)
}
result = _result || result
}
}
/**
* 修改属性描述符
*/
if (paramLen > 3 && result) {
Object.defineProperty(target, key, r)
}
return result;
}
// 装饰器
function addRun(target) { }
function addName(target) { }
//被装饰目标
let Person = class Person { }
// 挂载装饰器
Person = __decorate([
addRun,
addName
], Person);
- __decorate的函数主体是不会改变的,之所以我们实现了不同分类的装饰器,是通过给__decorate函数传入不同的参数,在__decorate内部就行分类判断
- 装饰器所需的参数是__decorate的函数主体传入
以源码的角度理解一下TS中的装饰器吧
装饰器分类
类装饰器
function addRun(target) { } // 装饰器
let Person = class Person { } // 被装饰的类
// __decorate接受的参数为2
// 在执行装饰器时,执行的是: _result = decorator(result) ,装饰器接受1个参数
// __decorate函数的返回值会重新赋值给被装饰的类,用于覆盖以前定义的类
Person = __decorate([
addRun
], Person);
方法装饰器
- 实例方法
function addRun(target, fieldName, descriptor) { } // 装饰器
class Person {
run() { } // 被装饰的实例方法
}
// __decorate接受的参数为4
// 在执行装饰器时,执行的是: _result = decorator(target, key, result) ,装饰器接受3个参数
// __decorate函数的返回值不会被使用,
// 参数长度为4,会执行Object.defineProperty(target, key, r) ,重新定义被装饰的方法的属性描述符
__decorate([
addRun
], Person.prototype, "run", null);
- 静态方法
function addRun(target, fieldName, descriptor) { } // 装饰器
class Person {
static run() { } // 被装饰的静态方法
}
// 和实例方法是一样的,只是__decorate的第二个参数是类的构造函数
__decorate([
addRun
], Person, "run", null);
访问器修饰器
和方法装饰器的执行是一样的,这里就不细讲了
function addArea(target, fieldName, descriptor) { } // 装饰器
class Person {
static get run() { // 被装饰的访问器
return "草地";
}
}
// __decorate接受的参数为4
__decorate([
addArea
], Person, "run", null);
属性修饰器
- 修饰实例属性
function show(target, fieldName) { } // 装饰器
class Person {
constructor() {
this.name = "李白"; // 被装饰的属性
}
}
// void 0的值是undefined,不会计入实参个数,__decorate接受的参数为3
// 在执行装饰器时,执行的是: _result = decorator(target, key) ,装饰器接受2个参数
// __decorate函数的返回值不会被使用,
// 参数长度为3,会执行Object.defineProperty(target, key, r) ,重新定义被装饰的属性的属性描述符
__decorate([
show
], Person.prototype, "name", void 0);
- 修饰静态属性
function show(target, fieldName) { } // 装饰器
class Person { }
Person.age = 18; // 被装饰的属性
// 和修饰实例属性是一样的,只是__decorate的第二个参数是类的构造函数
__decorate([
show
], Person, "age", void 0);
参数修饰器
参数装饰器和其他的装饰器不一样,对装饰器包装了一个__param,这个方法是为了传递索引
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
function show(target, fieldName, paramIndex) { }
class Person {
run(area) { }
}
// 和方法装饰器很相似,区别在于执行装饰器时:_result = decorator(target, key, result) result是索引
__decorate([
__param(0, show)
], Person.prototype, "run", null);
装饰器参数
function add(a, b) {
return function (target) {
target.sum = a + 1;
};
}
let Person = class Person { }
// 先执行add函数获取返回值
Person = __decorate([
add(1, 2) // 返回装饰器函数
], Person);
装饰器执行顺序
function test1() {
console.log("test1函数执行");
return function (target) {
console.log("test1装饰器执行");
}
}
function test2() {
console.log("test2函数执行");
return function (target) {
console.log("test2装饰器执行");
}
}
@test1()
@test2()
class Person { }
执行结果是什么?
test1函数执行
test2函数执行
test2装饰器执
test1装饰器执行
答对了嘛?再配合一下JS代码
function test1() {
console.log("test1函数执行");
return function test1_decorator(target) {
console.log("test1装饰器执行");
};
}
function test2() {
console.log("test1函数执行");
return function test2_decorator(target) {
console.log("test1装饰器执行");
};
}
let Person = class Person {
};
Person = __decorate([
test1(),
test2()
], Person);
先讨论一下装饰器工厂如何执行
-
先执行test1函数返回test1_decorator装饰器函数
-
再执行test2函数返回test2_decorator装饰器函数
-
__decorate参数变为:
Person = __decorate([ test1_decorator, test2_decorator ], Person);
得出结论,在给__decorate传入参数时,会先从上到下执行装饰器工厂的包装函数
再讨论一下__decorate主体执行装饰器的过程
for (var i = decorators.length - 1; i >= 0; i--) {
/** 执行装饰函数 **/
}
decorators就是传入的装饰器数组,会发现在执行decorators数组时,是从后往前执行,也就是在执行装饰器时,是按照从下到上执行装饰器函数