JavaScript 的裝飾器:它們是什麼及如何使用

yuyurenjie發表於2019-03-04

裝飾器的流行應該感謝在Angular 2+中使用,在Angular中,裝飾器因TypeScript能使用。但是在JavaScript中,還處於提議階段。本文將介紹裝飾器是什麼,及裝飾器如何讓程式碼更加簡潔和容易理解。

什麼是裝飾器

裝飾器是用一個程式碼包裝另一個程式碼的簡單方式。

這個概念與之前所聽過的函式複合和高階元件相似。

這已經用於很多情況,就是簡單的將一個函式包裝成另一個函式:

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做同樣事情,但是他們不同在於在包裝函式之前和之後輸出一些語句。

doSomething(`Graham`);
// Hello, Graham
wrapped(`Graham`);
// Starting
// Hello, Graham
// Finished複製程式碼

如何使用JavaScript裝飾器

JavaScript中裝飾器使用特殊的語法,使用@作為識別符號,且放置在被裝飾程式碼之前。

注意:現在裝飾器還處於提議階段,意味著還有可以變化之處

可以放置許多裝飾器在同樣程式碼之前,然後直譯器會按照順序相應執行

@log()
@immutable()
class Example {
  @time(`demo`)
  doSomething() {

  }
}複製程式碼

上例中定義了一個類,採用了三個裝飾器:兩個用於類本身,一個用於類的屬性:

  • @log能記錄所有所有訪問類
  • @immutable讓類不可變-也許新例項呼叫了Object.freeze
  • @time會記錄一個方法從執行到輸出一個獨特標籤

現在,雖然現在瀏覽器或Node還沒支援。但是如果使用Babel,能使用 transform-decorators-legacy外掛使用裝飾器。

外掛中使用legacy是因為Babel 5支援處理裝飾器,但是它也許會跟最終的標準有區別,所以才使用legacy這個詞。

為什麼使用裝飾器

函式複合在JavaScript已經成為可能,但是它相當困難或不可能用於另一個程式碼(如類或類屬性)。

裝飾器提議可以用於類或屬性,未來JavaScript版本可能會增加用於其他地方。

裝飾器也考慮到採用較為簡潔的語法。

不同型別的裝飾器

現在,裝飾器只支援類和類屬性,這包含屬性、方法、get函式和set函式

裝飾器只會在程式第一次執行時執行一次,裝飾的程式碼會被返回的值代替

類屬性裝飾器

屬性裝飾器適用於類的單獨成員-無論是屬性、方法、get函式或set函式。
裝飾器函式呼叫三個引數:

  • 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

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 {}複製程式碼

參考資料

JavaScript Decorators: What They Are and When to Use Them
阮老師ES6入門-修飾器


歡迎訂閱掘金專欄知乎專欄

相關文章