修饰器

作者: 了凡和纤风 | 来源:发表于2019-10-21 20:42 被阅读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()
    

    相关文章

      网友评论

          本文标题:修饰器

          本文链接:https://www.haomeiwen.com/subject/jxlxvctx.html