JS中装饰器到底是什么?

1,839 阅读4分钟

因主要的技术栈是Angular,对于Angular采用的装饰器特别认可,是一种优雅的拦截JS的方式。

TC39的装饰器提案其实共3个:class类和类属性装饰器、function函数装饰器、parameter方法参数装饰器,后2个仍处于Stage 0中,因此本文只针对class装饰器

目前Decorator仍处于Stage 2的阶段,不知道能否在ES2019(ES10)中推出,但一个提案只要能进入Stage 2,就基本会包括在以后的正式标准里面。

有N多文章写道Decorator是ES2016(ES7)推出的,不知道这是从哪里流传出来的,ES2016最终特性根本就没有Decorator,可能的原因:Decorator只是有望在ES2016推出的,实际上并没有。

image


以Angular中的一个组件为例,来说明装饰器的主要用法和装饰器到底是什么:

@Component({
  selector: 'app-transfer-common',
  templateUrl: './transfer-common.component.html',
  styleUrls: ['./transfer-common.component.scss']
})
@AutoUnsubscribe()
export class TransferCommonComponent implements OnInit {
  whichRouter: String;
  btnShowStatus: ShowOrHideBtn;
  queryParamsSubscribe: any;
  userInfo: any = this.commonUserService.getUserInfo(); 
  i18ns_common;
  constructor(
    private router: Router,
    private route: ActivatedRoute,
    private returnVariousBtn: ReturnVariousRouterParamsService,
    private commonUserService: CommonUserService,
    private httpService: HttpService,
    private translate: TranslateService,
  ) { }

  ngOnInit() {
  }

  // 跳转到新增
  @getProperty
  gotoAdd() {
  }

  /* ngOnDestroy() {
    this.queryParamsSubscribe.unsubscribe();

  } */
}

一.类装饰器

AutoUnsubscribe是一个自动取消订阅的装饰器,可传入参数指定某个可订阅对象不自动取消订阅。如不需要指定,可取消外层高阶函数

export function AutoUnsubscribe(params: string) {
  return function (constructor) {
    console.log(constructor);
    console.log(constructor.prototype);
    const originNgDestory = constructor.prototype.ngOnDestroy;
    constructor.prototype.ngOnDestroy = function () {
      console.log(this);
      console.log(this.constructor);
      console.log(constructor);
      for (const property of Object.values(this)) {
        if (property && (typeof property.unsubscribe === 'function')) {
          console.log(property);
        }
      }
      originNgDestory && typeof originNgDestory === 'function' && originNgDestory.apply(this);
    };
  };
}

1.类装饰器仅仅是一个接受一个参数的、被装饰的类的构造函数,常用于修改、添加类的原型方法

2.类装饰器传入的constructor就是类的constructor(特指constructor(){}),如下图红色部分.类装饰器作用于类的 constructor,并且观察、修改或者替换一个类的定义。

3.this是整个class,如图天蓝色外框 4.特殊的是:类的属性方法在类的__proto__中,其他的是类的直接子集,如图靑蓝色部分

4.参数constructor === this.constructor

5.执行订阅的时候,订阅的是一个Subscribe对象,property.unsubscribe是从原型链上获取的unsubscribe,如下图绿色部分

image

二.类属性(方法)装饰器

// 下一个事件队列中执行的装饰器
export const timeoutDecorator = function (milliseconds: number = 0) {
  return function (target, key, descriptor: any) {
    console.log(target);  // 即整个对象
    console.log(key);     // 装饰器修饰的某个key
    console.log(descriptor);
    const originalMethod = descriptor.value;  // value即这个key对应的方法
    
    /** 当执行gotoAdd方法时,即先执行以下函数 */
    descriptor.value = function (...args) {
      console.log(args);
      setTimeout(() => {
        originalMethod.apply(this, args);       // gotoAdd方法调用
      }, milliseconds);
    };
    console.log(descriptor);
    return descriptor;
  };
};

export const getProperty = function (target, name, descriptor) {
  console.log(target);
  console.log(name);
  console.log(descriptor);
  const originalMethod = descriptor.value;
  descriptor.value = function (...args) {
    console.log(args);
    originalMethod.apply(this, args);
  };
  console.log(descriptor);
  return descriptor;
};

image

1.类方法装饰器是一个函数,函数参数就是Object.defineProperty中的三个参数即:target(目标对象)、key(调用的属性名)、descriptor(调用的属性的描述,包括configurable、enumerable、writable、value)

2.类方法装饰器的作用是把类中的方法放入装饰器中执行,个人理解类似于管道或者拦截器

3.类的普通属性(不是方法的)也可以添加装饰器,比如Angular中的@ViewChild,@ViewContent

三.修饰器本质就是编译时执行的函数

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

表现上:添加上装饰器后,装饰器中descriptor.value = function(...args) ...... 函数外的内容会先执行.然后在每次触发类中的修饰的方法是会才会调用descriptor.value = function(...args) ...... 中的内容,args是指触发此方法时的实际参数.再通过apply方法,调用需要修饰的类方法,注意不一定是指向this,也有可能是target,具体看情况

四.特别注意:this

箭头函数中的this和普通函数的this是不同的,注意函数体内是否有this,有的话就不要用箭头函数了,详情请搜索箭头函数的this


划重点

1.类装饰器和类方法装饰器是不同的,但本质都是一个函数
2.类装饰器常用于修改、添加类的原型方法,类方法装饰器用于拦截类方法,需要掌握具体写法
3.注意箭头函数和this的搭配使用问题

参考资料:

ECMAScript 6 入门

ECMAScript proposals

decorator