修饰器

2019-10-21  本文已影响0人  了凡和纤风

一、Babel 环境配置

  1. 安装依赖
$ npm i babel-core babel-plugin-transform-decorators-legacy babel-cli --save-dev
  1. 配置 .babelrc 文件
{
  "plugins": ["transform-decorators-legacy"]
}
  1. 运行文件
$ npx babel-node .\01_test.js

Babel 的官方网站提供一个在线转码器勾选对应的选项即可

二、类的修饰器

修饰器(Decorator)是一个函数,用来修改类的行为。ES2017引入了这项功能,目前Babel转码器已经支持

@testable
class MyTestableClass {
  // ...
}

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

MyTestableClass.isTestable // true

上面的代码中,@testable 会是一个修饰器。修改了 MyTestableClass 这个类的行为,为它加上了静态属性 isTestable

修饰器的行为基本如下

@decorator
class A {}

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

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

修饰器函数的第一个参数就是所要修饰的目标类

function testable(target) {}

testable 函数的参数 target 就是会被修饰的类。
如果觉得一个参数不过用,可以在修饰器外面再封装一层函数

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

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

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

修饰器 testable 可以接受参数,这就等于可以修改修饰器的行为。

前面的例子是为类添加一个静态属性没如果想添加实例属性,可以通过目标类的 prototype 对象进行操作

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

@testable
class MyTestableClass {}

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

修饰器函数 testable 是在目标类的 prototype 对象上添加属性的,因此就可以在实例上调用

下面是另外一个例子

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


// main.js
import { mixins } from './mixins'

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

@mixins(Foo)
class MyClass {}

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

上面通过修饰器 mixins 把 Foo 类的方法添加到了 MyClass 的实例上面。

可以使用 Object.assign() 模拟这个功能

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

class MyClass {}

Object.assign(MyClass.prototype, Foo)

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

三、方法的修饰

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

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

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

修饰器函数一个可以接受3个 参数:

function readonly(target, name, descriptor) {
  /* desctiptor 对象原来的值如下
    {
      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 修饰器可以起到输出日志的作用。

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(null, arguments)
  }

  return descriptor
}

const math = new Math()
math.add(2, 4)

@log 修饰器的作用就是在执行原始的操作之前执行一次 console.log,从而达到输出日志的目的

修饰器有注释的作用

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

从上面的代码中,一眼就能看出,Person 类是可测试的,而 name 方法是只读且不可枚举的

如果同一个方法有多个修饰器,那么该方法会先从外到内进入修饰器,然后由内到外执行。

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

外层修饰器 @dec(1) 先进入,但是内层修饰器 @dec(2) 先执行

除了注释,修饰器还能用来进行类型检查,所有,对于类来说,这项功能相当有用,他将是 JavaScript 代码静态分析的重要工具

四、为什么修饰器不能用于函数

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

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

@add
function foo() {
}

上面代码的本意是使执行后的 counter 等于1,但实际上结果是 counter 等于0.因为 函数提升,使得实际执行的代码如下。

@add
function foo() {
}

var counter
var add

counter = 0
add = function() {
 counter++
}

总之,由于存在函数提升,修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。

另一方面,如果一定要修饰函数,可以采用高阶函数的形式直接执行

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)

五、core-decorators.js

core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器

@autobind
autobind 修饰器使得方法中的 this 对象绑定原始对象

import { autobind } from 'core-decorators';

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

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

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

import { readonly } from 'core-decorators';

class Meal {
  @readonly
  entree = 'steak';
}

var dinner = new Meal();
dinner.entree = 'salmon';
// Cannot assign to read only property 'entree' of [object Object]

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

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) {}
}

// 或者
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 修饰器在控制台显示一条警告,表示该方法将废除。

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 修饰器抑制 decorated 修饰器导致的 console.warn() 调用,但异步代码发出的调用除外

import { suppressWarnings, deprecated } from 'core-decorators'

class Person {
  @deprecated 
  facepalm() {}

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

let person = new Person()

person.facepalmWithoutWarning()
// no warning is logged

六、Mixin

在修饰器的基础上可以实现 Mixin 模式。所谓 Mixin 模式,就是对象继承的一种替代方案,在一个对象中混入另一个对象的方法。

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

class MyClass {}

Object.assign(MyClass.prototype, Foo)

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

通过 Objecy.assign 方法可以将 foo 方法 “混入” MyClass 类,导致 MyClass 的实例对象 obj 都具有 foo 方法。这就是 “混入” 模式的一个简单实现。

下面,部署一个通用脚本 mixins.js,将 Mixin 写成一个修饰器

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

使用上面这个修饰器为类“混入” 各种方法。

import { mixins } from './mixins'

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

@mixins(Foo)
class MyClass {}

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

上面的方法会改写 MyClass 类的 prototype 对象,如果不喜欢这一点,也可以通过下面的方法


通过 类的继承实现 Mixin

class MyClass extends MyBaseClass {
  /* ... */
}

上面的代码中,MyClass 继承了 MyBaseClass。

如果我们想在 MyClass 里面“混入” 一个 foo 方法,其中一个办法就是在 MyClass 和 MyBaseClass 之间插入一个混入类,这个类具有 foo 方法,并且继承了 MyBaseClass 的所有方法,然后 MyClass 再继承这个类。

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

上面的代码中,MyMixin 是一个混入类生成器,接受 superclass 作为参数,然后返回 一个继承 superclass 的子类,该子类包含一个 foo 方法

接着,目标类再去继承这个混入类就达到了 “混入” foo 方法的目的

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

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

如果需要“混入” 多个方法,就生成多个混入类

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

这种写法的一个好处是 可以调用 super,避免在“混入” 过程中覆盖父类的同名方法。

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

上面的代码中,每一次混入发生时都调用父类的 super.foo 方法,导致父类的同名方法没有被覆盖,行为被保留了下来。

new C().foo()
// foo from c
// foo from Mixin1
// foo from Mixin2
// foo from S

七、Trait

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

下面以 traits-decorator 这个第三方模块为例进行说明。这个模块提供的 traits 修饰器不仅可以接受对象,还可以接受 ES6 类作为参数。

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

通过 traits 修饰器在 MyClass 类上 “混入” 了 TFoo 类的 foo 方法和 TBar 类的 bar 方法

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

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 {}
// Error: Method named: foo is defined twice.

一种解决方法是排除 TBar 的 foo 方法。

import { traits, excludes } from 'traits-decorator'
// ...

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

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

上面的代码使用绑定运算符 (::)在 TBar 上排除了 foo 方法,混入就不会报错了。

另一种方法是为 TBar 的 foo 方法起一个别名

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

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

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

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

排除了 TExample 的 foo 方法 和 bar 方法,为 baz 方法起了别名 exampleBaz

as 方法则为上面的飞马提供了另一种写法

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

猜你喜欢

热点阅读