装饰器因为在Angular 2+中的使用而变得流行起来。在Angular采用的事TypeScript,所以装饰器是可用的,但在JavaScript中,它们目前是第2阶段的提议,这意味着它们应该是该语言未来更新的一部分。让我们看看什么是装饰器,以及如何使用它们使代码更清晰、更容易理解。

什么是装饰器?

在其最简单的形式中,装饰器只是一种用另一段代码包装代码的方式——字面上是“装饰”它。这是一个您以前可能听说过的概念,就像函数组合或高阶函数。

在很多应用中通过调用一个函数包装另一个:

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

这个示例中创建了一个新功能——在变量包装可以被称为完全一样 doSomething 的函数,并将做同样的事情。不同之处在于,它在包装函数调用之前和之后做一些记录:

doSomething('Graham');
// Hello, Graham

wrapped('Graham');
// Starting
// Hello, Graham
// Finished

如何使用

修饰符在JavaScript中使用一个特殊的语法: @+装饰符:

decorator目前还处于“第二阶段草案”形式,这意味着他们大部分都完成了,但仍然会有更新

可以按你的需求使用多个装饰器,这些修饰符会按你声明的顺序起作用:

@log()
@immutable()
class Example {
  @time('demo')
  doSomething() {
    //
  }
}

3个装饰器:两个用于装饰class, 一个装饰函数:

  • @log 可以记录所有访问类
  • @immutable 修饰类不可变-
  • @time 记录方法的执行时间

目前使用decorator需要进行编译,因为没有当前浏览器或Node版本支持。如果你使用Babel,需要启用transform-decorators-legacy插件。

为什么使用Decorator

用于给对象在运行期间动态的增加某个功能,职责等。相较通过继承的方式来扩充对象的功能,装饰器显得更加灵活,首先,我们可以动态给对象选定某个装饰器,而不用 hardcore 继承对象来实现某个功能点。

Decorator 分类

目前,唯一受支持的装饰器类型是类和类的成员。这包括属性、方法、getter和setter。

装饰器实际上就是返回另一个函数的函数,这些函数被调用时带有装饰器的适当细节。这些装饰器函数在程序首次运行时计算一次,并将装饰后的代码替换为返回值。

类成员修饰符

用于修饰类中的成员,包括属性、方法、getter和setter。

装饰器函数调用三个参数:

  • - target-被修饰的类
  • - name-类成员的名字
  • - descriptor-成员描述符。对象会将这个参数传给Object.defineProperty

@readonly是经典的例子:

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

上面的例子中会将成员描述符中的writable设为false

接着修饰类写法如下:

class Example {
  a() {}
  @readonly
  b() {}
}

const e = new Example();
e.a = 1;
e.b = 2;
// TypeError: Cannot assign to read only property 'b' of object '#<Example>'

但是我们可以做的更好,可以用别的形式代替装饰函数。例如,记录所有的输入和输出:

function log(target, name, descriptor) {
  const original = descriptor.value;
  if (typeof original === 'function') {
    descriptor.value = function(...args) {
      console.log(`Arguments: ${args}`);
      try {
        const result = original.apply(this, args);
        console.log(`Result: ${result}`);
        return result;
      } catch (e) {
        console.log(`Error: ${e}`);
        throw e;
      }
    }
  }
  return descriptor;
}

我们使用了扩展运算符,会自动将所有参数转为数组。

class Example {
  @log
  sum(a, b) {
    return a + b;
  }
}

const e = new Example();
e.sum(1, 2);
// Arguments: 1,2
// Result: 3

可以让装饰器获取一些参数,例如重写log装饰器如下:

function log(name) {
  return function decorator(t, n, descriptor) {
    const original = descriptor.value;
    if (typeof original === 'function') {
      descriptor.value = function(...args) {
        console.log(`Arguments for ${name}: ${args}`);
        try {
          const result = original.apply(this, args);
          console.log(`Result from ${name}: ${result}`);
          return result;
        } catch (e) {
          console.log(`Error from ${name}: ${e}`);
          throw e;
        }
      }
    }
    return descriptor;
  };
}

这与之前的log装饰器相同,唯一的不同是能接收外部函数的name参数:

class Example {
  @log('some tag')
  sum(a, b) {
    return a + b;
  }
}

const e = new Example();
e.sum(1, 2);
// Arguments for some tag: 1,2
// Result from some tag: 3

我们使用自己提供的标记来区分不同的日志。

类装饰器

类修饰符修饰整个类,装饰器函数的参数为被装饰的构造器函数。

注意只用于构造器函数,而不适用于类的每个实例。这就意味着如果想控制实例,就必须返回一个包装版本的构造器函数。

通常,类装饰器没什么用处,因为你所需要做的,同样可以用一个简单函数来处理。你所做的只需要在结束时返回一个新的构造函数来代替类的构造函数。

回到我们记录那个例子,编写一个记录构造函数参数:

function log(Class) {
  return (...args) => {
    console.log(args);
    return new Class(...args);
  };
}

这里接收一个类作为参数,返回新函数作为构造器。此函数打印出参数,返回这些参数构造的实例。 如:

@log
class Example {
  constructor(name, age) {
  }
}

const e = new Example('Graham', 34);
// [ 'Graham', 34 ]
console.log(e);
// Example {}

构造Example类时会输出提供的参数,构造值e也确实是Example的实例。

传递参数到类装饰器与类成员一样。

function log(name) {
  return function decorator(Class) {
    return (...args) => {
      console.log(`Arguments for ${name}: args`);
      return new Class(...args);
    };
  }
}

@log('Demo')
class Example {
  constructor(name, age) {}
}

const e = new Example('Graham', 34);
// Arguments for Demo: args
console.log(e);
// Example {}

真实例子

Core decorators是一个库,提供了几个常见的修饰器,通过它可以更好地理解修饰器。

想理解此库,也可以去看看阮老师的关于此库的介绍

React

React广泛运用了高阶组件,这让React组件成为一个函数,并且能包含另一个组件。
使用装饰器是不错的替代法,例如,Redux库有一个connect函数,用于连接React组件和React store。

通常是这么使用的:

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

当然可以使用装饰器代替:

@connect(mapStateToProps, mapDispatchToProps)

export default class MyReactComponent extends React.Component {}

MobX

MobX库大量的使用修饰符,允许您轻松地将字段标记为 Observable 或 Computed,并将类标记为 Observers。