JS 裝飾器解析

牧云云發表於2018-03-19

JS 裝飾器解析

隨著 ES6 和 TypeScript 中類的引入,在某些場景需要在不改變原有類和類屬性的基礎上擴充套件些功能,這也是裝飾器出現的原因。

裝飾器簡介

作為一種可以動態增刪功能模組的模式(比如 redux 的中介軟體機制),裝飾器同樣具有很強的動態靈活性,只需在類或類屬性之前加上 @方法名 就完成了相應的類或類方法功能的變化。

不過裝飾器模式仍處於第 2 階段提案中,使用它之前需要使用 babel 模組 transform-decorators-legacy 編譯成 ES5 或 ES6。

在 TypeScript 的 lib.es5.d.ts 中,定義了 4 種不同裝飾器的介面,其中裝飾類以及裝飾類方法的介面定義如下所示:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
複製程式碼

下面對這兩種情況進行解析。

作用於類的裝飾器

當裝飾的物件是類時,我們操作的就是這個類本身

@log
class MyClass { }

function log(target) { // 這個 target 在這裡就是 MyClass 這個類
   target.prototype.logger = () => `${target.name} 被呼叫`
}

const test = new MyClass()
test.logger() // MyClass 被呼叫
複製程式碼

由於裝飾器是表示式,我們也可以在裝飾器後面再新增提個引數:

@log('hi')
class MyClass { }

function log(text) {
  return function(target) {
    target.prototype.logger = () => `${text}${target.name} 被呼叫`
  }
}

const test = new MyClass()
test.logger() // hello,MyClass 被呼叫
複製程式碼

在使用 redux 中,我們最常使用 react-redux 的寫法如下:

@connect(mapStateToProps, mapDispatchToProps)
export default class MyComponent extends React.Component {}
複製程式碼

經過上述分析,我們知道了上述寫法等價於下面這種寫法:

class MyComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
複製程式碼

作用於類方法的裝飾器

與裝飾類不同,對類方法的裝飾本質是操作其描述符。可以把此時的裝飾器理解成是 Object.defineProperty(obj, prop, descriptor) 的語法糖,看如下程式碼:

class C {
  @readonly(false)
  method() { console.log('cat') }
}

function readonly(value) {
  return function (target, key, descriptor) { // 此處 target 為 C.prototype; key 為 method;
    // 原 descriptor 為:{ value: f, enumarable: false, writable: true, configurable: true }
    descriptor.writable = value
    return descriptor
  }
}

const c = new C()
c.method = () => console.log('dog')

c.method() // cat
複製程式碼

可以看到裝飾器函式接收的三個引數與 Object.defineProperty 是完全一樣的,具體實現可以看 babel 轉化後的程式碼,主要實現如下所示:

var C = (function() {
  class C {
    method() { console.log('cat') }
  }

  var temp
  temp = readonly(false)(C.prototype, 'method',
    temp = Object.getOwnPropertyDescriptor(C.prototype, 'method')) || temp // 通過 Object.getOwnPropertyDescriptor 獲取到描述符傳入到裝飾器函式中

  if (temp) Object.defineProperty(C.prototype, 'method', temp)
  return C
})()
複製程式碼

再將再來看看如果有多個裝飾器作用於同一個方法上呢?

class C {
  @readonly(false)
  @log
  method() { }
}
複製程式碼

經 babel 轉化後的程式碼如下:

desc = [readonly(false), log]
    .slice()
    .reverse()
    .reduce(function(desc, decorator) {
      return decorator(target, property, desc) || desc;
    }, desc);
複製程式碼

可以清晰地看出,經過 reverse 倒序後,裝飾器方法會至裡向外執行。

相關連結

javascript-decorators

Javascript 中的裝飾器

JS 裝飾器(Decorator)場景實戰

修飾器

Babel

相關文章