淺談JS中的裝飾器

什么是裝飾器贵白?

裝飾器模式(Decorator Pattern)是一種結(jié)構(gòu)型設(shè)計(jì)模式崩泡,旨在促進(jìn)代碼復(fù)用,可以用于修改現(xiàn)有的系統(tǒng)圈浇,希望在系統(tǒng)中為對(duì)象添加額外的功能靴寂,同時(shí)又不需要大量修改原有的代碼百炬。

JS中的裝飾器是ES7中的一個(gè)新語法,可以對(duì)庶弃、方法德澈、屬性進(jìn)行修飾,從而進(jìn)行一些相關(guān)功能定制, 它的寫法與Java的注解(Annotation)類似缴守,但是功能有比較大的區(qū)別镇辉。

大家可能聽說過 組合函數(shù) 和 高階函數(shù) 的概念,也可以這么理解村砂。

我們先來看一下以下代碼:

function doSomething(name) {
  console.log('Hi, I\'' + name);
}

funtion useLogging(func, name) {
    console.log('Starting');
    func(name);
    console.log('Finished');
}

以上邏輯不難理解屹逛,給原有的函數(shù)加一個(gè)打日志的功能,但是這樣的話色迂,每次都要傳參數(shù)給useLogging歇僧,而且破壞了之前的代碼結(jié)構(gòu),之前直接doSomething就好了祸轮,現(xiàn)在要改成useLogging(doSomething, 'Jiang')侥钳。
那有沒有更好的方式呢,當(dāng)然有啦苦酱。

簡(jiǎn)單裝飾器:

function useLogging(func) {
    return function() {
        console.log('Starting');
        const result = func.apply(this, arguments)
        console.log('Done');
        return result;
    }
}

const wrapped = useLogging(doSomething);

以上代碼返回了一個(gè)新的函數(shù) wrapped , 調(diào)用方式和doSomething相同给猾,在原來的基礎(chǔ)上能做多一點(diǎn)事情敢伸。

doSomething('angry');
// Hi, I'angry

const wrapped = useLogging(doSomething);


wrapped('angry');
// Starting
// Hi, I'angry
// Done

怎么使用裝飾器?

裝飾器主要有兩種用法:

  • 裝飾類方法或?qū)傩?類成員)
class MyClass {
  @readonly
  method() { }
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}
  • 裝飾類
@annotation
class MyClass { }

function annotation(target) {
   target.annotated = true;
}

類成員裝飾器

類成員裝飾器用來裝飾類里面的屬性尾序、方法躯砰、gettersetter琢歇。這個(gè)裝飾器函數(shù)調(diào)用三個(gè)參數(shù)調(diào):

  • target: 被裝飾的類的原型
  • name: 被裝飾的類、屬性、方法的名字
  • descriptor: 被裝飾的類尚揣、屬性快骗、方法的descriptor塔次,將傳遞給Object.defineProperty

我們來寫幾個(gè)裝飾器名秀,代碼如下:

寫一個(gè)@readonly裝飾器匕得,簡(jiǎn)單版實(shí)現(xiàn):

class Example {
  @log
  add(a, b) {
    return a + b;
  }

  @unenumerable
  @readonly
  name = "alibaba"
}

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

function unenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

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;
}

const e = new Example();

// Calling add with [2, 4]
e.add(2, 4);
e.name = 'antd'; // Error

我們可以通過Babel查看編譯后的代碼,也可以在本地編譯略吨。

npm i @babel/core @babel/cli
npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D

.babelrc文件

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", {"loose": true}]
  ]
}

編譯 ES6 語法輸出到文件

因?yàn)闆]用全局安裝@babel/cli, 建議用 npx 命令來執(zhí)行翠忠,或者./node_modules/.bin/babel乞榨,關(guān)于npx命令,可以看下官方文檔

npx babel decorator.js --out-file complied.js

編譯后的代碼:

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;
}

_applyDecoratedDescriptor(_class.prototype, "add", [log], Object.getOwnPropertyDescriptor(_class.prototype, "add"), _class.prototype)

Babel 構(gòu)建了一個(gè) _applyDecoratedDescriptor函數(shù),用于裝飾類成員

Object.getOwnPropertyDescriptor

Object.getOwnPropertyDescriptor()方法返回指定對(duì)象上一個(gè)自有屬性對(duì)應(yīng)的屬性描述符董虱。(自有屬性指的是直接賦予該對(duì)象的屬性申鱼,不需要從原型鏈上進(jìn)行查找的屬性),不是原型鏈上的這點(diǎn)很關(guān)鍵淫半。

詳情可以查看官方文檔匣砖,這里就不細(xì)說了。

var desc = {};
  // 這里對(duì) descriptor 屬性做了一層拷貝
  Object['ke' + 'ys'](descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;
  // 沒有 value 或者 initializer 屬性的对人,都是 get 和 set 方法
  if ('value' in desc || desc.initializer) {
    desc.writable = true;
  }

這里的 initializer 是 Babel 為了配合 decorator 而產(chǎn)生的一個(gè)屬性牺弄,就比方說對(duì)于上面代碼中的 name 屬性宜狐,被編譯成:

_descriptor = _applyDecoratedDescriptor(_class.prototype, "name", [unenumerable, readonly], {
  configurable: true,
  enumerable: true,
  writable: true,
  initializer: function initializer() {
    return "alibaba";
  }
})
desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    return decorator(target, property, desc) || desc;
  }, desc);

處理多個(gè) decorator 的情況,這里執(zhí)行了slice()和reverse()咱台,所以我們可以得出回溺,一個(gè)類成員有多個(gè)裝飾器,會(huì)由內(nèi)向外執(zhí)行馅而。

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;

最后無論是裝飾方法還是屬性瓮恭,都會(huì)執(zhí)行:

Object["define" + "Property"](target, property, desc);

由此可見,裝飾方法本質(zhì)上還是使用 Object.defineProperty() 來實(shí)現(xiàn)的维哈。

類裝飾器

類裝飾器相對(duì)簡(jiǎn)單

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 {}

裝飾器中傳入?yún)?shù):

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 {}

應(yīng)用

在 React 中阔挠,經(jīng)常會(huì)用到 redux 或者高階組件脑蠕。

class A extends React.Component {}
export default connect()(A);

裝飾器寫法:

@connect()
export default connect()(A);

總結(jié)

Decorator 雖然原理非常簡(jiǎn)單,但是的確可以實(shí)現(xiàn)很多實(shí)用又方便的功能.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末迂求,一起剝皮案震驚了整個(gè)濱河市揩局,隨后出現(xiàn)的幾起案子掀虎,更是在濱河造成了極大的恐慌,老刑警劉巖驰怎,帶你破解...
    沈念sama閱讀 210,835評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異二打,居然都是意外死亡砸西,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芹枷,“玉大人,你說我怎么就攤上這事莲趣≡Т龋” “怎么了?”我有些...
    開封第一講書人閱讀 156,481評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵喧伞,是天一觀的道長(zhǎng)走芋。 經(jīng)常有香客問我潘鲫,道長(zhǎng)翁逞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,303評(píng)論 1 282
  • 正文 為了忘掉前任溉仑,我火速辦了婚禮挖函,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘浊竟。我一直安慰自己怨喘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,375評(píng)論 5 384
  • 文/花漫 我一把揭開白布振定。 她就那樣靜靜地躺著必怜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪后频。 梳的紋絲不亂的頭發(fā)上梳庆,一...
    開封第一講書人閱讀 49,729評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音卑惜,去河邊找鬼膏执。 笑死,一個(gè)胖子當(dāng)著我的面吹牛残揉,可吹牛的內(nèi)容都是我干的胧后。 我是一名探鬼主播,決...
    沈念sama閱讀 38,877評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼抱环,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼壳快!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起镇草,我...
    開封第一講書人閱讀 37,633評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤眶痰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后梯啤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竖伯,經(jīng)...
    沈念sama閱讀 44,088評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,443評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了七婴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祟偷。...
    茶點(diǎn)故事閱讀 38,563評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖打厘,靈堂內(nèi)的尸體忽然破棺而出修肠,到底是詐尸還是另有隱情,我是刑警寧澤户盯,帶...
    沈念sama閱讀 34,251評(píng)論 4 328
  • 正文 年R本政府宣布嵌施,位于F島的核電站,受9級(jí)特大地震影響莽鸭,放射性物質(zhì)發(fā)生泄漏吗伤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,827評(píng)論 3 312
  • 文/蒙蒙 一硫眨、第九天 我趴在偏房一處隱蔽的房頂上張望足淆。 院中可真熱鬧,春花似錦捺球、人聲如沸缸浦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽裂逐。三九已至,卻和暖如春泣栈,著一層夾襖步出監(jiān)牢的瞬間卜高,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工南片, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掺涛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,240評(píng)論 2 360
  • 正文 我出身青樓疼进,卻偏偏與公主長(zhǎng)得像薪缆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伞广,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,435評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 隨著 ES6 和 TypeScript 中類的引入拣帽,使得我們?cè)诙鄠€(gè)不同類之間共享或者擴(kuò)展一些方法或者行為的時(shí)候,變...
    折木丶青梵閱讀 17,607評(píng)論 1 14
  • 本文為阮一峰大神的《ECMAScript 6 入門》的個(gè)人版提純嚼锄! babel babel負(fù)責(zé)將JS高級(jí)語法轉(zhuǎn)義减拭,...
    Devildi已被占用閱讀 1,972評(píng)論 0 4
  • 包(lib)、模塊(module) 在Python中区丑,存在包和模塊兩個(gè)常見概念拧粪。 模塊:編寫Python代碼的py...
    清清子衿木子水心閱讀 3,801評(píng)論 0 27
  • 網(wǎng)絡(luò)中四修陡,謝芳,9月29日可霎,持續(xù)分享79天 我們常常發(fā)現(xiàn)魄鸦,經(jīng)驗(yàn)豐富的心理咨詢師,在個(gè)案剛開始時(shí)癣朗,常以“嗯号杏,啊,...
    xfsunshine閱讀 790評(píng)論 0 2
  • 原來我沒有想像中的自信
    xhmdsj閱讀 180評(píng)論 2 1