es7装饰器(修饰符/语法糖)的使用详解以及使用场景

2,079 阅读5分钟

Decorator(修饰器),用来修改原类/方法的功能。

我们对装饰器并不陌生,例如 dvajs的 @connect写法

// 将 model 和 component 串联起来

@connect(({ user }) => ({
    currentUser: user.currentUser
}))

export default class BasicLayout extends React.PureComponent { 
    // ...
}

网上例举了很多假设场景,我这里属于实际应用到,所以写下文章记录下。看下面的例子就能懂了。

一、基本知识:

装饰器本身就是一个普通函数 可以装饰类、类的方法和属性 如果装饰方法,装饰器会先运行,完成后再运行原本方法

一、最简单的装饰器(装饰类):

注意:装饰类的时候,只在编译时候

const myDec = (target) => {
  target.aaa = 1
}

@myDec
class test1 {

}

上面的效果等价于

class test1 {
  static aaa = 1
}

二、装饰类里面的方法:

 //装饰器
const changeParams = () => {  
 //  三个参数分别是:
 // 装饰的方法的类本身(可以理解为class的this)
 //装饰的方法名称
 //可以理解为Object.defineProperty()
  return (target, name, desc) => {  
    const fun = desc.value   //保存一份源方法
     //这里获取到原方法的参数!也可以用 arguments 取得全部参数
    desc.value = function (params) {  
        //do something 
       return fun(params)
    }
  }
}

class test {
  //  放在类里面的方法上面
  @changeParams ()    
      someFunction () {
  } 
}

装饰器的基本写法,万变不离其中。

二、基本使用:

场景:某位A同学写的代码,myClass有很多方法getList1、getList2、getList3....,方法里面有很多复杂代码,最终目的需要把其中几个方法传进来的随机小写字母变为大写。在保持原方法不变情况下,使用装饰器来做。

1、需要修改的类里面的方法(使用前)

class myClass {

  getList1(params) {
    // some code
    console.log('params', params)
    return params
  }

  getList2(params) {
    // some code
    console.log('params', params)
    return params
  }

  //  ...some other method
}

const { getList1 , getList2 } = new myClass()
getList1('aaa')
getList2('abc')

//分别输出 aaa     abc

2、使用装饰器修改(使用后)

/**
 * 写一个装饰器
 * @returns {(function(*=, *=, *=): void)|*}
 */
const changeParams = () => {
  return (target, name, desc) => {
    const fun = desc.value   //保存源方法
    desc.value = function (params) {
      let translate = params.toUpperCase()   //修改参数,大小写转换
      return fun(translate)   // 返回要运行的方法
    }
  }
}

class myClass {

  @changeParams()    //这里使用装饰器!
  getList1(params) {
    // some code
    console.log('params', params)
    return params
  }

  @changeParams()    //这里使用装饰器!
  getList2(params) {
    // some code
    console.log('params', params)
    return params
  }

  //  ...some other method
}

const { getList1, getList2 } = new myClass()
getList1('aaa')
getList2('abc')

//分别输出 AAA     ABC

3、进阶使用(接上面的修改)

此时有个需求,我想getList1方法返回大写+小写,getList2返回大写,怎么办,我是不是还要再写一个装饰器?这时只需要给装饰器加个参数判断就可以

/**
 * 写一个装饰器
 * @returns {(function(*=, *=, *=): void)|*}
 */
const changeParams = (shouldUpAndLower) => {
  return (target, name, desc) => {
    const fun = desc.value
    desc.value = function (params) {
      let translate = shouldUpAndLower ? params.toUpperCase() + params : params.toUpperCase()  //装饰器的参数在这里做判断
      return fun(translate)
    }
  }
}

class myClass {

  @changeParams(true)    //这里使用装饰器!
  getList1(params) {
    // some code
    console.log('params', params)
    return params
  }

  @changeParams(false)    //这里使用装饰器!
  getList2(params) {
    // some code
    console.log('params', params)
    return params
  }

  //  ...some other method
}

const { getList1, getList2 } = new myClass()
getList1('aaa')
getList2('abc')

//分别输出   AAAaaa    ABC

这样即可完成。

this指向问题:

注意,上面的myClass 中方法我们用的不是箭头函数,那么就有可能出现,使用this 时候 this丢失的情况。 例如假设原本方法getList1有用到类内部的this,此时会出现报错: TypeError: Cannot read property 'data' of undefined 解决办法是,在装饰器里绑定this,(或者将方法改为箭头函数,但是我们写装饰器的原则就是为了不改变原方法,所以不推荐)。 完善一下装饰器,添加this绑定

/**
 * 写一个装饰器
 * @returns {(function(*=, *=, *=): void)|*}
 */
const changeParams = () => {
  return (target, name, desc) => {
    const fun = desc.value
    desc.value = function (params) {
      let translate = params.toUpperCase()
      return fun.call(target, translate)   //绑定this,target指向方法的类本身
    }
  }
}

三、在项目中的实际应用场景例举:

1、前端传参需要将驼峰转为下划线传给后端,且需要将后端返回的数据下划线格式转为驼峰格式。 (备注:后端是能有这个功能直接转换的,这里是我前端做了转换,因为后端为了规范,原来的个别!接口数据格式需要将驼峰转为下划线,所以为了保持原本写的方法不变,这里采用装饰器语法再合适不过了。)

实例全部代码:

----server.js文件(没使用之前)

class server {

  async getTableList(params) {
    return await fetch(`/aaa/bbb/ccc`, { params: params })
  }
   // other fetch....
}
export const {  getShiftList } = new server()

----server.js文件(使用之后)

import { objTranslate } from '@/util/utils'   //引入装饰器方法
class server {

  @objTranslate()   //装饰器,只需要在想转换的接口前面加上装饰器就可以
  async getTableList(params) {
    return await fetch(`/aaa/bbb/ccc`, { params: params })
  }
   // other fetch....
}
export const {  getShiftList } = new server()

-----utils文件

/**
 * 是否需要转换数据,下划线/驼峰
 * @param resTranslate 返回结果是否需要转换
 * @param paramsTranslate 入参是否转换
 * @returns {(function(*, *, *): void)|*}
 */
export const objTranslate = ({ resTranslate = true,  paramsTranslate = true } = {}) => {
  return function (target, name, desc) {
    const fun = desc.value  //记录原函数
    desc.value = async function (params, params2) {  //第二个参数一般接口自取字段,不需要处理
      if (!params) {
        return resTranslate ? await lineToCamel(fun.call(target)) : await fun.call(target)
      }
      const translateParams = paramsTranslate ? objectHumpToLine({ ...params }) : params
      return resTranslate ? lineToCamel(await fun.call(target, translateParams, params2)) :
               await fun.call(target, translateParams, params2)
    }
  }
}

/**
 * 驼峰转下划线
 * @param data
 * @returns {{}|*}
 */
export function objectHumpToLine(data) {
  if (typeof data != 'object' || !data) return data
  if (Array.isArray(data)) {
    return data.map(item => objectHumpToLine(item))
  }
  const newData = {}
  for (let key in data) {
    let newKey = key.replace(/([A-Z])/g, (p, m) => `_${m.toLowerCase()}`)
    newData[newKey] = objectHumpToLine(data[key])
  }
  return newData
}

/**
 * 下划线转驼峰
 * @param data
 * @returns {{}|*}
 */
export function lineToCamel(data) {
  if (typeof data != 'object' || !data) return data
  if (Array.isArray(data)) {
    return data.map(item => lineToCamel(item))
  }
  const newData = {}
  for (let key in data) {
    let newKey = key.replace(/_([a-z])/g, (p, m) => m.toUpperCase())
    newData[newKey] = lineToCamel(data[key])
  }
  return newData
}

2、部分接口数据需要先判断查询本地有无缓存,再做是否请求的动作(应用端时候,节流用) 不例举

注意:

如果装饰纯函数会有变量提升问题 这就是为什么你在react的纯函数组件里面不能使用@connect语法糖写法的原因!

总结:

优点:该语法最好的好处是不用修改原来的代码,就可以修改原来的方法。

缺点:刚接触有点隐涩难懂。