JavaScript Decorators

javascript decorator

現(xiàn)在什么 AOP 編程在前端領(lǐng)域越來(lái)越被大家追捧,所以我也來(lái)探究一下如何在javascript中進(jìn)行AOP編程。 裝飾器無(wú)疑是對(duì)AOP最有力的設(shè)計(jì),在es5 時(shí)代,可以通過(guò) Object.defineProperty 來(lái)對(duì)對(duì)象屬性/方法 進(jìn)行訪問(wèn)修飾,但用起來(lái)需要寫(xiě)一堆東西。現(xiàn)在decorator已經(jīng)在ES7的提案中了,借助Babel等轉(zhuǎn)碼工具,我們現(xiàn)在也能在javascript中使用裝飾器語(yǔ)法了!

什么是Decorator

decorator 也叫裝飾器(裝潢器)。它可以在不侵入到原有代碼內(nèi)部的情況下而通過(guò)標(biāo)注的方式修改類(lèi)代碼行為,裝飾器對(duì)代碼行為的改變是在編譯階段完成的,而不是在執(zhí)行階段。雖然Decorator還處在ES7草案階段,但是我們可以通過(guò)Babel來(lái)轉(zhuǎn)換es7代碼,所以大家還是可以愉快的使用decorator。
在ES7提案中,Decorator的描述如下:

  • an expression
  • that evaluates to a function
  • that takes the target, name, and decorator descriptor as arguments
  • and optionally returns a decorator descriptor to install on the target object.

出自 https://github.com/wycats/javascript-decorators

在代碼層面,Decorator其實(shí)就是一個(gè)函數(shù)。

function readonly(target, name, desc) {
  desc.writable = false;
  return desc;
}

let o = {
  @readonly  // 標(biāo)識(shí)為只讀屬性
  name: 'liuyan'
}

// 賦值失敗并報(bào)錯(cuò)
o.name = 'liuzheng'; // Cannot assign to read only property 'name' of object '#<Object>'

上面的代碼實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的裝飾器用來(lái)使對(duì)象屬性只讀。函數(shù)readonly 規(guī)定了裝飾器描述符的行為。不難看出,這和ES5中的 Object.defineProperty 方法很類(lèi)似,使用es5代碼一樣能夠?qū)崿F(xiàn)相同的功能,其實(shí)使用Babel轉(zhuǎn)碼最終也就是轉(zhuǎn)換成了Object.defineProperty 的實(shí)現(xiàn)形式,只是使用 @readonly 這種語(yǔ)法更能直觀的描述出來(lái), 對(duì)比Java中的注解、 Python中的裝飾器其實(shí)都使用類(lèi)似的語(yǔ)法。

Decorator用法

給屬性添加Decorator
和前面的例子一樣,有時(shí)候需要在JS中實(shí)現(xiàn)類(lèi)靜態(tài)成員,這個(gè)時(shí)候就可以使用Decorator來(lái)修飾了,代碼如下:

// 示例
class Person {
  @readonly
  static MIN_AGE = 0;
}

這樣,當(dāng)不小心重新為 Person.MIN_AGE 賦值的時(shí)候,就會(huì)拋出錯(cuò)誤。

給方法添加Decorator
也可以對(duì)方法進(jìn)行裝飾。比如現(xiàn)在需要實(shí)現(xiàn)一個(gè)功能: 設(shè)計(jì)一個(gè)裝飾器,它能夠統(tǒng)計(jì)出一個(gè)異步方法(這里只用Promise)的耗時(shí)。還是以Person類(lèi)為例,給Person增加一個(gè)request方法,統(tǒng)計(jì)request執(zhí)行耗時(shí),代碼實(shí)現(xiàn)非常簡(jiǎn)單:

class Person {
  static MIN_AGE = 0;
  constructor(name) {
    this.name = name
  }
 
  @duration
  request() {
    return new Promise((resolve, reject) => {
       setTimeout(() => {
         resolve({status: 0})
       }, 3000);
    })
  }
}

// 裝飾器
function duration(target, key, desc) {
  const { value } = desc;
  let _time = Date.now();
  desc.value = function(...args) {
    let res = value.apply(this, args);
    if (res && typeof res.then === 'function') {
      res.then(() => {
        console.log(`${key}() ==> 耗時(shí):${Date.now() - _time}ms`);
      }, () => {
        console.log(`${key}() ==> 耗時(shí):${Date.now() - _time}ms`);
      })
    } else {
      console.log(`${key}() ==> 耗時(shí):${Date.now() - _time}ms`);
    }
    return res;
  }
  // 需要把描述對(duì)象返回
  return desc;
}

// 開(kāi)始
var p = new Person('liuyan');
p.request(); // 輸出:  request() ==> 耗時(shí):3002ms

作用于class
也可以在為class 應(yīng)用裝飾器,現(xiàn)在我要通過(guò)裝飾器給Person類(lèi)增加一個(gè)靜態(tài)屬性IS_PERSON; (當(dāng)然,這沒(méi)什么卵用...)

// 增加靜態(tài)屬性IS_PERSON
@isPerson
class Person {
  ...
}

function isPerson(target) {
  target.IS_PERSON = true;
}

console.log(Person.IS_PERSON); // true

也可以作用于class的實(shí)例屬性

class Person {
  ...
}

function sayHi(target) {
  const {sayHi} = target.prototype;
  target.prototype.sayHi = function(...args) {
    if (typeof sayHi === 'function') {
      var res = sayHi.apply(this, args);
    }
    console.log(`Hi, I\'m ${this.name}`);
    return res;
  };
}

var p = new Person('liuyan');
p.sayHi(); // Hi, I'm liuyan

decorator作用于類(lèi)最常見(jiàn)的用法就是mixins了,mixin 也就是允許我們?yōu)榻M件(類(lèi)) 附加額外的功能,用過(guò)react的童鞋應(yīng)該對(duì)mixin不陌生,不過(guò)使用mixin擴(kuò)展新功能這種用法已經(jīng)不被推薦了。
decorator已經(jīng)在各知名框架中開(kāi)始大面積使用,比如Angular2(ng2), 雖然ng2使用TypeScript 來(lái)構(gòu)建的,但是裝飾器這種語(yǔ)法實(shí)現(xiàn)也是大同小異的。下圖是從angular js官網(wǎng)截取的示例代碼:

Angular2中,大量使用了decorator

實(shí)際使用場(chǎng)景(Logger)

一個(gè)東西被吹得再好,如果沒(méi)有使用場(chǎng)景那也是白搭。
在實(shí)際業(yè)務(wù)中,很多時(shí)候把裝飾器用在日志工具上面,因?yàn)槿罩具@種東西和業(yè)務(wù)幾乎是完全分離的,試想一下,如果業(yè)務(wù)代碼里面參雜了各種各樣的日志信息...., 對(duì)于閱讀代碼邏輯以及維護(hù)來(lái)說(shuō)都是災(zāi)難性的,這個(gè)時(shí)候我們的decorator就能派上用場(chǎng)了。
假設(shè)需要實(shí)現(xiàn)一個(gè)對(duì)定時(shí)任務(wù)的監(jiān)控logger, 需要監(jiān)控何時(shí)開(kāi)始、結(jié)束,以及任務(wù)運(yùn)行耗時(shí)的信息。代碼如下

class ScheduleJob {
  constructor(name) {
    this.name = name;
  }
  @log('info', '開(kāi)始')
  start() {
    setTimeout(() => {
      this.stop();
    }, 2000);
  }
  
  @log('info', '結(jié)束')
  stop() {}
}

var job = new ScheduleJob('liuyan');
job.start();

//輸出: 
//Thu Jan 05 2017 18:34:09 GMT+0800 (CST) - info - 開(kāi)始 
//Thu Jan 05 2017 18:34:09 GMT+0800 (CST) - info - 開(kāi)始....time: 1483612449481
//Thu Jan 05 2017 18:34:12 GMT+0800 (CST) - info - 結(jié)束 
//Thu Jan 05 2017 18:34:12 GMT+0800 (CST) - info - 結(jié)束....time: 1483612452258,耗時(shí):2777ms 

function log(t = 'info', msg = '') {
  return function(target, name, desc) {
    
    const {value} = desc;
    
    desc.value = function(...args) {
      console.log(`${new Date()} - ${t} - ${msg} `)
      let res = value.apply(this, args);
      if(name === 'start') {
        this[`startTime`] = Date.now();
        console.log(`${new Date()} - ${t} - 開(kāi)始....time: ${this[`startTime`]}`)
      }
      if( name === 'stop' ) {
        this[`endTime`] = Date.now();
        console.log(`${new Date()} - ${t} - 結(jié)束....time: ${this[`endTime`]},耗時(shí):${this[`endTime`] - this[`startTime`]}ms`)
      }
    }
  }
}

上面的是一個(gè)很簡(jiǎn)單的需求,我們沒(méi)有修改原有類(lèi)的任何代碼就實(shí)現(xiàn)了日志監(jiān)控。其實(shí)這種實(shí)現(xiàn)在編程器思想里面叫做 AOP,中文名也叫面向切面編程,java里面用得非常之多。

網(wǎng)上有牛人寫(xiě)了一些常用的decorators core-decorators,源碼比較簡(jiǎn)單,可以學(xué)習(xí)學(xué)習(xí)。

參考資料

decorator描述: https://github.com/wycats/javascript-decorators
core-decorators.js https://github.com/jayphelps/core-decorators.js
decorators 文檔 http://tc39.github.io/proposal-decorators/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1 場(chǎng)景問(wèn)題# 1.1 復(fù)雜的獎(jiǎng)金計(jì)算## 考慮這樣一個(gè)實(shí)際應(yīng)用:就是如何實(shí)現(xiàn)靈活的獎(jiǎng)金計(jì)算。 獎(jiǎng)金計(jì)算是相對(duì)復(fù)雜...
    七寸知架構(gòu)閱讀 4,293評(píng)論 4 67
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評(píng)論 19 139
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,820評(píng)論 25 709
  • java 1.5 引入了 泛型 增強(qiáng)循環(huán),可以使用迭代方式(也稱foreach語(yǔ)句) 自動(dòng)裝箱與自動(dòng)拆箱 枚舉 可...
    carway閱讀 602評(píng)論 0 2
  • 去年今日此門(mén)中,人面桃花相映紅。人面不知何處去,桃花依舊笑春風(fēng)。——唐.崔護(hù)《題都城南莊》 三月桃花開(kāi),在看描寫(xiě)桃...
    無(wú)語(yǔ)呤咽閱讀 1,456評(píng)論 15 7

友情鏈接更多精彩內(nèi)容