什么是装饰器

装饰器,顾名思义,就是在不影响原有功能的情况下,增加一些附属的东西。可以理解成抽象的一种实现,把通用的东西给抽象出来,独立去使用。

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression 美 [ɪkˈsprɛʃən] 求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

expression 美 [ɪkˈsprɛʃən] 表现,表示,表达;表情,脸色,态度,腔调,声调;[数]式,符号;词句,语句,措辞,说法
Decorator 美 [ˈdɛkəˌretɚ] 室内装饰师,油漆匠

目前装饰器还不属于标准,还在 建议征集的第二阶段,但这并不妨碍我们在ts中的使用。在 tsconfig.json中开启 experimentalDecorators编译器选项

  1. {
  2. "compilerOptions": {
  3. "target": "ES5",
  4. "experimentalDecorators": true
  5. }
  6. }
  7. 复制代码

所以目前 @Decorators 仅是一个语法糖,装饰器可以理解成一种解决方案,我觉得这个跟 AOP 面向切面编程 的思想有点类似。

在非typescript环境中使用装饰器

  1. vscode 解决装饰器 ESLint: Cannot read property 'type' of undefined 报错问题
  2. "javascript.implicitProjectConfig.experimentalDecorators":true,
  3. VSCode中,转到File => Preferences => Settings(或Control +逗号)
  4. ,它将打开用户设置文件。添加“javascript.implicitProjectConfig.experimentalDecorators”:
  5. 对文件为true

执行时机

注意,修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。

  1. 创建类和创建函数时立即执行装饰器
  2. 如果装饰器函数返回一个function,这个function会在所有的装饰器函数运行完毕后,继续运行
  3. 参考文章中的“装饰器执行顺序”
  4. @decorator
  5. class A {}
  6. // 等同于
  7. class A {}
  8. A = decorator(A) || A;

为什么有这个执行时机呢?

  1. 编译前
  2. function changeMood(isHappy) {
  3. return function(target) {
  4. target.isHappy = isHappy
  5. }
  6. }
  7. function changeMoodStatic(target){
  8. target.isHappyStatic = true;
  9. }
  10. @changeMoodStatic
  11. @changeMood(true)
  12. class Boy {}
  13. console.log(Boy);
  14. @changeMood(false)
  15. class Girl {}
  16. console.log(Girl);
  17. // 编译后
  18. var _dec, _class, _dec2, _class2;
  19. function changeMood(isHappy) {
  20. return function (target) {
  21. target.isHappy = isHappy;
  22. };
  23. }
  24. function changeMoodStatic(target) {
  25. target.isHappyStatic = true;
  26. }
  27. let Boy = (_dec = changeMood(true), changeMoodStatic(_class = _dec(_class = class Boy {}) || _class) || _class);
  28. console.log(Boy);
  29. let Girl = (_dec2 = changeMood(false), _dec2(_class2 = class Girl {}) || _class2);
  30. console.log(Girl);

使用方式

装饰器可以应用在如下几处地方

  1. Class
  2. 函数
  3. 函数参数
  4. 属性
  5. get set 访问器

使用的语法很简单,类似于java的注解

  1. @sealed // 使用装饰器
  2. class Greeter {
  3. greeting: string;
  4. constructor(message: string) {
  5. this.greeting = message;
  6. }
  7. greet() {
  8. return "Hello, " + this.greeting;
  9. }
  10. }
  11. // 定义装饰器
  12. function sealed(constructor: Function) { // 此装饰器的作用是封闭一个对象
  13. Object.seal(constructor);
  14. Object.seal(constructor.prototype);
  15. }
  16. // mixins.js
  17. export function mixins(...list) {
  18. return function (target) {
  19. Object.assign(target.prototype, ...list)
  20. }
  21. }
  22. //########################################################################
  23. // main.js
  24. import { mixins } from './mixins'
  25. const Foo = {
  26. foo() { console.log('foo') }
  27. };
  28. @mixins(Foo)
  29. class MyClass {}
  30. let obj = new MyClass();
  31. obj.foo() // 'foo'

装饰器的执行顺序

装饰器可以同时应用多个,所以在定义装饰器的时候应当每个装饰器都是相互独立的。举个官方的栗子

  1. function f() {
  2. console.log("f(): evaluated");
  3. return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
  4. console.log("f(): called");
  5. }
  6. }
  7. function g() {
  8. console.log("g(): evaluated");
  9. return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
  10. console.log("g(): called");
  11. }
  12. }
  13. class C {
  14. @f()
  15. @g()
  16. method() {}
  17. }

执行结果

  1. f(): evaluated
  2. g(): evaluated
  3. g(): called
  4. f(): called

babel下使用@Decorator

利用@Decorator给类添加静态属性

  1. npm install babel-cli -g
  2. npm install babel-plugin-transform-decorators-legacy -D
  3. babel --plugins transform-decorators-legacy decorator.js --out-file test.js
  4. node test.js
  5. // decorator.js
  6. function changeMood(isHappy) {
  7. return function(target) {
  8. target.isHappy = isHappy
  9. }
  10. }
  11. function changeMoodStatic(target){
  12. target.isHappyStatic = true;
  13. }
  14. @changeMoodStatic
  15. @changeMood(true)
  16. class Boy {}
  17. console.log(Boy);
  18. @changeMood(false)
  19. class Girl {}
  20. console.log(Girl);
  21. { [Function: Boy] isHappy: true, isHappyStatic: true }
  22. { [Function: Girl] isHappy: false }

Class 修饰类,修饰类的方法

类的装饰器

在类中使用时,参数只有target一个

  1. function testable(isTestable) {
  2. return function(target) {
  3. target.isTestable = isTestable;
  4. }
  5. }

类装饰器,在类定义前执行,在装饰器中我们可以重新定义构造函数,用来监视,修改或替换类定义。举个栗子

  1. // 定义装饰器
  2. const FTest = <T extends {new(...args:any[]):{}}>(constructor:T) => {
  3. return class extends constructor {
  4. newProperty = "new property";
  5. hello = "override";
  6. }
  7. }
  8. @FTest
  9. class Test {
  10. hello: string;
  11. constructor(){
  12. this.hello = 'test'
  13. }
  14. }
  15. const test = new Test();
  16. console.log(test.hello) // override

可以看到, hello 的值在构造器中被我们修改了。类装饰器只能有一个参数,即原本类的构造函数。
Mixin 的实现就可以使用类构造器。

类的方法装饰器

装饰函数时,可以有3个参数

  1. // 接收参数的语法
  2. function setHello(value) {
  3. console.log(value)
  4. return function(target, propKey, descriptor) {
  5. console.log(target, propKey, descriptor)
  6. }
  7. }
  8. // 不接收参数的语法
  9. function setHello(target, propKey, descriptor) {
  10. // descriptor 可以通过descriptor改变value的值
  11. var oldValue = descriptor.value;
  12. console.log('执行顺序1')
  13. descriptor.value = function() {
  14. console.log('执行顺序3')
  15. // 在这里可以输出日志
  16. console.log(`Calling ${name} with`, arguments);
  17. // return 1; 这里可以改变math.add的返回值
  18. return oldValue.apply(this, arguments);
  19. };
  20. console.log('执行顺序2')
  21. return descriptor;
  22. }
  23. class Boy {
  24. constructor(){
  25. this.hello;
  26. this._hello = this.hello;
  27. }
  28. @setHello('value')
  29. setHello(){}
  30. }
  31. const boy = new Boy();
  32. console.log(boy.hello);
  33. boy.hello = '777';
  34. console.log(boy.hello);
  35. /* 以下是输出
  36. value
  37. Boy {}
  38. 'setHello'
  39. { value: [Function: setHello],
  40. writable: true,
  41. enumerable: false,
  42. configurable: true }
  43. undefined
  44. 777
  45. */

应用场景

自动绑定this @autobind

autobind修饰器使得方法中的this对象,绑定原始对象。

  1. import { autobind } from 'core-decorators';
  2. class Person {
  3. @autobind
  4. getPerson() {
  5. return this;
  6. }
  7. }
  8. let person = new Person();
  9. let getPerson = person.getPerson;
  10. getPerson() === person;
  11. // true

检查子类是否正确复写父类方法@override

override修饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。

  1. import { override } from 'core-decorators';
  2. class Parent {
  3. speak(first, second) {}
  4. }
  5. class Child extends Parent {
  6. @override
  7. speak() {}
  8. // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
  9. }
  10. // or
  11. class Child extends Parent {
  12. @override
  13. speaks() {}
  14. // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
  15. //
  16. // Did you mean "speak"?
  17. }

该方法将要废弃@deprecate (别名@deprecated)

deprecatedeprecated修饰器在控制台显示一条警告,表示该方法将废除。

  1. import { deprecate } from 'core-decorators';
  2. class Person {
  3. @deprecate
  4. facepalm() {}
  5. @deprecate('We stopped facepalming')
  6. facepalmHard() {}
  7. @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  8. facepalmHarder() {}
  9. }
  10. let person = new Person();
  11. person.facepalm();
  12. // DEPRECATION Person#facepalm: This function will be removed in future versions.
  13. person.facepalmHard();
  14. // DEPRECATION Person#facepalmHard: We stopped facepalming
  15. person.facepalmHarder();
  16. // DEPRECATION Person#facepalmHarder: We stopped facepalming
  17. //
  18. // See http://knowyourmeme.com/memes/facepalm for more details.
  19. //

@suppressWarnings

suppressWarnings修饰器抑制deprecated修饰器导致的console.warn()调用。但是,异步代码发出的调用除外。

  1. import { suppressWarnings } from 'core-decorators';
  2. class Person {
  3. @deprecated
  4. facepalm() {}
  5. @suppressWarnings
  6. facepalmWithoutWarning() {
  7. this.facepalm();
  8. }
  9. }
  10. let person = new Person();
  11. person.facepalmWithoutWarning();
  12. // no warning is logged

使用修饰器实现自动发布事件

  1. const postal = require("postal/lib/postal.lodash");
  2. export default function publish(topic, channel) {
  3. const channelName = channel || '/';
  4. const msgChannel = postal.channel(channelName);
  5. msgChannel.subscribe(topic, v => {
  6. console.log('频道: ', channelName);
  7. console.log('事件: ', topic);
  8. console.log('数据: ', v);
  9. });
  10. return function(target, name, descriptor) {
  11. const fn = descriptor.value;
  12. descriptor.value = function() {
  13. let value = fn.apply(this, arguments);
  14. msgChannel.publish(topic, value);
  15. };
  16. };
  17. }
  18. 上面代码定义了一个名为publish的修饰器,它通过改写descriptor.value,使得原方法被调用时,会自动发出一个事件。它使用的事件“发布/订阅”库是Postal.js
  19. 它的用法如下。
  20. // index.js
  21. import publish from './publish';
  22. class FooComponent {
  23. @publish('foo.some.message', 'component')
  24. someMethod() {
  25. return { my: 'data' };
  26. }
  27. @publish('foo.some.other')
  28. anotherMethod() {
  29. // ...
  30. }
  31. }
  32. let foo = new FooComponent();
  33. foo.someMethod();
  34. foo.anotherMethod();
  35. 以后,只要调用someMethod或者anotherMethod,就会自动发出一个事件。
  36. $ bash-node index.js
  37. 频道: component
  38. 事件: foo.some.message
  39. 数据: { my: 'data' }
  40. 频道: /
  41. 事件: foo.some.other
  42. 数据: undefined

属性不可写

  1. class Person {
  2. @readonly
  3. name() { return `${this.first} ${this.last}` }
  4. }
  5. function readonly(target, name, descriptor){
  6. // descriptor对象原来的值如下
  7. // {
  8. // value: specifiedFunction,
  9. // enumerable: false,
  10. // configurable: true,
  11. // writable: true
  12. // };
  13. descriptor.writable = false;
  14. return descriptor;
  15. }
  16. readonly(Person.prototype, 'name', descriptor);
  17. // 类似于
  18. Object.defineProperty(Person.prototype, 'name', descriptor);

设置属性为不可枚举

  1. class Person {
  2. // children = [1,2,3,4];
  3. constructor(){
  4. this.x = 1;
  5. this.y = 2;
  6. this.children = [1,2,3,4];
  7. this.kidCount = this.children.length;
  8. }
  9. @nonenumerable
  10. get kidCount() { return this.children.length; }
  11. set kidCount(value) { return value; }
  12. add(x,y){
  13. return x * y;
  14. }
  15. }
  16. var a = new Person();
  17. console.log(Object.keys(Person)); // 返回[],因为this作用域的缘故,他本身没有可枚举
  18. console.log(Object.getOwnPropertyNames(Person)); // 返回 ["length", "prototype", "name"]
  19. console.log(Object.keys(a)); // ["x", "y", "children"]
  20. console.log(Object.getOwnPropertyNames(a)); // ["x", "y", "children"]
  21. // 注意 add方法与kidCount 可枚举方法与不可枚举方法,都显示不出来
  22. function nonenumerable(target, name, descriptor) {
  23. descriptor.enumerable = false;
  24. return descriptor;
  25. }

打印日志

  1. class Math {
  2. @log
  3. add(a, b) {
  4. return a + b;
  5. }
  6. }
  7. function log(target, name, descriptor) {
  8. var oldValue = descriptor.value;
  9. console.log('执行顺序1')
  10. descriptor.value = function() {
  11. console.log('执行顺序3')
  12. // 在这里可以输出日志
  13. console.log(`Calling ${name} with`, arguments);
  14. // return 1; 这里可以改变math.add的返回值
  15. return oldValue.apply(this, arguments);
  16. };
  17. console.log('执行顺序2')
  18. return descriptor;
  19. }
  20. const math = new Math();
  21. console.log(math.add(2, 4));
  22. console.log(math.add(3, 4));
  23. console.log(math.add(4, 4));
  24. /*
  25. 结果打印
  26. 执行顺序1
  27. 执行顺序2
  28. 执行顺序3
  29. Calling add with [Arguments] { '0': 2, '1': 4 }
  30. 6
  31. 执行顺序3
  32. Calling add with [Arguments] { '0': 3, '1': 4 }
  33. 7
  34. 执行顺序3
  35. Calling add with [Arguments] { '0': 4, '1': 4 }
  36. 8
  37. */

我觉得函数装饰器的使用场景会跟多一些,比如说函数的权限判断、参数校验、日志打点等一些通用的处理,因为这些都跟函数本身的业务逻辑相独立,所以就可以通过装饰器来实现。
在举栗子之前,我们想要介绍一个ts官方的库 reflect-metadata
reflect-metadata 的作用就是在装饰器中类给类添加一些自定义的信息,然后在需要使用的地方通过反射定义的信息提取出来。举个栗子

  1. const Custom = (value?: any): MethodDecorator => {
  2. return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
  3. Reflect.defineMetadata('name', value, target, propertyKey);
  4. }
  5. }
  6. class A{
  7. @Custom('test')
  8. method(){}
  9. }
  10. console.log(Reflect.getMetadata('name', new A(), 'method')) // test
  11. 复制代码

看下上面两个 Reflect API
Reflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method");
Reflect.getMetadata(metadataKey, obj, "method") 可见上面的栗子中,在Custom装饰器中,给元数据设置的值,可以在任何地方获取。
Reflect API

  1. namespace Reflect {
  2. // 用于装饰器
  3. metadata(k, v): (target, property?) => void
  4. // 在对象上面定义元数据
  5. defineMetadata(k, v, o, p?): void
  6. // 是否存在元数据
  7. hasMetadata(k, o, p?): boolean
  8. hasOwnMetadata(k, o, p?): boolean
  9. // 获取元数据
  10. getMetadata(k, o, p?): any
  11. getOwnMetadata(k, o, p?): any
  12. // 获取所有元数据的 Key
  13. getMetadataKeys(o, p?): any[]
  14. getOwnMetadataKeys(o, p?): any[]
  15. // 删除元数据
  16. deleteMetadata(k, o, p?): boolean
  17. }
  18. 复制代码

再回到函数装饰器,装饰器有三个参数

  1. 如果装饰器挂载于静态成员上,则会返回构造函数,如果挂载于实例成员上则会返回类的原型
  2. 装饰器挂载的成员名称,函数名称或属性名
  3. 成员的描述符,也就是Object.getOwnPropertyDescriptor的返回值

我简单实现了几个装饰器

  1. // 当前函数的请求方式
  2. enum METHOD {
  3. GET = 0
  4. }
  5. const Methor = (method: METHOD) => (value?: any): MethodDecorator => {
  6. return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
  7. Reflect.defineMetadata('methodMetaData', method, target, propertyKey);
  8. }
  9. }
  10. const Get = Methor(METHOD.GET)
  11. 复制代码
  1. // 记录函数执行的耗时
  2. const ConsumeTime = (target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<Function>) => {
  3. let method = descriptor.value;
  4. descriptor.value = function () {
  5. let start = new Date().valueOf()
  6. try {
  7. return method.apply(this, arguments).then(() => {
  8. let end = new Date().valueOf()
  9. console.log(`${target.constructor.name}-${propertyKey} start: ${start} end: ${end} consume: ${end - start}`)
  10. }, (e: any) => {
  11. console.error(e)
  12. });
  13. } catch (e) {
  14. console.error('error')
  15. }
  16. }
  17. }
  18. 复制代码
  1. // 函数参数校验,这里使用了 Joi
  2. const ParamValidate = (value: any) => {
  3. return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
  4. const schema = Joi.object().keys(value);
  5. let method = descriptor.value;
  6. descriptor.value = function () {
  7. const { error, value } = Joi.validate(arguments[1], schema);
  8. if (error) {
  9. throw new Error("ParamValidate Error.");
  10. }
  11. return method.apply(this, arguments);
  12. }
  13. }
  14. }
  15. 复制代码

使用如下

  1. class Test {
  2. @ConsumeTime
  3. @Get()
  4. @ParamValidate({
  5. username: Joi.string(),
  6. password: Joi.string(),
  7. })
  8. async userInfo(ctx: any, param: any) {
  9. await this.sleep(1000)
  10. }
  11. async sleep(ms:number){
  12. return new Promise((resolve:any)=>setTimeout(resolve,ms));
  13. }
  14. }
  15. 复制代码

函数、函数参数、属性、访问器

小结

reflect-metadata 我们想要介绍一个ts官方的库
core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。

装饰器是个很方便的东西,在前端领域它算是个比较新的东西,但是它的思想在后端已经非常成熟了,也可看出,前端工程化是个大趋势,引入成熟的思想,完善前端工程的空缺,以后的前端可做的将越来越广。

作者:小黎也
链接:https://juejin.im/post/5c84c6afe51d453ac76c2d97
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。