前端冷知识-装饰器

7,728 阅读6分钟

装饰器这个名词,如果不是写angularnest的其他前端同学应该都不怎么熟悉,简单来说,装饰器就是函数,提供了某种特定的功能,用于描述方法属性参数,为其添加更加强大的功能,同时与原有逻辑进行解耦,算是aop编程的一种实现。

或者说可能在平时有用过一些

例如react中使用redux的时候,有用过@connect

@connect(mapStateToProps, mapDispatchToProps)
class Index extends React.Component{}

vue2中,使用过vue-property-decorator插件

@Component({})
class Index extends Vue {}

今天,我就来带大家看看装饰器到底是个什么东西。

javascript中的装饰器提案

javascript是有装饰器这个提案的,但是迟迟无法落地。

最早的时候,装饰器的提案与现在不同,而Typescript早早实现了装饰器,正好angular2彻底使用了Typescript来重构,大量使用了在当时还是提案的装饰器。

但是到现在,装饰器的提案早已与当时不同,被改的面目全非,在这种前提下,angular团队以及后来的nest团队,肯定是不同意新的装饰器提案的。就像当时的Promise A+,也是社区推动,但是在官方实现Promise之前,社区已经有使用了Promise的库,为了兼容这些库,现在对于Promise的判断,现在都是基于thenable这种鸭子类型来进行判断,而不是通过instanceof这种更为精准的从底层进行判断的。

Promise不同,Promise是双重控制反转,重点在于执行顺序,具体如何实现,其实并不是很重要的,人们更在意的是它的用处。而装饰器,是实实在在的代码逻辑层面的,更改某个规则,就意味着整体逻辑可能是完全不一样的。强行推行,对于之前使用angularnest的项目,完全是破坏性的打击,angular团队和nest团队,已经Typescript团队,在tc39上肯定是不愿意通过提案的。这也就导致了装饰器提案的持续搁置。

不过说来也有意思,一般来说,提案被否决之后,都需要重新回到stage1从头来过,但是装饰器却一直在stage2

使用装饰器

在官方没有实现这个提案之前,我们要使用装饰器,通常有两种做法

  • 使用babel插件
  • 使用Typescript

之前说过,Typescript团队,在很早之前就实现了装饰器的功能,因此我们只需要创建一个.ts文件,就可以自由的使用装饰器了,当然,要开启experimentalDecorators选项。

装饰器工厂

在介绍装饰器之前,先简单介绍一个概念——装饰器工厂。

顾名思义,工厂是用来进行组装的地方,装饰器工厂也就是用来组装某些值以及要装饰的东西的。

与普通的装饰器函数相比,它多了一层调用,用于传递要组装的数据,因此装饰器工厂与普通装饰器最大的差别就是它的自定义参数。

类装饰器

类装饰器,声明在class关键字上方。

简单理解,就是将这个类,作为装饰器的参数传递进去,在装饰器函数中,可以对这个类进行各种操作。

废话不多说,直接来看代码

@Init
class Index {
  public age = 12
}

function Init<T extends {new (...args: any[]): {}}>(constructor: T) {
  return class extends constructor {
    age = 21
  }
}

console.log(new Index())

// class_1 { age: 21 }

// function Init<T extends new (...args: any[]) => {}>(constructor: T): {
//  new (...args: any[]): (Anonymous class);
//  prototype: Init<any>.(Anonymous class);
// } & T

在实例化这个Index类的时候,同时会调用它的装饰器Init,并将Index传递进去,在此基础上,我们就可以通过这个函数对类进行各种操作。

下面我们来看看类装饰器的装饰器工厂,怎么使用,也就是平时用的@connect这种的方法

@InjectSex('男')
class Two {}

function InjectSex(sex: '男' | '女') {
  return function<T extends {new (...args: any): {}}>(target: T) {
    target.prototype.sex = sex
    return target
  }
}

console.log(Reflect.getPrototypeOf(new Two()))

// { sex: '男' }

方法装饰器

方法装饰器是用于修饰方法的,与类装饰器只有一个target参数不同,方法装饰器共接收三个参数,分别是

  • target 类实例
  • key 方法的名字
  • descriptor 用于描述这个方法的描述符,也就是Object.defineProperty方法的第三个参数中的valuewritableenummerableconfigurable
class Fun {
  @AddOne
  log(x: number) {
    console.log(x)
  }
}

function AddOne(target, key, descriptor) {
  console.log(target, 'target') // { log: [Function (anonymous)] } target
  console.log(key, 'key') // log key
  console.log(descriptor, 'descriptor')
  // {
  //   value: [Function (anonymous)],
  //   writable: true,
  //   enumerable: true,
  //   configurable: true
	// } descriptor
  
  const val = descriptor.value
  descriptor.value = function(...args) {
    return val(args[0] + 1)
  }
  return descriptor
}

const fun = new Fun
fun.log(1)

// 2

我们通过descriptor中的value属性,劫持到原有的方法,并进行重新改写,这样就可以以最小的切入面修改一个现有的方法了。

如果是装饰器工厂的话,我们还是需要在外面包裹一层函数

class FuncTwo {
  @InjectPrefix('托尼-')
  log(x) {
    console.log(x)
  }
}

function InjectPrefix(prefix: string) {
  return function(target, key, descriptor) {
    const val = descriptor.value
    descriptor.value = function(...args) {
      return val(prefix + args[0])
    }
    return descriptor
  }
}

const funcTwo = new FuncTwo
funcTwo.log('斯塔克')

// 托尼-斯塔克

属性装饰器

属性装饰器一般用于属性的劫持,它接收两个参数,分别是target和当前属性的名称,我们可以通过装饰器工厂来向被装饰的属性添加值。

class Prop {
  @init(16)
  age: number
}

function init(age: number) {
  return function(target, key) {
    target[key] = age
    return target
  }
}

const prop = new Prop
console.log(prop.age)

// 16

参数装饰器

参数装饰器接收三个参数,分别是targetkey(当前方法)和index(当前参数的下标)

class Param {
  log(@require name: string, @require age: number) {
    console.log(name, age)
  }
}

function require(target, key, index) {
  console.log(target, key, index)
  return target
}

const param = new Param
param.log('张三', 18)

// { log: [Function (anonymous)] } log 1
// { log: [Function (anonymous)] } log 0
// 张三 18

不过一般都使用方法装饰器来配合其使用,例如下面这个例子

class Param {
  @Validate
  log(@require name?: string, @require age?: number) {
    console.log(name, age)
  }
}

function Validate(target, key, descriptor) {
  const val = descriptor.value
  const required = val.required
  console.log(required) // [0, 1]
  descriptor.value = function(...args) {
    required.forEach(index => {
      if (!args[index]) {
        throw new Error('缺少参数')
      }
    })
    return val(...args)
  }
  return descriptor
}

function require(target, key, index) {
  target[key].required = [index, ...(target[key].required || [])]
  return target
}

const param = new Param
param.log()

// /Users/asarua/Desktop/demo/decorator/params-decorator.ts:13
//    required.forEach(index => {
             ^
// Error: 缺少参数

通过require参数装饰器,向target[key]方法中添加required的参数,然后通过Validate进行校验。

结语

装饰器这个东西,一直都是看java工程师在使用,在每个ControllerService、还有方法中,加一大堆。

其实前端工程师在日常工作中也可以试试用,在一些需要执行log啥的方法中,使用装饰器,可以更好的将无关逻辑进行解耦,更好的进行维护。