理解 JS 装饰器:@Decorator

Javascript 里的装饰器目前还处于 stage-2 阶段,借助 TypeScript 或者 Babel,已经有大量的优秀开源项目深度用上它了,比如:VS Code:

理解装饰器有助于帮助我们更好的看到这些优秀项目的源码核心思想。包括AOP,IoC,DI等编程思想:

  • AOP 主要意图是将日志记录,性能统计,安全控制,异常处理等辅助功能逻辑代码从业务逻辑代码中解耦划分出来,将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
  • IoC 即控制反转 (Inversion of Control),是解耦的一种设计理念,IoC 控制反转的设计模式可以大幅度地降低了程序的耦合性。
  • DI 即依赖注入 (Dependency Injection),是 IoC 的一种具体实现。

装饰器在 VS Code 的控制反转设计模式里,其主要作用是实现 DI 依赖注入的功能以及将部分重复的写法进行精简。使用 IoC 前:

使用 IoC 后:

 

装饰器的存在就是希望实现装饰器模式的设计理念而装饰器的语法是一种便捷的语法糖(写法),通过 @ 来引用,需要编译后才能运行。

Object.getOwnPropertyDescriptor(obj, prop) 方法返回指定对象上一个自有属性对应的属性描述符对象。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

​​​​​​Object.defineProperty(obj, prop, descriptor)方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

实现一个记录函数执行耗时的功能的装饰器 

1. 编写装饰器函数业务逻辑代码

function logTime(target, key, descriptor) {
  const oldMethed = descriptor.value;
  const logTime = function (...arg) {
    let start = +new Date();
    try {
      /** 调用函数 */
      return oldMethed.apply(this, arg); 
    } finally {
      let end = +new Date()
      console.log(`耗时: ${end - start}ms`);
    }
  }
  descriptor.value = logTime;
  return descriptor;
}

2. 语法糖装饰指定属性

@logTime
fn() {
  // do something ...
}

无论是最新版的 Chrome 浏览器还是 Node.js 都不能直接运行带有 @Decorator 语法糖的代码。我们需要借助 TypeScript 或者 Babel 的能力,将源码编译后才能正常运行。而在 TypeSciprt Playground 上,我们可以直接看到编译后代码:

/**
 * 1. (this && this.__decorate)  this 是指 window 对象,这一步的含义是避免重复定义 __decorate 函数;
 */
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    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--) if (d = decorators[i]) 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;
};

/** 精简后 */
var __decorate = function (decorators, target, key, desc) {
    var c = arguments.length;
    /** 备份原来类构造器 (Class.prototype) 的属性描述符 (Descriptor) */
    var r = desc = Object.getOwnPropertyDescriptor(target, key),
    var d;

    for (var i = decorators.length - 1; i >= 0; i--) {
      /** d 为装饰器业务逻辑函数 */
      if (d = decorators[i]) {
        /*** 执行 d,并传入 target 类构造器,key 属性名,r 属性描述符 */
        r = d(target, key, r) || r;
      }
    }

    /** 用装饰器函数覆盖原来属性描述符 */
    Object.defineProperty(target, key, r)
    return r;
};

装饰器工具函数(__decorate)的执行会传入以下 4 个参数: 

  1. 装饰器业务逻辑函数

  2. 类的构造器

  3. 类的构造器属性名

  4. 属性描述符(可以为 null)。

给 logTime 添加参数,即实现带参数的装饰器:装饰器工厂函数

logTime 是个高阶函数,可以理解成装饰器工厂函数,其接收参数执行后,返回一个装饰器函数:

function logTime(tag) { 
  return function(target, key, descriptor) {
    const oldMethed = descriptor.value
    const logTime = function (...arg) {
      let start = +new Date()
      try {
        return oldMethed.apply(this, arg)
      } finally {
        let end = +new Date()
        console.log(`【${tag}】耗时: ${end - start}ms`)
      }
    }
    descriptor.value = logTime
    return descriptor
  }
}

/** 使用 */
@logTime('');
fn() {
  // do something ...
}

装饰器的种类:类、属性、方法、参数、访问器

装饰器一共有 5 种类型:

        1. 类装饰器

/** 
 * 类装饰器
 * @params:target 类的构造器
 * @returns:如果类装饰器返回了一个值,她将会被用来代替原有的类构造器的声明。
 */
function classDecorator(target: any) {
  return // ...
};

因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。例如我们可以添加一个 addToJsonString 方法给所有的类来新增一个 toString 方法:

function addToJSONString(target) {
  return class extends target {
    toJSONString() {
      return JSON.stringify(this);
    }
  };
}

@addToJSONString
class C {
  public foo = "foo";
  public num = 24;
}

console.log(new C().toJSONString())
// "{"foo":"foo","num":24}"

        2. 属性装饰器

/** 
 * 属性装饰器
 * @params target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链
 *         propertyKey: 属性的名称
 * @returns 返回的结果将被忽略。
 */
function propertyDecorator(target: any, propertyKey: string) {

}

利用属性装饰器,可以实现当属性改变时触发指定函数的监听功能 :

function observable(fnName) {  // 装饰器工厂函数
  return function (target: any, key: string): any {  // 装饰器
    let prev = target[key];
    Object.defineProperty(target, key, {
      set(next) {
        target[fnName](prev, next);
        prev = next;
      }
    })
  }
}

class Store {
  @observable('onCountChange')
  count = -1;

  onCountChange(prev, next) {
    console.log('>>> count has changed!')
    console.log('>>> prev: ', prev)
    console.log('>>> next: ', next)
  }
}

const store = new Store();
store.count = 10
// >>> count has changed!
// >>> prev: ",  undefined
// >>> next: ",  -1
// >>> count has changed!"
// >>> prev: ",  -1
// >>> next: ",  10

        3. 方法装饰器

/** 
 * 访问器装饰器
 * @params target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链
 *         propertyKey: 属性的名称
 *         descriptor: 属性的描述器 - { value writable enumerable configurable}
 * @returns 如果返回了值,它会被用于替代属性的描述器。
 */
function methodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  return // ...
};

利用方法装饰器可以实现与 Before / After 钩子 相关的场景功能。

        4. 访问器装饰器

/** 
 * 访问器装饰器
 * @params target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链
 *         propertyKey: 属性的名称
 *         descriptor: 属性的描述器 - { get, set, enumerable, configurable }
 * @returns 如果返回了值,它会被用于替代属性的描述器。
 */
function accessorDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  return // ...
};

利用访问器装饰器,可以将某个属性在赋值或访问的时候做一层代理,比如额外相加一个值: 

function addExtraNumber(num) {
  return function (target, propertyKey, descriptor) {
    const original = descriptor.set;

    descriptor.set = function (value) {
      const newObj = {}
      Object.keys(value).forEach(key => {
        newObj[key] = value[key] + num
      })
      return original.call(this, newObj)
    }
  }
}

class C {
  private _point = { x: 0, y: 0 }

  @addExtraNumber(2)
  set point(value: { x: number, y: number }) {
    this._point = value;
  }

  get point() {
    return this._point;
  }
}

const c = new C();
c.point = { x: 1, y: 1 };

console.log(c.point)
/** 
  {
    "x": 3,
    "y": 3
  }
*/

        5. 参数装饰器

/** 
 * 参数装饰器
 * @params target: 对于静态成员来说是类的构造器,对于实例成员来说是类的原型链
 *         methedKey: 方法的名称,注意!是方法的名称,而不是参数的名称
 *         parameterIndex: 参数在方法中所处的位置的下标
 * @returns 返回的结果将被忽略。
 */
function parameterDecorator(target: any, methedKey: string, parameterIndex: number) {

}

一般都被用于记录可被其它装饰器所使用的信息,比如,参数,方法名等:

function Log(target, methedKey, parameterIndex) {
  console.log(`方法名称 ${methedKey}`);
  console.log(`参数顺序 ${parameterIndex}`);
}

class Student {
  watch(@Log bookName) {
    console.log(`看了一本书:${bookName} `)
  }
}

// "方法名称 watch"
// "参数顺序 0"

多个装饰器的执行顺序

同种装饰器组合,其顺序会像剥洋葱一样,先从外到内进入,然后由内向外执行。和 Koa 的中间件顺序类似。

function dec(id){
  console.log('装饰器初始化', id);
  return function (target, property, descriptor) { // 装饰器函数
    console.log('装饰器执行', id);
  }
}

class Example {
  @dec(1)
  @dec(2)
  method(){}
}

/** dec(1), dec(2) 初始化立即执行, dec 返回的装饰器函数倒序执行 */
/** 编译后的代码 */
var __decorate = function (decorators, target, key, desc) {
    var c = arguments.length,
        r = desc = Object.getOwnPropertyDescriptor(target, key),
        d;
    for (var i = decorators.length - 1; i >= 0; i--)
      if (d = decorators[i]) r = d(target, key, r) || r;
    Object.defineProperty(target, key, r)
    return r;
};

function dec(id) {
    console.log('装饰器初始化', id);
    return function (target, property, descriptor) {
        console.log('装饰器执行', id);
    };
}
class Example {
    method() { }
}
__decorate([
    dec(1),
    dec(2)
], Example.prototype, "method", null);

不同类型装饰器顺序,实例成员最高,内部成员里面的装饰器则按定义顺序执行,类装饰器最低:

  1. 实例成员:(参数 > 方法) / 访问器 / 属性 装饰器 (按顺序)

  2. 静态成员:(参数 > 方法) / 访问器 / 属性 装饰器 (按顺序)

  3. 构造器:参数装饰器

  4. 类装饰器

比如:

function decorator(key: string): any {
  return function () {
    console.log("执行: ", key);
  };
}

@decorator("8. 类")
class C {
  @decorator("4. 静态属性")
  static prop?: number;

  @decorator("5. 静态方法")
  static method(@decorator("6. 静态方法的参数") foo) {}

  constructor(@decorator("7. 构造器的参数") foo) {}

  @decorator("2. 实例方法")
  method(@decorator("1. 实例方法的参数") foo) {}

  @decorator("3. 实例属性")
  prop?: number;
}

// "执行: ",  "1. 实例方法的参数"
// "执行: ",  "2. 实例方法"
// "执行: ",  "3. 实例属性"
// "执行: ",  "4. 静态属性"
// "执行: ",  "6. 静态方法的参数"
// "执行: ",  "5. 静态方法"
// "执行: ",  "7. 构造器的参数"
// "执行: ",  "8. 类"

总结

利用装饰器可以在不修改原有代码情况下,对功能进行扩展。同时,也可以把“辅助性功能逻辑”从“业务逻辑”中分离,解耦出来。而且装饰类和被装饰类可以独立发展,不会相互耦合。装饰模式是 Class 继承的一个替代模式,可以理解成组合。但是,若滥用装饰器,导致装饰器层次过多,不仅会增加调试追溯 bug 的成本,也会增加理解代码本身逻辑的难度。

因此,装饰器的功能逻辑代码应该是辅助性的,这样才符合 AOP 面向切面编程的思想,此外,装饰器语法标准化的过程还需要很长时间。

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值