js装饰器@Decorator

随着 ES6 和 TypeScript 中类的引入,使得我们在多个不同类之间共享或者扩展一些方法或者行为的时候,变得并不是那么优雅。在某些场景需要在不改变原有类和类属性的基础上扩展些功能,这也是装饰器出现的原因。

装饰器简介

装饰器接收一个参数,也就是我们被装饰的目标方法,处理完扩展的内容后再返回一个方法,供以后调用,同时也失去了对原方法对象的访问。

当我们对某个方法应用了装饰之后,其实就是改变了被装饰方法的入口引用,使其重新指向了装饰器返回的方法的入口点,从而来实现对原函数的扩展、修改等操作

不过装饰器模式仍处于第 2 阶段提案中,使用它之前需要使用 babel 模块 transform-decorators-legacy 编译成 ES5 或 ES6。

.babelrc中

"plugins": [

  • "transform-decorators-legacy"*

]

ES7的装饰器decorator是依赖于ES5的Object.defineProperty方法

相关知识:Object.defineProperty

Object.defineProperty()在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

语法:
Object.defineProperty(obj, prop, descriptor)

obj:操作的对象
prop:被定义或者修改的属性名称
descriptor:将被定义或修改的属性描述符
返回值:被传递给函数的对象

属性描述符:descriptor

对象中目前存在的属性描述符有2种:数据描述符和存取描述符
1、数据描述符:描述属性的值和值是否可被赋值运算符改变
2、存取描述符:由getter、setter函数对属性的描述
属性描述符必须是上述两者之一;且不可同时是两者

属性描述符通用键值(即数据描述符和存取描述符都有的键值):
1、configurable:configurable特性表示对象的属性是否可以被删除,以及除value和writable特性外的其他特性是否可以被修改。默认值false,即不可改变
2、enumerable:定义了当前操作的这个属性是否可以for...in和Object.key()中被枚举。设为true时,该属性才能出现在对象的枚举属性中。默认值false,即不可被枚举

数据描述符特有的键值:
1、value:该属性对应的值,可以是任意有效的javascript值(string,number,object,function等等)。默认值undefined
2、writable:当且仅当writable为true时,value才能被赋值运算符改变。默认值false,即不可被改变

let o = {};
o.a = 1;
// 等同于 :
Object.defineProperty(o, "a", {
  value: 1,
  writable: true,
  configurable: true,
  enumerable: true
});
// 另一方面,
Object.defineProperty(o, "a", {value: 1});
// 等同于 :
Object.defineProperty(o, "a", {
  value: 1,
  writable: false,
  configurable: false,
  enumerable: false
});

存取描述符特有的键值:
1、get:一个给属性提供getter的方法,如果没有getter则为undefined。当访问该属性时get方法会执行,方法执行时没有参数传入,但会传入this对象(由于继承关系,此this不一定是定义改属性的对象)
2、set:一个给属性提供setter的方法,如果没有setter则为undefined。当属性值修改时set方法会执行,该方法将接收唯一参数,即该属性新的参数值

let obj = {}
let num = 30
Object.defineProperty(obj, 'id', {
    configurable: true,
    enumerable: true,
    get: () => num,
    set: (newValue) => {
        num = newValue
    }
})
console.info(obj.id, num) // 30 30
obj.id = 20
console.info(obj.id, num) // 20 20
num = 40
console.info(obj.id, num) // 40 40

装饰器

一、作用于类的装饰器

当装饰的对象是类时,我们操作的就是这个类本身。

类的装饰器函数的第一个参数,就是所有装饰的目标类

装饰器对类的行为的改变是代码编译时发生的,而不是在运行时。这意味着,装饰器能够在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数

例子🌰:

1、simple class decorator

in decorator.js

// 类的装饰器
export const classDecorator = (target) => {
  // 此处的target为类本身
  target.a = true // 给类添加一个静态属性
}

in index.js

@classDecorator
export class ClassA {
  constructor() {
    this.a = 1
  }
  a = 2
}
console.info('ClassA.a: ', ClassA.a) // true
2、class decorator with params 传参的类装饰器

in decorator.js

// 传参的类的装饰器
export const classDecoratorWithParams = (params = true) => (target) => {
  target.a = params
}

in index.js

@classDecoratorWithParams(false)
export class ClassB {
  constructor() {
    this.a = 1
  }
  fun = () => {
    console.info('fun中ClassB.a: ', this.a, ClassB.a) // 1, false
  }
}
console.info('ClassB.a: ', ClassB.a) // false
const classB = new ClassB()
console.info('new ClassB().a: ', classB.a) // 1
classB.fun()
3、class decorator add prototype 给修饰类添加实例属性

in decorator.js

// 类的装饰器(给类添加实例属性)
export const classDecoratorAddPrototype = prototypeList => (target) => {
  target.prototype = { ...target.prototype, ...prototypeList }
  target.prototype.logger = () => console.info(`${target.name} 被调用`) // target.name即获得类的名
}

in index.js

@classDecoratorAddPrototype({ fn() { console.info('fnfnfn') } }) // 此处不能使用箭头函数?
export class ClassC {
  constructor() {
    this.a = 1
  }
}
// console.info('ClassC.fn: ', ClassC.fn()) // 报错,fn不在ClassC的静态属性上
const classC = new ClassC()
classC.fn()
classC.logger()

例子github:https://github.com/zzsscc/decorators

在redux中我们经常使用react-redux的connect装饰器即为作用于类的装饰器

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
export default class MyComponent extends React.Component {}

相当于
class MyComponent extends React.Component {}
export default connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(MyComponent)

二、作用于类方法的装饰器

与装饰类不同,对类方法的装饰本质是操作其描述符

可以把此时的装饰器理解成是 Object.defineProperty(obj, prop, descriptor) 的语法糖

例子🌰:

1、class function decorator

in decorator.js

// 方法的装饰器
export const funDecorator = (params = { readonly: true }) => (target, prototypeKey, descriptor) => {
  /*
    此处target为类的原型对象,即方法Class.prototype
    ps:装饰器的本意是要装饰类的实例,但此时实例还未生成,所以只能装饰类的原型
   */
  /*
    prototypeKey为要装饰的方法(属性名)
   */
  /*
    descriptor为要修饰的方法(属性名)的描述符,即(默认值为):
    {
      value: specifiedFunction,
      enumerable: false,
      configurable: true,
      writable: true
    }
   */

  // 实现一个传参的readonly,修改描述符的writable
  descriptor.writable = !params.readonly
  // 返回这个新的描述符
  return descriptor
}

/*
  调用funDecorator(Class.prototype, prototypeKey, descriptor)
  相当于
  Object.defineProperty(Class.prototype, prototypeKey, descriptor)
  */

in index.js

export class ClassD {
  constructor() {
    this.a = 1
  }

  @funDecorator()
  fun = (tag) => {
    this.a = 2
    console.info(`this.a ${tag}`, this.a)
  }
}
const classD = new ClassD()
classD.fun('first')

// 报错,无法改变classD.fun,因为他的描述符descriptor.writable已经被装饰器修改为false
try {
  classD.fun = (tag) => {
    console.info(`this.a changed ${tag}`)
  }
  classD.fun('sec')
} catch (err) {
  throw new Error(err)
}
2、fun enhance(front/end) decorator

in decorator.js

// 方法的装饰器(在方法执行的前后添加操作:如show/hide loading)
export const funEnhanceDecorator = (params = {}) => (target, prototypeKey, descriptor) => {
  // 默认需要showLoading
  const { showLoading = true } = params
  const oldValue = descriptor.value
  descriptor.value = async function A(...args) {
    try {
      showLoading && console.info('加载中')
      const result = await oldValue.apply(this, args)
      console.info('hide')
      return result
    } catch (err) {
      console.info('hide')
      console.error(err)
      return null
    }
  };
  return descriptor
}

in index.js

export class ClassE {
  constructor() {
    this.result = {}
  }

  afun = (params) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(params.id)
      }, 2000)
    })
  }

  @funEnhanceDecorator()
  async fun(params = {}) { // 不能使用箭头函数?
    const result = await this.afun(params)
    console.info(result)
  }
}
const classE = new ClassE()
classE.fun({ id: 100 })
3、test decorators sequence多个装饰器的包装顺序

in decorator.js

// time => 计数and计时
const labels = {};
// Exported for mocking in tests
export const defaultConsole = {
  time: console.time ? console.time.bind(console) : (label) => {
    labels[label] = new Date();
  },
  timeEnd: console.timeEnd ? console.timeEnd.bind(console) : (label) => {
    const timeNow = new Date();
    const timeTaken = timeNow - labels[label];
    delete labels[label];
    console.info(`${label}: ${timeTaken}ms`);
  }
};
let count = 0;

export const time = (params = { prefix: null, console: defaultConsole }) => (target, prototypeKey, descriptor) => {
  const fn = descriptor.value
  let { prefix } = params
  const { console } = params
  if (prefix === null) {
    prefix = `${target.constructor.name}.${prototypeKey}`
  }

  if (typeof fn !== 'function') {
    throw new SyntaxError(`@time can only be used on functions, not: ${fn}`)
  }

  return {
    ...descriptor,
    async value(...args) {
      const label = `${prefix}-${count}`
      count += 1
      console.time(label)

      try {
        return await fn.apply(this, args)
      } finally {
        console.timeEnd(label)
      }
    }
  }
}

// deprecate => 标记废弃
const DEFAULT_MSG = 'This function will be removed in future versions.'
export const deprecate = (params = { options: {} }) => (target, prototypeKey, descriptor) => {
  if (typeof descriptor.value !== 'function') {
    throw new SyntaxError('Only functions can be marked as deprecated')
  }

  const methodSignature = `${target.constructor.name}#${prototypeKey}`
  let { msg = DEFAULT_MSG } = params
  const { options } = params

  if (options.url) {
    msg += `\n\n    See ${options.url} for more details.\n\n`;
  }

  return {
    ...descriptor,
    value(...args) {
      console.warn(`DEPRECATION ${methodSignature}: ${msg}`)
      return descriptor.value.apply(this, args)
    }
  }
}

// test sequence 测试顺序
export const testSequence1 = (params = {}) => (target, prototypeKey, descriptor) => {
  const oldValue = descriptor.value
  return {
    ...descriptor,
    value(...args) {
      console.log('test1')
      oldValue.apply(this, args)
    }
  }
}

export const testSequence2 = (params = {}) => (target, prototypeKey, descriptor) => {
  const oldValue = descriptor.value
  return {
    ...descriptor,
    value(...args) {
      console.log('test2')
      oldValue.apply(this, args)
    }
  }
}

in index.js

export class ClassF {
  constructor() {
    this.result = {}
  }

  @time()
  @deprecate({ options: { url: 'https://github.com/zzsscc' } })
  @testSequence1()
  @testSequence2()
  fun() {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(this.result)
      }, 3000)
    })
  }
}
const classf = new ClassF()
classf.fun()
classf.fun()

三、core-decorators.js

提供了一些常用的装饰器方法
code view更有助于你理解装饰器

https://github.com/jayphelps/core-decorators

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容