装饰器是什么
装饰器(Decorator)本质上就是一个函数,是一种设计模式的实现,它可以作用在类,属性,方法,方法参数上,在不修改原本程序的情况下,执行一些特定的操作或者提供一些额外的功能。可以想象成送给别人生日礼物,其中礼物是千差万别的,但是包装礼物的过程可以是一样的,比如用彩纸包起礼物,再系个丝带等就是装饰器程序做的事。那这么做的好处是什么呢,为啥要多出个装饰器的概念呢?实际上,这么做可以将某些通用的流程分离出去单独管理,将通用的功能抽离出去,减少代码量。
环境准备
- 创建一个空的文件夹,通过 npm 进行初始化
新建项目:study-decorator 文件夹
cd 到 study-decorator 中使用 npm init -y 初始化项目
- 创建 src 文件夹,dist 文件夹
- 安装 typescript 依赖,创建并修改 tsconfig.json 文件
安装 typescript 依赖: npm i typescript
生成 tsconfig.json 文件: tsc --init
配置 tsconfig.json 文件:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
- 安装 ts-node 依赖
使用 npm i ts-node -D 命令安装 ts-node,用来执行 ts 文件
- 配置 package.json 文件
在 package.json 文件的 script 中增加两条脚本命令,tsc 用来将写好的 ts 代码编译成 js 代码,-w 就是 watch 变化,有变化就重新编译,dev 用来运行写好的 ts 代码:
"tsc": "tsc -w --experimentalDecorators",
"dev": "ts-node ./src/index.ts"
- 测试环境是否可以使用
1.在 src 目录下新建 index.ts 文件,并写入内容
2.在命令行执行 npm run tsc 命令
3.可以看到在 dist 目录中生成了编译后的 index.js 文件
4.改动 src 下的 index.ts 文件保存后,可以看到 dist 目录下的 index.js 文件也重新编译了
5.在命令行执行 npm run dev 命令,可以直接运行写好的 index.ts 文件
装饰器的本质
装饰器本质上其实就是一个函数,也可以说是一个语法糖,下图就是个普通的装饰器
装饰器的类别
装饰器通过作用的对象不同而有不同的分类,包括类装饰器,属性装饰器,方法装饰器,方法参数装饰器,用来实现不同的功能逻辑。
类装饰器
- 顾名思义,类装饰器就是将装饰器函数作用在类上
- 写一个不带参数的装饰器
/**
* 不带参数的类装饰器
* @param constructor
*/
export function classDecorator(constructor: any) {
constructor.prototype.sayHi = function () {
console.log('Hi~~');
}
}
- 写一个 People 类使用这个装饰器
import { classDecorator } from './decorator/classDecorator'
@classDecorator
class People {
name: string
constructor(name: string) {
this.name = name
}
}
const p = new People('小红');
(p as any).sayHi()
- 所以装饰器语法就是 @ 加上装饰器函数的名字,很简单对不对,我们使用这个简单的装饰器在传递进来的原型上增加了一个 sayHi 方法,结果使用 People 的实例对象就可以调用这个方法,当然这里我们还需要将 People 的实例断言成 any 对象,不然 ts 会认为 People 没有这个方法而报错,接下来我们会改进这个问题。
/**
*
* @returns 返回一个装饰器函数,参数是个构造函数
*/
export function classDecorator() {
return function <T extends new (...args: any[]) => any>(constructor: T) {
return class extends constructor {
sayHi() {
console.log('我又来 sayHi 了~~');
}
}
}
}
- 我们将之前的装饰器函数改造一下,使之前的函数返回一个装饰器函数,而这个装饰器函数接收一个参数是 T 类型的 new (...args: any[]) => any,这是一个构造函数,接着返回一个匿名类继承这个构造函数。然后我们通过传递一个类来构造 People 对象,可以看到,这样通过 People 实例调用在构造器中定义的方法就不需要断言了,而且还有语法提示。
const People = classDecorator()(
class {
name: string;
constructor(name: string) {
this.name = name
}
}
)
const p = new People('小红');
p.sayHi()
- 有参数的装饰器只需要在之前的函数中返回一个函数就可以了,如下所示:
/**
* 这个一个有参数的装饰器函数
* @param param 传递的参数
* @returns
*/
export function classDecoratorWithParam(param: string) {
return function (constructor: any) {
constructor.prototype.showParam = function () {
console.log(`传递的参数为:${param}`);
}
}
}
@classDecoratorWithParam('Test')
class Test { }
const t = new Test();
(t as any).showParam()
属性装饰器
/**
* 属性装饰器
* @param target 目标属性的原型
* @param key 目标属性 key
*/
export function attributeDecorator(target: any, key: string) {
target[key] = '小花'
}
- 上面的代码里定义了一个属性装饰器,然后在原型上增加一个名为 key 的属性
class Test {
@attributeDecorator
name = '小明'
}
const t = new Test()
console.log(t.name); // 输出 小明
console.log((t as any).__proto__.name); // 输出 小花
- 测试后发现通过属性拦截器,我们在 Test 的原型上也增加了 name 属性并赋值了
方法装饰器
/**
*
* @param target 1. 普通方法是原型对象 2.静态方法是类的构造函数
* @param key 方法名
* @param descriptor 属性描述符
* {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
*/
export function methodDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
console.log('target: ', target);
console.log('key: ', key);
console.log('descriptor.value: ', descriptor.value);
// 这里对应着传递进来的方法
descriptor.value = function () {
return 'descriptor'
}
}
- 上面定义了一个方法装饰器,和类,属性装饰器大同小异,唯一要注意的就是,装饰器作用在普通方法和静态方法上时,target 的值是不一样的
class Test {
name: string
constructor(name: string) {
this.name = name
}
@methodDecorator
getName() {
return this.name
}
}
const t = new Test('小明')
console.log(t.getName()); // 这里打印出 descriptor
- 通过测试,可以看到 getName 方法在装饰器中被重写了
方法参数装饰器
/**
*
* @param target 原型
* @param method 方法名
* @param paramIndex 参数所在的位置
*/
export function paramDecorator(target: any, method: string, paramIndex: number) {
console.log('paramDecorator target: ', target);
console.log('paramDecorator method: ', method);
console.log('paramDecorator paramIndex: ', paramIndex);
}
- 方法装饰器和其它装饰器的区别就是最后一个参数代表装饰的参数在方法参数中的位置,从 0 开始,类似于下标
class Test {
getInfo(name: string, @paramDecorator age: number) {
return `${name},${age}`
}
}
const t = new Test()
console.log(t.getInfo('小明', 5));
装饰器的源码
通过上面几个简单的装饰器函数可以看出来,装饰器的作用就是类似于拦截的功能,可以悄无声息的在不改变原函数的情况下增加一些额外的东西,那么这究竟是怎么做到的,我们可以通过源码来探究一下。
- 写一个简单的装饰器,然后通过我们在 package.json 文件中写的脚本命令 npm run tsc 来执行编译一下,如果报了 TS2688: Cannot find type definition file for 'node' 这个错误,需要在安装一下 npm install @types/node --save-dev
// index.ts
// 简单的带参数的装饰器
function firstDecorator(param: any) {
return function (target: any) {
console.log('firstDecorator: ', target);
}
}
// 测试类
@firstDecorator('这是个参数')
class Test {
name: string
constructor(name: string) {
this.name = name
}
sayHi() {
console.log(`Hi,${this.name}`);
}
}
const t = new Test('小红')
t.sayHi()
- 接下来我们看一下编译过后的 index.js 文件
"use strict";
// __decorate 函数: 第一次执行的时候 this 上并没有 __decorate 函数,所以不会往下走
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
// 这里只有两个参数:
// decorators 是 数组 【】
// target 是 Test 构造函数
// key 和 desc 都不存在
console.log(arguments);
var c = arguments.length,
// c 的个数是 2,所以 r 是传递进来的 Test
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d;
// Reflect.decorate 是 undefined 所以会走到 else 逻辑
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
// decorators 数组为 【firstDecorator 返回的函数,undefined】
// d 就等于 firstDecorator 返回的函数,也就是真正的装饰器函数
if ((d = decorators[i]))
// 执行 c < 3 的逻辑因为参数只有两个,所以装饰器函数会接收到一个参数 r,就是我们在函数中接收到的 Test 构造函数
// 由于我们的装饰器函数什么也没返回,所以 r 依然是 Test
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
// 元数据: 第一次执行的时候 this 上并没有 __metadata 函数,所以也会不往下走
var __metadata =
(this && this.__metadata) ||
function (k, v) {
// 执行完 firstDecorator 后执行
if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
return Reflect.metadata(k, v);
};
// 包裹装饰器的函数
function firstDecorator(param) {
// 装饰器函数: 最先执行的函数
return function (target) {
console.log("firstDecorator: ", target);
};
}
// 测试类
var Test = /** @class */ (function () {
function Test(name) {
this.name = name;
}
Test.prototype.sayHi = function () {
console.log("Hi,".concat(this.name));
};
// 这个函数传递了两个参数,第一个参数是装饰器的数组,第二个参数是 Test 的构造函数
// firstDecorator 函数最先执行,然后返回一个装饰器函数
Test = __decorate(
[firstDecorator("这是个参数"), __metadata("design:paramtypes", [String])],
Test
);
return Test;
})();
var t = new Test("小红");
t.sayHi();
- 可以看到编译过后的 js 代码中主要分为四个部分:
- 我们写的测试类
- 包裹着装饰器函数的函数
- __decorate 函数
- 元数据函数
- 上面代码函数的执行顺序:
- firstDecorator 函数,并且返回一个函数
- __metadata 这个函数只是储存了一些元数据信息
- __decorate 函数开始执行,此时接收了两个参数:第一个参数是数组,数组的第一个是 firstDecorator 返回的装饰器函数,第二个是 __metadata 返回的元数据信息,第二个参数是 Test 构造函数,所以进入 __decorate 这个函数后 r 是 target 也就是 Test。
- 因为 Reflect.decorate 是 undefined 所以会走到 else 逻辑,在这里开始遍历了 decorators 数组,并且如果数组项中执行的结果有值的话会赋值给 d。此时 decorators 数组为 【firstDecorator 返回的函数,undefined】,所以 d 就是我们真正写的装饰器函数,也就是 firstDecorator 这个函数返回的函数。
- 接着执行 c < 3 的逻辑因为参数只有两个,所以装饰器函数会接收到一个参数 r,就是我们在函数中接收到的 Test 构造函数,由于我们的装饰器函数什么也没返回,所以 r 依然是 Test,最后将 r 返回。
- 分析完了类装饰器函数编译后的源码是不是觉得这就是个语法糖,逻辑也不复杂,其它装饰器像方法,属性的底层代码都是大同小异,无非就是参数个数的不同导致执行流程的不同而已。
装饰器的执行顺序
装饰器的执行顺序:属性装饰器 -> 方法参数装饰器 -> 方法装饰器 -> 构造函数参数装饰器 -> 类装饰器
/**
* 属性装饰器
* @param target
* @param attrname
*/
function one(target: any, attrname: any) {
console.log('one: ', target, attrname);
}
/**
* 方法参数装饰器
* @param target
* @param paramname
* @param paramindex
*/
function two(target: any, paramname: string, paramindex: number) {
console.log('two: ', target, paramname, paramindex);
}
/**
* 方法装饰器
* @param target
* @param methodname
*/
function three(target: any, methodname: string) {
console.log('three: ', target, methodname);
}
/**
* 构造函数参数装饰器
* @param target
* @param paramname
* @param paramindex
*/
function four(target: any, paramname: string, paramindex: number) {
console.log('four: ', target, paramname, paramindex);
}
/**
* 类装饰器
* @param target
*/
function five(target: any) {
console.log('five: ', target);
}
@five
class OrderTest {
@one
public name = '小红'
constructor(@four param: string) { }
@three
sayHi(@two param: string) {
console.log(param);
}
}
Reflect Metadata
reflect-metadata 是一个第三方的 npm 包,它的作用就是通过一些 api 操作可以增加一些扩展信息
import 'reflect-metadata'
class SuperTest {
@Reflect.metadata('getSuperNameData', 'SuperTest')
@Reflect.metadata('getSuperNameData1', 'SuperTest1')
getSuperName() {
}
}
class Test extends SuperTest {
// 定义元数据
@Reflect.metadata('sayHiData', '小红')
sayHi() {
}
// 定义元数据
@Reflect.metadata('getNameData', '小明')
getName() {
}
getAge() {
// 定义元数据
Reflect.defineMetadata('getAgeData', '18', Test.prototype, 'getAge')
}
}
const t = new Test()
t.getAge()
// 获取元数据
// output: Test: 小红
console.log('Test: ', Reflect.getMetadata('sayHiData', t, 'sayHi'));
// output: Test: 小明
console.log('Test: ', Reflect.getMetadata('getNameData', t, 'getName'))
// output: Test: 18
console.log('Test: ', Reflect.getMetadata('getAgeData', t, 'getAge'))
// 获取全部元数据
// output: Test: [
// 'design:returntype',
// 'design:paramtypes',
// 'design:type',
// 'getSuperNameData1',
// 'getSuperNameData'
// ]
console.log('Test: ', Reflect.getMetadataKeys(Test.prototype, 'getSuperName'))
// output: Test: []
console.log('Test: ', Reflect.getOwnMetadataKeys(Test.prototype, 'getSuperName'))
// output: Test: true
console.log('Test: ', Reflect.hasMetadata('getSuperNameData', Test.prototype, 'getSuperName'))
// output: Test: false
console.log('Test: ', Reflect.hasOwnMetadata('getSuperNameData', Test.prototype, 'getSuperName'))
几个关键的 api
- 定义元数据: 有两种方式,第一种通过注解的方式,第二种通过方法调用,其中传递的参数为元数据的 key,value,对象原型,方法名
- @Reflect.metadata(key, value)
- Reflect.defineMetadata(key, value, XX.prototype, method)
- 获取元数据: 通过 key 值获取对象上的元数据信息
- Reflect.getMetadata(key, obj, method)
- 获取自身的元数据,包括父类中的元数据
- Reflect.getMetadataKeys
- 获取自身的元数据,不包括父类中的元数据
- Reflect.getOwnMetadataKeys
- 是否包括元数据 key,包括父类中的元数据
- Reflect.hasMetadata
- 是否包括元数据 key,不包括父类中的元数据
- Reflect.hasOwnMetadata
装饰器应用的例子
抽取通用的逻辑
// user 是个 any 类型的对象
const user: any = undefined
class ErrorCatchTest {
getName() {
return user.name
}
getAge() {
return user.age
}
}
const et = new ErrorCatchTest()
et.getName()
et.getAge()
- 考虑上面这段代码,我们在调用方法时使用了外部的对象 user,但是我们并不确定这个 user 内部属性是否都赋值了,那么如果不处理,就有可能出现 TypeError: Cannot read properties of undefined 错误,那么解决办法就是加上 try catch,如下:
// user 是个 any 类型的对象
const user: any = undefined
class ErrorCatchTest {
getName() {
try {
return user.name
} catch (e) {
console.log(' getName 发生错误!', e)
}
}
getAge() {
try {
return user.age
} catch (e) {
console.log(' getName 发生错误!', e)
}
}
}
const et = new ErrorCatchTest()
et.getName()
et.getAge()
- 经过了 try catch 的处理后,程序不会崩溃了,我们还可以在错误发生时做一些其它操作,比如上传日志等等,但是现在我们的类中只有两个方法,如果存在几十个方法,每一个都这么处理,就既麻烦代码也不优雅了,那么我们就可以使用装饰器来将错误处理的逻辑抽取出来
// user 是个 any 类型的对象
const user: any = undefined
/**
* 捕获错误处理的装饰器函数
* @param errorMsg 错误信息
*/
function catchError(errorMsg: string = '') {
// target 原型对象
// key 方法名
// descriptor 属性描述符
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value
descriptor.value = function () {
try {
fn()
} catch (e) {
console.log(`${key} 发生错误:${errorMsg}`)
}
}
}
}
class ErrorCatchTest {
@catchError('user.name 不存在')
getName() {
return user.name
}
@catchError('user.age 不存在')
getAge() {
return user.age
}
}
const et = new ErrorCatchTest()
et.getName()
et.getAge()
- 可以看到,使用装饰器后,只需要在可能会发生错误的方法加增加装饰器函数注解,就可以了
总结
ts 的装饰器其实并不复杂,很多东西只要通过编译成 js 源码后看一下就能明白底层做了什么,明白了底层逻辑,也就很容易理解为什么装饰器函数可以接收那几个特定的参数了,是因为底层代码中传递过去的。当然如果了解过其它语言,比如 Java,对这种语法就更不会陌生,包括与之息息相关的依赖注入,控制反转。