什么是 decorator,何时用 decorator

在学习 ES2015+的时候,转码已经很是的广泛。不少人都已经在实践中使用过了新的语言特性,或者至少是在教程里学习过。这些新特性之中常常让人挠头的莫属 decorator(装饰器,后文也不会翻译)了。javascript

因为在 Angular2+的普遍使用,decorator 变得流行。在 Angular 里,因为有 TypeScript 因此用上了 decorator。可是在 javascript 里,decorator 还在 stage-2,也就是说会和 js 的更新一块儿发布。咱们来看一下 decorator 是什么,如何使用它来让你的代码更加的简洁易懂。java

什么是 decorator

它最简单的形式是一段代码的包装,也就是说装饰这段代码。这个概念也叫作高阶方法。这个模式已经使用的很是的多了,好比:node

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

这个例子生成了一个新的方法,在wrapped变量中,能够和doSomething同样被调用,而且行为也彻底一致。惟一不一样的地方是它会在被调用后输出一些日志。如:react

doSomething('Graham');

// Hello, Graham

wrapped('Graham);

// Starting...
// Hello, Graham
// Finished

如何使用 decorator

Decorator 的语法稍微有点特殊,它以@开始,放在须要装饰的代码上方。git

在写做的时候decorator已经进入了 Stage 2,也就是说基本上不会变了,可是仍是可能会发生改变的。

理论上你能够在同一代码上使用任意多的 decorator,他们会以你声明的顺序执行。好比:github

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

上例中定义了另外一个Example类,而且在里面使用了三个 decorator。两个做用在类上,一个做用在属性上:npm

  • @log能够访问类
  • @immutable可让类只读 -- 也许它在新的实例上调用了Object.freeze
  • @time会记录一个方法执行使用了多长时间,并输出日志

目前,使用 decorator 须要转码工具的支持。由于尚未浏览器和 node 的版本支持 decorator。若是你使用的是 Babel,只要使用transform-decorators-legacy plugin就能够。注意里面的
legacy这个词,这个是 babel 为了支持 es5 的方式实现 decorator,也许最后会和语言标准有些不一样。redux

何时使用 decorator

高阶方法在 javascript 中已经使用的很是多了,可是它仍是很难做用于其余的代码段(好比类和类的属性)上,至少写起来很是的别扭。数组

Decorator 支持类和属性,很好的解决了上面的问题。之后的 javascript 标准里也许会赋予 decorator 更多的做用,处理不少以前无法优雅的处理的代码。浏览器

Decorator 的不一样类型

目前的 decorator 还只支持类和类的成员,包括:属性、方法、getter 和 setter。

Decorator 的实质是一个放回方法的方法,而且会在里面以某种方式处理被装饰的代码段。这些 decorator 代码会在程序开始的时候运行一次,而且被装饰的代码会被 decorator 返回的值替换。

类成员 decorator

属性 decorator 做用在一个类成员上,不管是属性、方法、或者 getter 和 setter。这个 decorator 方法在调用的时候会传入三个参数:

  • target: 成员所在的类
  • name: 类成员的名字
  • descriptor: 类成员的 descriptor。这个是在Object.defineProperty里使用的对象。

这里使用经典的例子@readonly。它是这么实现的:

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

这里在属性的 descriptor 里更新了writable的值为 false。

这个 decorator 是这么使用在类成员上的:

class Example {
  a() {}

  @readonly
  b() {}
}

const e = new Example();
e.a = 1;
e.b = 2; // TypeError: Cannot assign to readonly 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;
}

原来的方法基本就彻底被取代了,咱们在里面加上了日志打印输入、输出。

注意咱们用了...操做符来自动把输入的参数转化为一个数组,这样比以前处理arguments的写法简单了不少。

运行以后会获得:

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

const e = new Example();

e.sum(1, 2);

// Arguments: 1,2
// Result: 3

你看到咱们须要用一个有趣的语法来执行 decorator 方法。这一点能够单独写一篇文章来叙述了。简单来讲apply方法可让你指定所执行的方法的this和参数。

咱们也可让 decorator 接收参数。好比,咱们能够这样重写log decorator。

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);
        } catch (e) {}
      };
    }

    return descriptor;
  };
}

这就更加的复杂了,可是分解开来看你会发现:

  • 一个方法只接收一个参数的方法。 log只接收一个name参数。
  • 这个方法返回了一个方法,这个方法才是 decorator

这个以前所些的log decorator 基本是同样的,只是它使用了外部方法传入的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

这样使用的结果是咱们能够用某些 tag 来区分开不一样的 log。

这样的写法能够运行是由于log('some tag')方法会被 javascript 运行时当即执行,而后把log方法的返回结果做为sum方法的 decorator。

类 decorator

类 decorator 会修饰整个类。Decorator 方法接收构造器方法做为惟一的参数。

注意类 decorator 做用域构造器方法,不是这个类的实例。也就是说若是你想修改类的实例等话你要本身写构造方法的包装函数。

通常来讲,类 decorator 不如类成员 decorator 有用。由于你在这里能够实现的,均可以经过一样的方法调用一个方法来实现。总之,不管作什么你都须要在最后返回新的构造方法来代替就的构造方法。

咱们来改造一下前面的log方法,让它来处理构造方法。

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

这里咱们接受一个类做为参数,而后返回一个新的方法。这个方法会被做为构造方法使用。它只是简单的把参数打印到 console 里,返回一个类的实例。

好比:

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

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

咱们能够看到构造 Example 类的时候就会有参数的日志输出出来。Decorator 以后的类输出的实例也仍是 Example 的实例。这正是咱们要的效果。

给类 decorator 传入参数的办法和前面的类成员的 decorator 的方法是同样的:

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", 12);
// Arguments for Demo: args
console.log(e);
// Example {}

真实的例子

Core Decorators

有一个很不错的库叫作Core Decorators。这个库提供了不少有用的 decorator,而且已经在使用中了。这个库里经常使用到的功能有 timing、警告、只读等工具方法。

React

React 库使用了不少高阶组件。高阶组件其实就是 React 的组件,只不过写成了一个方法,而且包装了另外的一个组件。

尤为是在和 react-redux 库一块儿使用的时候,要写不少次的connect方法:

class MyComponent extends React.Component {}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

然而,这些均可以用 decorator 来代替:

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

MobX

MobX 库普遍的使用了 decorator,这样你很容易的把字段标记为 Observable 或者 Computed,把类标记为 Observer。

总结

类成员 decorator 提供了很好的方法来包装类里的代码。这样你能够很是容易的把通用的工具类代码做用的类或者类成员上。

相关文章
相关标签/搜索