什么是装饰器
装饰器,顾名思义,就是在不影响原有功能的情况下,增加一些附属的东西。可以理解成抽象的一种实现,把通用的东西给抽象出来,独立去使用。
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression 美 [ɪkˈsprɛʃən] 求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
expression 美 [ɪkˈsprɛʃən] 表现,表示,表达;表情,脸色,态度,腔调,声调;[数]式,符号;词句,语句,措辞,说法
Decorator 美 [ˈdɛkəˌretɚ] 室内装饰师,油漆匠
目前装饰器还不属于标准,还在 建议征集的第二阶段,但这并不妨碍我们在ts中的使用。在 tsconfig.json中开启 experimentalDecorators编译器选项
{"compilerOptions": {"target": "ES5","experimentalDecorators": true}}复制代码
所以目前 @Decorators 仅是一个语法糖,装饰器可以理解成一种解决方案,我觉得这个跟 AOP 面向切面编程 的思想有点类似。
在非typescript环境中使用装饰器
vscode 解决装饰器 ESLint: Cannot read property 'type' of undefined 报错问题"javascript.implicitProjectConfig.experimentalDecorators":true,在VSCode中,转到File => Preferences => Settings(或Control +逗号),它将打开用户设置文件。添加“javascript.implicitProjectConfig.experimentalDecorators”:对文件为true
执行时机
注意,修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
创建类和创建函数时立即执行装饰器如果装饰器函数返回一个function,这个function会在所有的装饰器函数运行完毕后,继续运行参考文章中的“装饰器执行顺序”@decoratorclass A {}// 等同于class A {}A = decorator(A) || A;
为什么有这个执行时机呢?
编译前function changeMood(isHappy) {return function(target) {target.isHappy = isHappy}}function changeMoodStatic(target){target.isHappyStatic = true;}@changeMoodStatic@changeMood(true)class Boy {}console.log(Boy);@changeMood(false)class Girl {}console.log(Girl);// 编译后var _dec, _class, _dec2, _class2;function changeMood(isHappy) {return function (target) {target.isHappy = isHappy;};}function changeMoodStatic(target) {target.isHappyStatic = true;}let Boy = (_dec = changeMood(true), changeMoodStatic(_class = _dec(_class = class Boy {}) || _class) || _class);console.log(Boy);let Girl = (_dec2 = changeMood(false), _dec2(_class2 = class Girl {}) || _class2);console.log(Girl);
使用方式
装饰器可以应用在如下几处地方
- Class
- 函数
- 函数参数
- 属性
- get set 访问器
使用的语法很简单,类似于java的注解
@sealed // 使用装饰器class Greeter {greeting: string;constructor(message: string) {this.greeting = message;}greet() {return "Hello, " + this.greeting;}}// 定义装饰器function sealed(constructor: Function) { // 此装饰器的作用是封闭一个对象Object.seal(constructor);Object.seal(constructor.prototype);}// mixins.jsexport function mixins(...list) {return function (target) {Object.assign(target.prototype, ...list)}}//########################################################################// main.jsimport { mixins } from './mixins'const Foo = {foo() { console.log('foo') }};@mixins(Foo)class MyClass {}let obj = new MyClass();obj.foo() // 'foo'
装饰器的执行顺序
装饰器可以同时应用多个,所以在定义装饰器的时候应当每个装饰器都是相互独立的。举个官方的栗子
function f() {console.log("f(): evaluated");return function (target, propertyKey: string, descriptor: PropertyDescriptor) {console.log("f(): called");}}function g() {console.log("g(): evaluated");return function (target, propertyKey: string, descriptor: PropertyDescriptor) {console.log("g(): called");}}class C {@f()@g()method() {}}
执行结果
f(): evaluatedg(): evaluatedg(): calledf(): called
babel下使用@Decorator
利用@Decorator给类添加静态属性
npm install babel-cli -gnpm install babel-plugin-transform-decorators-legacy -Dbabel --plugins transform-decorators-legacy decorator.js --out-file test.jsnode test.js// decorator.jsfunction changeMood(isHappy) {return function(target) {target.isHappy = isHappy}}function changeMoodStatic(target){target.isHappyStatic = true;}@changeMoodStatic@changeMood(true)class Boy {}console.log(Boy);@changeMood(false)class Girl {}console.log(Girl);{ [Function: Boy] isHappy: true, isHappyStatic: true }{ [Function: Girl] isHappy: false }
Class 修饰类,修饰类的方法
类的装饰器
在类中使用时,参数只有target一个
function testable(isTestable) {return function(target) {target.isTestable = isTestable;}}
类装饰器,在类定义前执行,在装饰器中我们可以重新定义构造函数,用来监视,修改或替换类定义。举个栗子
// 定义装饰器const FTest = <T extends {new(...args:any[]):{}}>(constructor:T) => {return class extends constructor {newProperty = "new property";hello = "override";}}@FTestclass Test {hello: string;constructor(){this.hello = 'test'}}const test = new Test();console.log(test.hello) // override
可以看到, hello 的值在构造器中被我们修改了。类装饰器只能有一个参数,即原本类的构造函数。
Mixin 的实现就可以使用类构造器。
类的方法装饰器
装饰函数时,可以有3个参数
// 接收参数的语法function setHello(value) {console.log(value)return function(target, propKey, descriptor) {console.log(target, propKey, descriptor)}}// 不接收参数的语法function setHello(target, propKey, descriptor) {// descriptor 可以通过descriptor改变value的值var oldValue = descriptor.value;console.log('执行顺序1')descriptor.value = function() {console.log('执行顺序3')// 在这里可以输出日志console.log(`Calling ${name} with`, arguments);// return 1; 这里可以改变math.add的返回值return oldValue.apply(this, arguments);};console.log('执行顺序2')return descriptor;}class Boy {constructor(){this.hello;this._hello = this.hello;}@setHello('value')setHello(){}}const boy = new Boy();console.log(boy.hello);boy.hello = '777';console.log(boy.hello);/* 以下是输出valueBoy {}'setHello'{ value: [Function: setHello],writable: true,enumerable: false,configurable: true }undefined777*/
应用场景
自动绑定this @autobind
autobind修饰器使得方法中的this对象,绑定原始对象。
import { autobind } from 'core-decorators';class Person {@autobindgetPerson() {return this;}}let person = new Person();let getPerson = person.getPerson;getPerson() === person;// true
检查子类是否正确复写父类方法@override
override修饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。
import { override } from 'core-decorators';class Parent {speak(first, second) {}}class Child extends Parent {@overridespeak() {}// SyntaxError: Child#speak() does not properly override Parent#speak(first, second)}// orclass Child extends Parent {@overridespeaks() {}// SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.//// Did you mean "speak"?}
该方法将要废弃@deprecate (别名@deprecated)
deprecate或deprecated修饰器在控制台显示一条警告,表示该方法将废除。
import { deprecate } from 'core-decorators';class Person {@deprecatefacepalm() {}@deprecate('We stopped facepalming')facepalmHard() {}@deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })facepalmHarder() {}}let person = new Person();person.facepalm();// DEPRECATION Person#facepalm: This function will be removed in future versions.person.facepalmHard();// DEPRECATION Person#facepalmHard: We stopped facepalmingperson.facepalmHarder();// DEPRECATION Person#facepalmHarder: We stopped facepalming//// See http://knowyourmeme.com/memes/facepalm for more details.//
@suppressWarnings
suppressWarnings修饰器抑制deprecated修饰器导致的console.warn()调用。但是,异步代码发出的调用除外。
import { suppressWarnings } from 'core-decorators';class Person {@deprecatedfacepalm() {}@suppressWarningsfacepalmWithoutWarning() {this.facepalm();}}let person = new Person();person.facepalmWithoutWarning();// no warning is logged
使用修饰器实现自动发布事件
const postal = require("postal/lib/postal.lodash");export default function publish(topic, channel) {const channelName = channel || '/';const msgChannel = postal.channel(channelName);msgChannel.subscribe(topic, v => {console.log('频道: ', channelName);console.log('事件: ', topic);console.log('数据: ', v);});return function(target, name, descriptor) {const fn = descriptor.value;descriptor.value = function() {let value = fn.apply(this, arguments);msgChannel.publish(topic, value);};};}上面代码定义了一个名为publish的修饰器,它通过改写descriptor.value,使得原方法被调用时,会自动发出一个事件。它使用的事件“发布/订阅”库是Postal.js。它的用法如下。// index.jsimport publish from './publish';class FooComponent {@publish('foo.some.message', 'component')someMethod() {return { my: 'data' };}@publish('foo.some.other')anotherMethod() {// ...}}let foo = new FooComponent();foo.someMethod();foo.anotherMethod();以后,只要调用someMethod或者anotherMethod,就会自动发出一个事件。$ bash-node index.js频道: component事件: foo.some.message数据: { my: 'data' }频道: /事件: foo.some.other数据: undefined
属性不可写
class Person {@readonlyname() { return `${this.first} ${this.last}` }}function readonly(target, name, descriptor){// descriptor对象原来的值如下// {// value: specifiedFunction,// enumerable: false,// configurable: true,// writable: true// };descriptor.writable = false;return descriptor;}readonly(Person.prototype, 'name', descriptor);// 类似于Object.defineProperty(Person.prototype, 'name', descriptor);
设置属性为不可枚举
class Person {// children = [1,2,3,4];constructor(){this.x = 1;this.y = 2;this.children = [1,2,3,4];this.kidCount = this.children.length;}@nonenumerableget kidCount() { return this.children.length; }set kidCount(value) { return value; }add(x,y){return x * y;}}var a = new Person();console.log(Object.keys(Person)); // 返回[],因为this作用域的缘故,他本身没有可枚举console.log(Object.getOwnPropertyNames(Person)); // 返回 ["length", "prototype", "name"]console.log(Object.keys(a)); // ["x", "y", "children"]console.log(Object.getOwnPropertyNames(a)); // ["x", "y", "children"]// 注意 add方法与kidCount 可枚举方法与不可枚举方法,都显示不出来function nonenumerable(target, name, descriptor) {descriptor.enumerable = false;return descriptor;}
打印日志
class Math {@logadd(a, b) {return a + b;}}function log(target, name, descriptor) {var oldValue = descriptor.value;console.log('执行顺序1')descriptor.value = function() {console.log('执行顺序3')// 在这里可以输出日志console.log(`Calling ${name} with`, arguments);// return 1; 这里可以改变math.add的返回值return oldValue.apply(this, arguments);};console.log('执行顺序2')return descriptor;}const math = new Math();console.log(math.add(2, 4));console.log(math.add(3, 4));console.log(math.add(4, 4));/*结果打印执行顺序1执行顺序2执行顺序3Calling add with [Arguments] { '0': 2, '1': 4 }6执行顺序3Calling add with [Arguments] { '0': 3, '1': 4 }7执行顺序3Calling add with [Arguments] { '0': 4, '1': 4 }8*/
我觉得函数装饰器的使用场景会跟多一些,比如说函数的权限判断、参数校验、日志打点等一些通用的处理,因为这些都跟函数本身的业务逻辑相独立,所以就可以通过装饰器来实现。
在举栗子之前,我们想要介绍一个ts官方的库 reflect-metadatareflect-metadata 的作用就是在装饰器中类给类添加一些自定义的信息,然后在需要使用的地方通过反射定义的信息提取出来。举个栗子
const Custom = (value?: any): MethodDecorator => {return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {Reflect.defineMetadata('name', value, target, propertyKey);}}class A{@Custom('test')method(){}}console.log(Reflect.getMetadata('name', new A(), 'method')) // test复制代码
看下上面两个 Reflect APIReflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");Reflect.getMetadata(metadataKey, obj, "method")
可见上面的栗子中,在Custom装饰器中,给元数据设置的值,可以在任何地方获取。
Reflect API
namespace Reflect {// 用于装饰器metadata(k, v): (target, property?) => void// 在对象上面定义元数据defineMetadata(k, v, o, p?): void// 是否存在元数据hasMetadata(k, o, p?): booleanhasOwnMetadata(k, o, p?): boolean// 获取元数据getMetadata(k, o, p?): anygetOwnMetadata(k, o, p?): any// 获取所有元数据的 KeygetMetadataKeys(o, p?): any[]getOwnMetadataKeys(o, p?): any[]// 删除元数据deleteMetadata(k, o, p?): boolean}复制代码
再回到函数装饰器,装饰器有三个参数
- 如果装饰器挂载于静态成员上,则会返回构造函数,如果挂载于实例成员上则会返回类的原型
- 装饰器挂载的成员名称,函数名称或属性名
- 成员的描述符,也就是Object.getOwnPropertyDescriptor的返回值
我简单实现了几个装饰器
// 当前函数的请求方式enum METHOD {GET = 0}const Methor = (method: METHOD) => (value?: any): MethodDecorator => {return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {Reflect.defineMetadata('methodMetaData', method, target, propertyKey);}}const Get = Methor(METHOD.GET)复制代码
// 记录函数执行的耗时const ConsumeTime = (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<Function>) => {let method = descriptor.value;descriptor.value = function () {let start = new Date().valueOf()try {return method.apply(this, arguments).then(() => {let end = new Date().valueOf()console.log(`${target.constructor.name}-${propertyKey} start: ${start} end: ${end} consume: ${end - start}`)}, (e: any) => {console.error(e)});} catch (e) {console.error('error')}}}复制代码
// 函数参数校验,这里使用了 Joiconst ParamValidate = (value: any) => {return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {const schema = Joi.object().keys(value);let method = descriptor.value;descriptor.value = function () {const { error, value } = Joi.validate(arguments[1], schema);if (error) {throw new Error("ParamValidate Error.");}return method.apply(this, arguments);}}}复制代码
使用如下
class Test {@ConsumeTime@Get()@ParamValidate({username: Joi.string(),password: Joi.string(),})async userInfo(ctx: any, param: any) {await this.sleep(1000)}async sleep(ms:number){return new Promise((resolve:any)=>setTimeout(resolve,ms));}}复制代码
函数、函数参数、属性、访问器
小结
reflect-metadata 我们想要介绍一个ts官方的库
core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。
装饰器是个很方便的东西,在前端领域它算是个比较新的东西,但是它的思想在后端已经非常成熟了,也可看出,前端工程化是个大趋势,引入成熟的思想,完善前端工程的空缺,以后的前端可做的将越来越广。
作者:小黎也
链接:https://juejin.im/post/5c84c6afe51d453ac76c2d97
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
