ES7-装饰器Decorator详解

Decorator的使用分析

Featured image

Javascript中的装饰器

js中的装饰器是在ES7里的,它依旧依赖ES5里的 Object.defineProperty 方法。

先看下装饰器在代码中的样子:

@frozen 
class Foo {
  @configurable(false)
  method() {}
}

@frozen 和 @configurable 就是我们说的装饰器。
可以看出是通过@来使用装饰器。一共用了两种:一个用在类上,一个用在方法上。

讲讲Object.defineProperty

看下ES6里类的写法:

class Cat {
    meow() {
        return `${this.name} says Meow!`;
    }
}

这个写法只是一个语法糖,这段代码实际上在执行时是这样的:

function Cat() {}
Object.defineProperty(Cat.prototype, "meow", {
    value: function() { return `${this.name} says Meow!`; },
    enumerable: false,
    configurable: true,
    writable: true
});

Object.defineProperty(target, name, descriptor) 可以直接在一个对象上定义一个新属性,或者修改一个现有的属性,并返回这个对象。 defineProperty接受三个参数:

属性描述符

定义的对象里,每一种属性都对应一个属性描述符对象,用来描述该属性的特性。
目前有两种属性描述符:

数据描述符独有:

存取描述符独有的:

二者同时拥有的:

一个没有get/set/value/writable定义的属性被称为”通用的”,并被认为是一个数据描述符

@Decorator在class中使用

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

新增属性

@name
class Cat {
  // ...
}

function name(target) {
  target.name = "MiaoMiao";
}

// 显示"MiaoMiao"
console.log(Cat.name)

上面代码中,@testable 就是一个装饰器。它修改了 MyTestableClass 这个类的行为,为它加上了静态属性 isTestabletestable 函数的参数 targetMyTestableClass 类本身。

修改原有属性的描述符

@seal
class Person {
  sayHi() {}
}

function seal(constructor) {
  let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHi')
  Object.defineProperty(constructor.prototype, 'sayHi', {
    ...descriptor,
    writable: false
  })
}

Person.prototype.sayHi = 1 // 无效

使用闭包来实现mixin

class A { say() { return 1 } }
class B { hi() { return 2 } }

@mixin(A, B)
class C { }

function mixin(...args) {
    return function(constructor) {
        for (let arg of args) {
            for (let key of Object.getOwnPropertyNames(arg.prototype)) {
                // 跳过构造函数
                if (key === 'constructor') continue 
                Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key))
            }
        }
    }
}

let c = new C()
console.log(c.say(), c.hi()) // 1, 2

多个装饰器同时使用

@decorator1
@decorator2
class { }

执行的顺序为decorator2 -> decorator1,离class定义最近的先执行。

@Decorator在class属性中使用

装饰器不仅可以装饰类,还可以装饰类的属性。 比如我们把 meow() 设置成只读,如果使用装饰器的话可以这样写:

function readonly(target, name, descriptor) {
    discriptor.writable = false;
    return discriptor;
}

class Cat {
    @readonly
    meow() {
        return `${this.name} says Meow!`;
    }
}

我们看到,定义装饰器的时候,参数有三个: targetnamedescriptor,是不是和 Object.defineProperty 的入参很像。 其实,装饰器在作用于方法的时候,实际上是通过 Object.defineProperty 来进行扩展和封装的。 所以上面的这段代码中,装饰器实际是这样的:

let descriptor = {
    value: function() { return `${this.name} says Meow!`; },
    enumerable: false,
    configurable: true,
    writable: true
};

descriptor = readonly(Cat.prototype, "meow", descriptor) || descriptor;

Object.defineProperty(Cat.prototype, "meow", descriptor);

这样就看得比较清楚了。

注意点:

  • 如果装饰器作用在类本身,我们操作的对象也是类本身。
  • 如果装饰器作用在类的某个具体方法时,我们操作的对象是它的描述符(descriptor)。

使用示例

下面看一些常用的使用案例

输出日志

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);

参考资料:
Javascript装饰器的妙用
proposal-decorators
JS 装饰器,一篇就够