装饰器模式(Decorator模式)理解、使用

简介

在一些框架(比如 nest.jsmidway.js 等)中,经常看到在类代码的附近有 @...... 这种代码,这就是 Decorator

Decorator 可以叫做修饰器,或者是装饰器

修饰器是一种特殊类型的声明,它只能够被附加到类的声明、方法、属性或参数上,可以修改类的行为。但不能用于函数(因为存在函数提升)

常见的修饰器有:类修饰器属性修饰器方法修饰器参数修饰器

修饰器写法:普通修饰器(无法传参)、修饰器工厂(可传参)

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

修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类

修饰器的行为类似如下

@decorator
class A {}

// 等同于
class A {}
A = decorator(A) || A

类修饰器

类修饰器在类声明之前被声明(紧靠着类声明)

类修饰器应用于类构造函数,可以用来监视,修改或替换类定义

  • 类修饰器示例
function logClass (target) {
  console.log(target)                                       // [Function: MyTest] target指向修饰的类
  target.nTest = '扩展的静态属性'                     // 扩展静态属性
  target.prototype.nName = '动态扩展的属性' // 给原型扩展属性
  target.prototype.nFun = () => {
    console.log('动态扩展的方法')
  }
}

@logClass
class MyTest {}

const test = new MyTest()
console.log(test.nTest)  // 扩展的静态属性
console.log(test.nName)  // 动态扩展的属性
test.nFun()              // 动态扩展的方法
  • 修饰器工厂(闭包传参)
function logClass (params) {
  return function (target) {
    console.log(target)      // [Function: MyTest]
    console.log(params)      // hello
    target.prototype.nName = '动态扩展的属性'
    target.prototype.nFun = () => {
      console.log('动态扩展的方法')
    }
  }
}

@logClass('hello')
class MyTest {}

const test = new MyTest()
console.log(test.nName)  // 动态扩展的属性
test.nFun()              // 动态扩展的方法
  • Mixins 混入例子
// mixins.js  可以返回一个函数
export function mixins(...list) {
  return function(target) {
    Object.assign( target.prototype, ...list )
  }
}

// main.js
import { mixins } from './mixins.js'
const Foo = {
  foo() {console.log('foo')}
}

@mixins(Foo)  // 当函数调用,传入参数
class MyClass {}

const obj = new MyClass()
obj.foo             // 'foo'
  • 重载构造函数的例子
function logClass (target) {
  return class extends target {
    attr = '重载属性'

    getData () {
      console.log('重载方法', this.attr)
    }
  }
}

@logClass
class MyTest {
  attr

  constructor () {
    this.attr = '构造函数的属性'
  }

  getData () {
    console.log(this.attr)
  }
}

const test = new MyTest()
test.getData()      // 重载方法 重载属性
  • React + Redux 例子
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

// 可改写成
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

属性修饰器

属性装饰器表达式会在运行时当作函数被调用,传入下列 2 个参数

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. 修饰的属性名
function logProperty (params) {
  return function (target, name) {
    console.log(target, name)  // MyTest { getData: [Function] } 'attr'
    target[name] = params
  }
}

class MyTest {
  @logProperty('属性修饰器的参数')
  attr

  constructor () {}

  getData () {
    console.log(this.attr) // 属性修饰器的参数
  }
}

const test = new MyTest()
test.getData()

方法修饰器

它会被应用到方法的属性描述符上,可以用来监听、修改或者替换方法定义

修饰器会修改属性的描述对象,然后被修改的描述对象再用来定义属性

方法修饰器会在运行时传入下列 3 个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. 修饰的属性名
  3. 该属性的描述对象
  • 方法修饰器示例
// 方法修饰器
function logFunction (params) {
  return function (target, methodName, desc) {
    console.log(target, methodName, desc)
    /*
    MyTest { getData: [Function] }
    'getData'
    {
      value: [Function], // 值
      writable: true, // 可读
      enumerable: true, // 可枚举
      configurable: true // 可设置
      }
    */

    target.nName = '动态扩展的属性'
    target.nFun = () => {
      console.log('动态扩展的方法')
    }

    // 将接收到的参数改为 string 类型
    const oMethod = desc.value
    desc.value = function (...args) {
      args = args.map((v) => {
        return String(v)
      })
      return oMethod.apply(this, args)
    }
  }
}

class MyTest {
  @logFunction('方法修饰器的参数')
  getData (...args) {
    console.log(args)
  }
}

const test = new MyTest()
console.log(test.nName)              // 动态扩展的属性
test.nFun()                                    // 动态扩展的方法
test.getData(123, '234', () => {})  // [ '123', '234', 'function () { }' ]
  • 输出日志的例子
class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

// @log修饰器的作用就是在执行原始的操作之前,执行一次console.log,从而达到输出日志的目的
function log(target, name, descriptor) {
  const oldValue = descriptor.value;

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

  return descriptor;
}

const math = new Math();

// passed parameters should get logged now
math.add(2, 4);
  • 修饰器有注释的作用,看上去一目了然
@Component({
  tag: 'my-component',
  styleUrl: 'my-component.scss'
})
export class MyComponent {
  @Prop() first: string;  // props
  @Prop() last: string;   // props
  @State() isVisible: boolean = true; // props

  render() {
    return (
      <p>Hello, my name is {this.first} {this.last}</p>
    );
  }
}

参数修饰器

参数修饰器表达式会在运行时当作函数被调用,可以使用参数修饰器为类的原型增加一些元素数据,传入下列 3 个参数

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  2. 参数的名字
  3. 参数在函数参数列表中的索引
function logParams (params) {
  return function (target, methodName, paramsIndex) {
    // MyTest { getData: [Function] } 'getData' 0
    console.log(target, methodName, paramsIndex)
    target.param = params
  }
}

class MyTest {
  getData (@logParams('参数修饰符的参数') id) {
    console.log(id)
  }
}

const test = new MyTest()
test.getData(123)
console.log(test.param) // 参数修饰符的参数

执行顺序

属性修饰器 > 参数修饰器 > 方法修饰器 > 类修饰器

function log (params) {
  return function () {
    console.log(params)
  }
}

@log('类修饰器')
class MyTest {
  @log('属性修饰器')
  id

  @log('方法修饰器')
  getData (@log('参数修饰器') id) {
    this.id = id
  }
}

new MyTest()
  • 同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行
function dec(id){
  console.log('evaluated', id);
  return (target, property, descriptor) => console.log('executed', id);
}

class Example {
  @dec(1)
  @dec(2)
  method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1

不能用于函数

修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。

// 意图是执行后counter等于 1,但是实际上结果是counter等于 0
var counter = 0;

var add = function () {
  counter++;
};

@add
function foo() {
}

上面的代码,函数提升后相当于

@add
function foo() {
}

var counter;
var add;

counter = 0;

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

推荐阅读更多精彩内容

  • 目的: iOS APP在下一个版本会用swift开发,在多人开发中,即使有官网的规范模板.但每个人的代码风格和规...
    nick5683阅读 1,799评论 0 1
  • 目的: iOS APP在下一个版本会用swift开发,在多人开发中,即使有官网的规范模板.但每个人的代码风格和规...
    技术进阶在路上阅读 12,813评论 2 15
  • 随着转译变得司空见惯,我们会经常在一些实际代码或教程中遇到新的语言特性。这些特性中,装饰器绝对是让人第一次碰到时会...
    shallynon阅读 7,664评论 0 2
  • 类是一个重要的C#编程概念,它在一个单元内定义了表示和行为。类提供了面向对象编程和面向组件编程所需的语言支持,是创...
    CarlDonitz阅读 839评论 0 2
  • 装饰器 概念 装饰器是一种特殊类型的声明,他能够被附加到类声明,方法,属性或者参数上。可以修改类的行为。常见的装饰...
    TouchMe丶阅读 519评论 0 0