ES7 Decorator 入門解析

Decorator 提供了一種獨特的抽象邏輯,可在原有代碼基礎上,零侵入添加新功能特性。商業代碼老是多種交織並存的,在平常開發中,除了實現業務功能以外,咱們還須要考慮諸如:異常處理、性能分析、日誌等額外的需求。未經設計的的開發方法會傾向於將各類需求耦合組成一個功能模塊,好比:javascript

class Math{
  static add(num1,num2){
    try{
      console.time('some label');
      log('log for something');
      const result= num1+num2;
      console.timeEnd('some label');
      return result;
    }catch(e){
      error('something had broken');
    }
  }
}

上述簡單的兩數相加功能,在添加各種需求以後,已經變的面目全非。Decorator 語法經過描述,可將功能特性疊加到原有功能中:java

class Math{
  @log
  @error
  @time
  static add(num1,num2){
    return num1+num2;
  }
}

Decorator 是什麼

Decorator 就是一個的包裹函數,運行時在編譯階段調用該函數,修改目標對象的行爲、屬性。咱們先來看一個簡單實例:python

const log = (target,prop)=>console.log(`Wrap function: '${prop}'`);

const tec={
  @log
  say(){
    console.log('hello world')
  }
}

// => Wrap function 'say'

Decorator 函數簽名以下:git

// @param   target  做用對象
// @param   prop    做用的屬性名
// @param   descriptor  屬性描述符
// @return  descriptor  屬性描述符
function decorator(target,prop,descriptor){}

參數詳解:github

  1. target : 做用的對象,有以下狀況:babel

    • 做用於 class 時,target 爲該 class 函數app

    • 做用於 class 中的函數、屬性 時,target 爲該 class 的 prototype 對象函數

    • 做用於 對象字面量中的函數、屬性 時,target 爲該對象性能

  2. prop : 描述的屬性名,若decorator做用於class時,該參數爲空this

  3. descriptor : 屬性本來的描述符,該描述符可經過 Object.getOwnPropertyDescriptor() 獲取,若decorator做用於class時,該參數爲空

decorator 函數支持返回描述符undefined,當返回值爲描述符時,運行時會調用Object.defineProperty()修改原有屬性。

Decorator 的ES5實現

理解 Decorator 機制,最佳方式是使用ES5實現該過程。
class裝飾器機制比較簡單,僅作一層包裝,僞代碼:

// 調用實例
@log 
class Person{}
// 實現代碼
const Person = log(Person);

屬性裝飾器機制則比較複雜,babel 就此提供了一個參考範例:

// decorator 處理
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
  var desc = {};
  Object['ke' + 'ys'](descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;

  if ('value' in desc || desc.initializer) {
    desc.writable = true;
  }

  desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    return decorator(target, property, desc) || desc;
  }, desc);

  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }

  if (desc.initializer === void 0) {
    Object['define' + 'Property'](target, property, desc);
    desc = null;
  }

  return desc;
}

// 調用實例
class Person{
  @log
  say(){}
}

// 實現代碼
_applyDecoratedDescriptor(
  Person.prototype, 
  'say', 
  [log],
  Object.getOwnPropertyDescriptor(Person.prototype, 'say'),
  Person.prototype)
)

用例

Decorator 主要應用於以下幾類對象:

  1. class

  2. class 中,除構造函數外的方法

  3. class 中的屬性

  4. 對象字面量中的函數

  5. 對象字面量中的屬性

// 類
@log
class Person{
  // 函數
  @log
  say(){}
  
  // 屬性
  @log
  name = 'tec';
}

// 一樣適用於對象字面量的方法、屬性
const tec = {
  @log
  name:'tec',
  
  @log
  walk(){}
};

Decorator 實踐

在JS中,Decorator 是一個新概念,對於多數沒有接觸過諸如python、C#的開發者而言,很難理解實際應用場景。幸運的是github已經有人封裝了經常使用Decorator。筆者分析該庫,總結以下幾種定義模式:

  1. 經過 descriptor 的 value 值修改:

function decorate(target, key, descriptor) {
  const fn = descriptor.value;

  return {
    ...descriptor,
    value() {
      return fn.apply(this, arguments);
    }
  }
}
  1. 經過 descriptor 的 getset 函數修改:

function decorate(target, key, descriptor) {
  let value = descriptor.value;

  return {
    ...descriptor,
    get() {
      return value;
    }
    set(v) {
      value=v;
    }
  }
}
  1. 經過 descriptor 的 writableenumerable 等屬性修改:

function readonly(target, key, descriptor) {
  return {
    ...descriptor,
    writable:false
  }
}
  1. 針對 class ,返回包裹函數

function log(target){
  let initTimes=0;
  return (...arg)=>{
    console.log(++initTimes);
    target.call(this,...arg);
  };
}

在實際開發中,還須要注意如下事項:

  1. Decorator 的目標是在原有功能基礎上,添加功能,切忌覆蓋原有功能

  2. Decorator 不是管道模式,decorator之間不存在交互,因此必須注意保持decorator獨立性、透明性

  3. Decorator 更適用於非業務功能需求

  4. 肯定 decorator 的用途後,切記執行判斷參數類型

  5. decorator 針對每一個裝飾目標,僅執行一次

參考資料:

  1. babel 裝飾器插件

  2. Object.defineProperty

  3. 屬性描述符

相關文章
相關標籤/搜索