Skip to content

装饰器

装饰器(Decorator)用来增强 JavaScript 类(class)的功能

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

js
@frozen
class Foo {
  @configurable(false)
  @enumerable(true)
  method() {}

  @throttle(500)
  expensiveMethod() {}
}

凡是标注“新语法”的章节,都是基于当前的语法,简写为 ”新“。未标注“新语法”的章节基于以前的语法,是过去遗留的稿子。

装饰器的种类(新)

装饰器可以装饰四种类型的值

  • 类的属性(public, private, and static)
  • 类的方法(public, private, and static)
  • 属性存取器(accessor)(public, private, and static)

装饰器 API(新)

装饰器是一个函数

js
type Decorator = (value: Input, context: {
  kind: string;
  name: string | symbol;
  access: {
    get?(): unknown;
    set?(value: unknown): void;
  };
  private?: boolean;
  static?: boolean;
  addInitializer?(initializer: () => void): void;
}) => Output | void;

装饰器函数调用,接受两个参数

  • value:被装饰的值,某些情况下可能是undefined(装饰属性时)
  • context:一个对象,包含了被装饰的值的上下文信息

context对象的属性如下

  • kind:字符串,表示装饰类型,取值classmethodgettersetterfieldaccessor
  • name:被装饰的值的名称: The name of the value, or in the case of private elements the description of it (e.g. the readable name).
  • access:对象,包含访问这个值的方法,即存值器和取值器
  • static: 布尔值,该值是否为静态元素
  • private:布尔值,该值是否为私有元素
  • addInitializer:函数,允许用户增加初始化逻辑

装饰器的执行步骤如下。

  1. 计算各个装饰器的值,按照从左到右,从上到下的顺序
  2. 调用方法装饰器
  3. 调用类装饰器

类的装饰

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

js
@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true
}

MyTestableClass.isTestable // true

代码分析:@testable 是一个装饰器,修改了 MyTestableClass 的行为,添加静态属性 isTestable

装饰器的行为

js
@decorator
class A {}

// 等同于

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

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

一个参数不够,装饰器外封装一层函数

js
function testable(isTestable) {
  return function (target) {
    target.isTestable = isTestable
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

注意:装饰器对类的行为的改变,是代码编译时发生,而不是在运行时

前面例子是为类添加静态属性,添加实例属性,通过目标类操作

js
function testable(target) {
  target.prototype.isTestable = true
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass()
obj.isTestable // true

另外一个例子

js
// 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 {}

let obj = new MyClass()
obj.foo() // 'foo'

React 与 Redux 库结合使用

js
class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent)

装饰器改写

js
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

类装饰器(新语法)

类装饰器的类型描述

js
type ClassDecorator = (
  value: Function,
  context: {
    kind: 'class',
    name: string | undefined,
    addInitializer(initializer: () => void): void
  }
) => Function | void

参数说明

value:被修饰的类

context:上下文对象,修饰匿名类,name 属性为 undefined

js
function logged(value, { kind, name }) {
  if (kind === 'class') {
    return class extends value {
      constructor(...args) {
        super(...args)
        console.log(`constructing an instance of ${name} with arguments ${args.join(', ')}`)
      }
    }
  }

  // ...
}

@logged
class C {}

new C(1)
// constructing an instance of C with arguments 1

方法装饰器(新语法)

方法装饰器

js
type ClassMethodDecorator = (
  value: Function,
  context: {
    kind: 'method',
    name: string | symbol,
    access: { get(): unknown },
    static: boolean,
    private: boolean,
    addInitializer(initializer: () => void): void
  }
) => Function | void

参数说明

value:所要装饰的方法

context:与类装饰器相同

返回值

  1. 返回一个方法,取代原来方法
  2. 不返回值,使用原来方法
  3. 返回其他值,报错
js
function logged(value, { kind, name }) {
  if (kind === 'method') {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(', ')}`)
      const ret = value.call(this, ...args)
      console.log(`ending ${name}`)
      return ret
    }
  }
}

class C {
  @logged
  m(arg) {}
}

new C().m(1)
// starting m with arguments 1
// ending m

装饰器本质为语法糖,实际操作如下

js
class C {
  m(arg) {}
}

C.prototype.m =
  logged(C.prototype.m, {
    kind: 'method',
    name: 'm',
    static: false,
    private: false
  }) ?? C.prototype.m

方法的装饰

装饰器不仅可以装饰类,还可以装饰类的属性

js
class Person {
  @readonly
  name() {
    return `${this.first} ${this.last}`
  }
}

装饰器 readonly 用来装饰 “类” 的 name 方法

装饰器函数可以接受三个参数

target:类的原型对象。本意是装饰实例,但实例还未生成,只能装饰原型。

name:所修饰的属性名

descriptor:属性描述对象

js
function readonly(target, name, descriptor) {
  // descriptor对象原来的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false
  return descriptor
}

readonly(Person.prototype, 'name', descriptor)
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor)

@log 装饰器,可以起到输出日志的作用

js
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()

// passed parameters should get logged now
math.add(2, 4)

使用 Decorator 写法的组件

js
@Component({
  tag: 'my-component',
  styleUrl: 'my-component.scss'
})
export class MyComponent {
  @Prop() first: string
  @Prop() last: string
  @State() isVisible: boolean = true

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

同一个方法有多个装饰器,先从外到内进入,然后由内向外执行

js
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

为什么装饰器不能用于函数?

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

js
var readOnly = require("some-decorator");

@readOnly
function foo() {
}

实际执行

js
var readOnly;

@readOnly
function foo() {
}

readOnly = require("some-decorator");

装饰函数:使用高阶函数形式执行

js
function doSomething(name) {
  console.log('Hello, ' + name)
}

function loggingDecorator(wrapped) {
  return function () {
    console.log('Starting')
    const result = wrapped.apply(this, arguments)
    console.log('Finished')
    return result
  }
}

const wrapped = loggingDecorator(doSomething)

存取器装饰器(新语法)

存取器装饰器使用 TypeScript 描述的类型如下

js
type ClassGetterDecorator = (
  value: Function,
  context: {
    kind: 'getter',
    name: string | symbol,
    access: { get(): unknown },
    static: boolean,
    private: boolean,
    addInitializer(initializer: () => void): void
  }
) => Function | void

type ClassSetterDecorator = (
  value: Function,
  context: {
    kind: 'setter',
    name: string | symbol,
    access: { set(value: unknown): void },
    static: boolean,
    private: boolean,
    addInitializer(initializer: () => void): void
  }
) => Function | void

参数说明

value:原始的存值器(setter)和取值器(getter)

返回值

与方法装饰器等同

存取器装饰器对存值器(setter)和取值器(getter)是分开作用的

js
class C {
  @foo
  get x() {
    // ...
  }

  set x(val) {
    // ...
  }
}

修改 @Logged 装饰器

js
function logged(value, { kind, name }) {
  if (kind === 'method' || kind === 'getter' || kind === 'setter') {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(', ')}`)
      const ret = value.call(this, ...args)
      console.log(`ending ${name}`)
      return ret
    }
  }
}

class C {
  @logged
  set x(arg) {}
}

new C().x = 1
// starting x with arguments 1
// ending x

属性装饰器(新语法)

属性装饰器的类型描述

js
type ClassFieldDecorator = (
  value: undefined,
  context: {
    kind: 'field',
    name: string | symbol,
    access: { get(): unknown, set(value: unknown): void },
    static: boolean,
    private: boolean
  }
) => (initialValue: unknown) => unknown | void

第一个参数是 undefined,可以让装饰器返回一个初始化函数,当该属性被赋值时,这个初始化函数会自动运行,它会收到属性的初始值,然后返回一个新的初始值 属性装饰器也可以不返回任何值。只要返回的不是函数,而是其他类型的值,就会报错

js
function logged(value, { kind, name }) {
  if (kind === 'field') {
    return function (initialValue) {
      console.log(`initializing ${name} with value ${initialValue}`)
      return initialValue
    }
  }

  // ...
}

class C {
  @logged x = 1
}

new C()
// initializing x with value 1

实际执行

js
let initializeX = logged(undefined, {
  kind: "field",
  name: "x",
  static: false,
  private: false,
}) ?? (initialValue) => initialValue;

class C {
  x = initializeX.call(this, 1);
}

accessor 命令(新语法)

类装饰器引入了一个新命令 accessor,用来属性的前缀。简写形式,相当于声明属性 x 是私有属性#x 的存取接口

js
class C {
  accessor x = 1
}

等效写法

js
class C {
  #x = 1

  get x() {
    return this.#x
  }

  set x(val) {
    this.#x = val
  }
}

accessor 命令前面,还可以加上 static 命令和 private 命令

js
class C {
  static accessor x = 1
  accessor #y = 2
}

accessor 命令前面还可以接受属性装饰器

js
function logged(value, { kind, name }) {
  if (kind === 'accessor') {
    let { get, set } = value

    return {
      get() {
        console.log(`getting ${name}`)

        return get.call(this)
      },

      set(val) {
        console.log(`setting ${name} to ${val}`)

        return set.call(this, val)
      },

      init(initialValue) {
        console.log(`initializing ${name} with value ${initialValue}`)
        return initialValue
      }
    }
  }

  // ...
}

class C {
  @logged accessor x = 1
}

let c = new C()
// initializing x with value 1
c.x
// getting x
c.x = 123
// setting x to 123

addInitializer() 方法(新语法)

addInitializer()方法,用来完成初始化操作。

运行时间

类装饰器:在类被完全定义之后。

方法装饰器:在类构造期间运行,在属性初始化之前。

静态方法装饰器:在类定义期间运行,早于静态属性定义,但晚于类方法的定义。

例子:自定义 html 元素

js
function customElement(name) {
  return (value, { addInitializer }) => {
    addInitializer(function () {
      customElements.define(name, this)
    })
  }
}

@customElement('my-element')
class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ['some', 'attrs']
  }
}

customElements.define[1]:创建自定义元素

等效写法

js
class MyElement {
  static get observedAttributes() {
    return ['some', 'attrs']
  }
}

let initializersForMyElement = []

MyElement =
  customElement('my-element')(MyElement, {
    kind: 'class',
    name: 'MyElement',
    addInitializer(fn) {
      initializersForMyElement.push(fn)
    }
  }) ?? MyElement

for (let initializer of initializersForMyElement) {
  initializer.call(MyElement)
}

方法修饰器例子,实现类方法 this 绑定

js
function bound(value, { name, addInitializer }) {
  addInitializer(function () {
    this[name] = this[name].bind(this)
  })
}

class C {
  message = 'hello!'

  @bound
  m() {
    console.log(this.message)
  }
}

let { m } = new C()

m() // hello!

等效写法

js
class C {
  constructor() {
    for (let initializer of initializersForM) {
      initializer.call(this)
    }

    this.message = 'hello!'
  }

  m() {}
}

let initializersForM = []

C.prototype.m =
  bound(C.prototype.m, {
    kind: 'method',
    name: 'm',
    static: false,
    private: false,
    addInitializer(fn) {
      initializersForM.push(fn)
    }
  }) ?? C.prototype.m

core-decorators.js

core-decorators.js[2] 提供了常见装饰器

@autobind

autobind:方法中的 this 对象,绑定原始对象

js
import { autobind } from 'core-decorators'

class Person {
  @autobind
  getPerson() {
    return this
  }
}

let person = new Person()
let getPerson = person.getPerson

getPerson() === person
// true

@readonly

readonly 装饰器使得属性或方法不可写

@override

override 装饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错

js
import { override } from 'core-decorators'

class Parent {
  speak(first, second) {}
}

class Child extends Parent {
  @override
  speak() {}
  // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}

// or

class Child extends Parent {
  @override
  speaks() {}
  // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
  //
  //   Did you mean "speak"?
}

@deprecate (别名@deprecated)

deprecate 或 deprecated 装饰器在控制台显示一条警告,表示该方法将废除

js
import { deprecate } from 'core-decorators'

class Person {
  @deprecate
  facepalm() {}

  @deprecate('We stopped facepalming')
  facepalmHard() {}

  @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  facepalmHarder() {}
}

let person = new Person()

person.facepalm()
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard()
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder()
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
//     See http://knowyourmeme.com/memes/facepalm for more details.
//

@suppressWarnings

suppressWarnings 装饰器抑制 deprecated 装饰器导致的 console.warn()调用

js
import { suppressWarnings } from 'core-decorators'

class Person {
  @deprecated
  facepalm() {}

  @suppressWarnings
  facepalmWithoutWarning() {
    this.facepalm()
  }
}

let person = new Person()

person.facepalmWithoutWarning()
// no warning is logged

实现自动发布事件

使用装饰器,使得对象的方法被调用时,自动发出一个事件

例子:publish的装饰器,它通过改写descriptor.value,使得原方法被调用时,会自动发出一个事件。

时间发布和订阅的库 [3] postal

js
const postal = require('postal/lib/postal.lodash')

export default function publish(topic, channel) {
  const channelName = channel || '/'
  const msgChannel = postal.channel(channelName)
  msgChannel.subscribe(topic, (v) => {
    console.log('频道: ', channelName)
    console.log('事件: ', topic)
    console.log('数据: ', v)
  })

  return function (target, name, descriptor) {
    const fn = descriptor.value

    descriptor.value = function () {
      let value = fn.apply(this, arguments)
      msgChannel.publish(topic, value)
    }
  }
}
js
// index.js
import publish from './publish'

class FooComponent {
  @publish('foo.some.message', 'component')
  someMethod() {
    return { my: 'data' }
  }
  @publish('foo.some.other')
  anotherMethod() {
    // ...
  }
}

let foo = new FooComponent()

foo.someMethod()
foo.anotherMethod()

调用someMethod或者anotherMethod,就会自动发出一个事件

bash
$ bash-node index.js
频道:  component
事件:  foo.some.message
数据:  { my: 'data' }

频道:  /
事件:  foo.some.other
数据:  undefined

Mixin

Mixin 模式,对象继承的一种替代方案

在装饰器的基础上,可以实现 Mixin 模式

例子:将方法混入类中

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

class MyClass {}

Object.assign(MyClass.prototype, Foo)

let obj = new MyClass()
obj.foo() // 'foo'

通用脚本 mixins.js,将 Mixin 写成一个装饰器

js
export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list)
  }
}
js
import { mixins } from './mixins.js'

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

@mixins(Foo)
class MyClass {}

let obj = new MyClass()
obj.foo() // "foo"

上面方法会改写 prototype 对象,可以通过类继承实现

js
class MyClass extends MyBaseClass {
  /* ... */
}
js
let MyMixin = (superclass) =>
  class extends superclass {
    foo() {
      console.log('foo from MyMixin')
    }
  }

目标类再去继承这个混入类

js
class MyClass extends MyMixin(MyBaseClass) {
  /* ... */
}

let c = new MyClass()
c.foo() // "foo from MyMixin"

“混入”多个方法,就生成多个混入类

js
class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
  /* ... */
}

好处:可以调用 super,因此可以避免在“混入”过程中覆盖父类的同名方法。

js
let Mixin1 = (superclass) =>
  class extends superclass {
    foo() {
      console.log('foo from Mixin1')
      if (super.foo) super.foo()
    }
  }

let Mixin2 = (superclass) =>
  class extends superclass {
    foo() {
      console.log('foo from Mixin2')
      if (super.foo) super.foo()
    }
  }

class S {
  foo() {
    console.log('foo from S')
  }
}

class C extends Mixin1(Mixin2(S)) {
  foo() {
    console.log('foo from C')
    super.foo()
  }
}

Trait

Trait 也是一种装饰器,效果与 Mixin 类似。但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等

采用 traits-decorator[4]这个第三方模块作为例子

js
import { traits } from 'traits-decorator'

class TFoo {
  foo() {
    console.log('foo')
  }
}

const TBar = {
  bar() {
    console.log('bar')
  }
}

@traits(TFoo, TBar)
class MyClass {}

let obj = new MyClass()
obj.foo() // foo
obj.bar() // bar

Trait 不允许“混入”同名方法

js
import { traits } from 'traits-decorator'

class TFoo {
  foo() {
    console.log('foo')
  }
}

const TBar = {
  bar() {
    console.log('bar')
  },
  foo() {
    console.log('foo')
  }
}

@traits(TFoo, TBar)
class MyClass {}
// 报错
// throw new Error('Method named: ' + methodName + ' is defined twice.');
//        ^
// Error: Method named: foo is defined twice.

TFoo 和 TBar 都有 foo 方法,结果 traits 装饰器报错

解决方法 1:使用绑定运算符(::)在 TBar 上排除 foo 方法

js
import { traits, excludes } from 'traits-decorator'

class TFoo {
  foo() {
    console.log('foo')
  }
}

const TBar = {
  bar() {
    console.log('bar')
  },
  foo() {
    console.log('foo')
  }
}

@traits(TFoo, TBar::excludes('foo'))
class MyClass {}

let obj = new MyClass()
obj.foo() // foo
obj.bar() // bar

解决方法 2:为 TBar 的 foo 方法起一个别名

js
import { traits, alias } from 'traits-decorator'

class TFoo {
  foo() {
    console.log('foo')
  }
}

const TBar = {
  bar() {
    console.log('bar')
  },
  foo() {
    console.log('foo')
  }
}

@traits(TFoo, TBar::alias({ foo: 'aliasFoo' }))
class MyClass {}

let obj = new MyClass()
obj.foo() // foo
obj.aliasFoo() // foo
obj.bar() // bar

alias 和 excludes 方法,可以结合起来使用

js
@traits(TExample::excludes('foo', 'bar')::alias({ baz: 'exampleBaz' }))
class MyClass {}

上述代码:排除了TExamplefoo方法和bar方法,为baz方法起了别名exampleBaz

as 方法则为上面的代码提供了另一种写法

js
@traits(TExample::as({ excludes: ['foo', 'bar'], alias: { baz: 'exampleBaz' } }))
class MyClass {}

相关链接

[-] 装饰器

[1] CustomElementRegistry.define()

[2] core-decorators

[3] Postal.js

[4] traits-decorator