javaScript---decorator学习笔记

154 阅读2分钟

装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。许多面向对象的语言都有这项功能,目前有一个提案将其引入了 ECMAScript

装饰器是一种函数,写成@ + 函数名,可以放在类和类方法的定义前面。

装饰器目前浏览器与nodeJs均不支持,需要使用@babel/plugin-proposal-decorators转译下。

基本用法

可以用装饰器来修饰整个类

@name
class Person {
    //
}

function name(target){
    target.age = '18'
}

console.log(Person.age)
//18

name就是一个装饰器,用来装饰整个Person类,为其添加上age属性,装饰器参数target就是我们要装饰的目标类。装饰器的行为也可以理解成这样:

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

如果要为装饰器传递更多的参数,可以在装饰器外面封装一层函数:

@name('20')
class Person {
    //
}


function name(age){
    return function(target){
        target.age = age
    }
    
}

console.log(Person.age)
//20

装饰器不经可以装饰类,也可以装饰类的属性:

class Person {
    //
    @job
    getJob() {
        console.log(this.job)
    }
}

function job(target,name,descriptor){
    target.job = 'boss'
}
p.getJob()
//'boss'

装饰器一共接受三个参数,第一个是类的原型对象,第二个是要装饰的属性名,第三个是改属性的描述对象。

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function() {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

const math = new Math();

math.add(2, 4);

上面这个例子是可以起到日志输出的作用。

应用
模拟混入

可以利用装饰器实现混入模式,混入就是一个对象之中混入另外一个对象的方法。可以利用Object.assign方法来实现,该方法将对象的可枚举属性添加到目标方法。

function mixins(...list) {
    return function (target) {
        Object.assign(target.prototype, ...list);
    };
}

const Foo = {
    foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass { }

let obj = new MyClass();
obj.foo() // "foo"
监控函数执行时间

我们现在有一个需求,要监控一个函数的执行时间。可以硬编码,如下:

class Model1 {
    getData() {
        let start = new Date().valueOf()
        try {
            // 此处省略获取数据的逻辑
            return [{
                id: 1,
                name: 'Niko'
            }, {
                id: 2,
                name: 'Bellic'
            }]
        } finally {
            let end = new Date().valueOf()
            console.log(`start: ${start} end: ${end} consume: ${end - start}`)
        }
    }
}

console.log(new Model1().getData())

//start: 1640248210669 end: 1640248210669 consume: 0
//[ { id: 1, name: 'Niko' }, { id: 2, name: 'Bellic' } ]

这种监控代码与原代码没有任何关系,对原函数算是破坏性修改,如果不用装饰器@语法,可以在目标类外部包裹一个函数:

class Model1 {
  getData() {
    // 此处省略获取数据的逻辑
    return [{
      id: 1,
      name: 'Niko'
    }, {
      id: 2,
      name: 'Bellic'
    }]
  }
}

function wrap(Model, key) {
  // 获取Class对应的原型
  let target = Model.prototype

  // 获取函数对应的描述符
  let descriptor = Object.getOwnPropertyDescriptor(target, key)

  // 生成新的函数,添加耗时统计逻辑
  let log = function (...arg) {
    let start = new Date().valueOf()
    try {
      return descriptor.value.apply(this, arg) // 调用之前的函数
    } finally {
      let end = new Date().valueOf()
      console.log(`start: ${start} end: ${end} consume: ${end - start}`)
    }
  }

  // 将修改后的函数重新定义到原型链上
  Object.defineProperty(target, key, {
    ...descriptor,
    value: log      // 覆盖描述重的value
  })
}

wrap(Model1, 'getData')

console.log(new Model1().getData())


//start: 1640313418991 end: 1640313418991 consume: 0
//[ { id: 1, name: 'Niko' }, { id: 2, name: 'Bellic' } ]

这里定义了一个wrap函数,传入目标类和目标方法,在wrap函数中重写了目标方法,将getData转换成了log方法,随后使用defineProperty方法进行覆盖,随后调用getData实际上就是调用的log方法。这就是装饰器模式。然而这种方式有点小繁琐,可以使用@语法更加简洁:

@log('getData')
class Model1 {
  getData(id) {
    // 此处省略获取数据的逻辑
    return [{
      id: id,
      name: 'Niko'
    }, {
      id: id,
      name: 'Bellic'
    }]
  }
}

function log(key){
  return function(target){
    // 获取函数对应的描述符
    let descriptor = Object.getOwnPropertyDescriptor(target.prototype, key)
    // 生成新的函数,添加耗时统计逻辑
    let log = function (...arg) {
      let start = new Date().valueOf()
      try {
        return descriptor.value.apply(this, arg) // 调用之前的函数
      } finally {
        let end = new Date().valueOf()
        console.log(`start: ${start} end: ${end} consume: ${end - start}`)
      }
    }
    // 将修改后的函数重新定义到原型链上
    Object.defineProperty(target.prototype, key, {
      ...descriptor,
      value: log      // 覆盖描述符重的value
    })
  }
}

console.log(new Model1().getData(3))