什麼是 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 提供了很好的方法來包裝類裏的代碼。這樣你能夠很是容易的把通用的工具類代碼做用的類或者類成員上。

相關文章
相關標籤/搜索