本文主要爲三方面的內容:javascript
學習的目的是對裝飾者模式模式有進一步的理解,並運用在本身的項目中;對TypeScript裝飾器的理解,更好的使用裝飾器,例如在 nodejs web 框架中、 vue-property-decorator 中,或者是自定義裝飾器,能熟練運用並掌握其基本的實現原理。html
裝飾者模式(Decorator Pattern)也稱爲裝飾器模式,在不改變對象自身的基礎上,動態增長額外的職責。屬於結構型模式的一種。前端
使用裝飾者模式的優勢:把對象核心職責和要裝飾的功能分開了。非侵入式的行爲修改。vue
舉個例子來講,本來長相通常的女孩,藉助美顏功能,也能拍出逆天的顏值。只要善於運用輔助的裝飾功能,開啓瘦臉,增大眼睛,來點磨皮後,咔嚓一拍,驚豔無比。java
通過這一系列疊加的裝飾,你仍是你,長相不增不減,卻能在鏡頭前增長了多重美。若是你願意,還能夠嘗試不一樣的裝飾風格,只要裝飾功能作的好,你就能成爲「百變星君」。node
能夠用代碼表示,把每一個功能抽象成一個類:git
// 女孩子 class Girl { faceValue() { console.log('我本來的臉') } } class ThinFace { constructor(girl) { this.girl = girl; } faceValue() { this.girl.faceValue(); console.log('開啓瘦臉') } } class IncreasingEyes { constructor(girl) { this.girl = girl; } faceValue() { this.girl.faceValue(); console.log('增大眼睛') } } let girl = new Girl(); girl = new ThinFace(girl); girl = new IncreasingEyes(girl); // 閃瞎你的眼 girl.faceValue(); //
從代碼的表現來看,將一個對象嵌入到另外一個對象中,至關於經過一個對象對另外一個對象進行包裝,造成一條包裝鏈。調用後,隨着包裝的鏈條傳遞給每個對象,讓每一個對象都有處理的機會。github
這種方式在增長刪除裝飾功能上都有極大的靈活性,假如你有勇氣展現真實的臉,去掉瘦臉的包裝便可,這對其餘功能毫無影響;假如要增長磨皮,再來個功能類,繼續裝飾下去,對其餘功能也無影響,能夠並存運行。web
在 javascript 中增長小功能使用類,顯的有點笨重,JavaScript 的優勢是靈活,可使用對象來表示:express
let girl = { faceValue() { console.log('我本來的臉') } } function thinFace() { console.log('開啓瘦臉') } function IncreasingEyes() { console.log('增大眼睛') } girl.faceValue = function(){ const originalFaveValue = girl.faceValue; // 原來的功能 return function() { originalFaveValue.call(girl); thinFace.call(girl); } }() girl.faceValue = function(){ const originalFaveValue = girl.faceValue; // 原來的功能 return function() { originalFaveValue.call(girl); IncreasingEyes.call(girl); } }() girl.faceValue();
在不改變原來代碼的基礎上,經過先保留原來函數,從新改寫,在重寫的代碼中調用原來保留的函數。
用一張圖來表示裝飾者模式的原理:
從圖中能夠看出來,經過一層層的包裝,增長了原先對象的功能。
TypeScript 中的裝飾器使用 @expression 這種形式,expression 求值後爲一個函數,它在運行時被調用,被裝飾的聲明信息會被作爲參數傳入。
Javascript規範裏的裝飾器目前處在 建議徵集的第二階段,也就意味着不能在原生代碼中直接使用,瀏覽器暫不支持。
能夠經過 babel 或 TypeScript 工具在編譯階段,把裝飾器語法轉換成瀏覽器可執行的代碼。(最後會有編譯後的源碼分析)
如下主要討論 TypeScript 中裝飾器的使用。
TypeScript 中的裝飾器能夠被附加到類聲明、方法、 訪問符(getter/setter)、屬性和參數上。
開啓對裝飾器的支持,命令行 編譯文件時:
tsc --target ES5 --experimentalDecorators test.ts
配置文件 tsconfig.json
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
裝飾器實際上就是一個函數,在使用時前面加上 @ 符號,寫在要裝飾的聲明以前,多個裝飾器同時做用在一個聲明時,能夠寫一行或換行寫:
// 換行寫 @test1 @test2 declaration //寫一行 @test1 @test2 ... declaration
定義 face.ts 文件:
function thinFace() { console.log('開啓瘦臉') } @thinFace class Girl { }
編譯成 js 代碼,在運行時,會直接調用 thinFace 函數。這個裝飾器做用在類上,稱之爲類裝飾器。
若是須要附加多個功能,能夠組合多個裝飾器一塊兒使用:
function thinFace() { console.log('開啓瘦臉') } function IncreasingEyes() { console.log('增大眼睛') } @thinFace @IncreasingEyes class Girl { }
多個裝飾器組合在一塊兒,在運行時,要注意,調用順序是 從下至上 依次調用,正好和書寫的順序相反。例子中給出的運行結果是:
'增大眼睛' '開啓瘦臉'
若是你要在一個裝飾器中給類添加屬性,在其餘的裝飾器中使用,那就要寫在最後一個裝飾器中,由於最後寫的裝飾器最早調用。
有時須要給裝飾器傳遞一些參數,這要藉助於裝飾器工廠函數。裝飾器工廠函數實際上就是一個高階函數,在調用後返回一個函數,返回的函數做爲裝飾器函數。
function thinFace(value: string){ console.log('1-瘦臉工廠方法') return function(){ console.log(`4-我是瘦臉的裝飾器,要瘦臉${value}`) } } function IncreasingEyes(value: string) { console.log('2-增大眼睛工廠方法') return function(){ console.log(`3-我是增大眼睛的裝飾器,要${value}`) } } @thinFace('50%') @IncreasingEyes('增大一倍') class Girl { }
@ 符號後爲調用工廠函數,依次從上到下執行,目的是求得裝飾器函數。裝飾器函數的運行順序依然是從下到上依次執行。
運行的結果爲:
1-瘦臉工廠方法 2-增大眼睛工廠方法 3-我是增大眼睛的裝飾器,要增大一倍 4-我是瘦臉的裝飾器,要瘦臉50%
總結一下:
做用在類聲明上的裝飾器,能夠給咱們改變類的機會。在執行裝飾器函數時,會把類構造函數傳遞給裝飾器函數。
function classDecorator(value: string){ return function(constructor){ console.log('接收一個構造函數') } } function thinFace(constructor){ constructor.prototype.thinFaceFeature = function() { console.log('瘦臉功能') } } @thinFace @classDecorator('類裝飾器') class Girl {} let g = new Girl(); g.thinFaceFeature(); // '瘦臉功能'
上面的例子中,拿到傳遞構造函數後,就能夠給構造函數原型上增長新的方法,甚至也能夠繼承別的類。
做用在類的方法上,有靜態方法和原型方法。做用在靜態方法上,裝飾器函數接收的是類構造函數;做用在原型方法上,裝飾器函數接收的是原型對象。
這裏拿做用在原型方法上舉例。
function methodDecorator(value: string, Girl){ return function(prototype, key, descriptor){ console.log('接收原型對象,裝飾的屬性名,屬性描述符', Girl.prototype === prototype) } } function thinFace(prototype, key, descriptor){ // 保留原來的方法邏輯 let originalMethod = descriptor.value; // 改寫,增長邏輯,並執行原有邏輯 descriptor.value = function(){ originalMethod.call(this); // 注意修改this的指向 console.log('開啓瘦臉模式') } } class Girl { @thinFace @methodDecorator('方式裝飾器', Girl) faceValue(){ console.log('我是本來的面目') } } let g = new Girl(); g.faceValue();
從代碼中能夠看出,裝飾器函數接收三個參數,原型對象、方法名、描述對象。對描述對象陌生的,能夠參考 這裏;
要加強功能,能夠先保留原來的函數,改寫描述對象的 value 爲另外一函數。
當使用 g.faceValue() 訪問方法時,訪問的就是描述對象 value 對應的值。
在改寫的函數中增長邏輯,並執行原來保留的原函數。注意原函數要用 call 或 apply 將 this 指向原型對象。
做用在類中定義的屬性上,這些屬性不是原型上的屬性,而是經過類實例化獲得的實例對象上的屬性。
裝飾器一樣會接受兩個參數,原型對象,和屬性名。而沒有屬性描述對象,爲何呢?這與TypeScript是如何初始化屬性裝飾器的有關。 目前沒有辦法在定義一個原型對象的成員時描述一個實例屬性。
function propertyDecorator(value: string, Girl){ return function(prototype, key){ console.log('接收原型對象,裝飾的屬性名,屬性描述符', Girl.prototype === prototype) } } function thinFace(prototype, key){ console.log(prototype, key) } class Girl { @thinFace @propertyDecorator('屬性裝飾器', Girl) public age: number = 18; } let g = new Girl(); console.log(g.age); // 18
下面組合多個裝飾器寫在一塊兒,出了上面提到的三種,還有 訪問符裝飾器、參數裝飾器。這些裝飾器在一塊兒時,會有執行順序。
function classDecorator(value: string){ console.log(value) return function(){} } function propertyDecorator(value: string) { console.log(value) return function(){ console.log('propertyDecorator') } } function methodDecorator(value: string) { console.log(value) return function(){ console.log('methodDecorator') } } function paramDecorator(value: string) { console.log(value) return function(){ console.log('paramDecorator') } } function AccessDecorator(value: string) { console.log(value) return function(){ console.log('AccessDecorator') } } function thinFace(){ console.log('瘦臉') } function IncreasingEyes() { console.log('增大眼睛') } @thinFace @classDecorator('類裝飾器') class Girl { @propertyDecorator('屬性裝飾器') age: number = 18; @AccessDecorator('訪問符裝飾器') get city(){} @methodDecorator('方法裝飾器') @IncreasingEyes faceValue(){ console.log('本來的臉') } getAge(@paramDecorator('參數裝飾器') name: string){} }
運行了這段編譯後的代碼,會發現這些訪問器的順序是,屬性裝飾器 -> 訪問符裝飾器 -> 方法裝飾器 -> 參數裝飾器 -> 類裝飾器。
更詳細的用法能夠參考官網文檔:https://www.tslang.cn/docs/handbook/decorators.html#decorator-factories
裝飾器在瀏覽器中不支持,沒辦法直接使用,須要通過工具編譯成瀏覽器可執行的代碼。
分析一下經過工具編譯後的代碼。
生成 face.js 文件:
tsc --target ES5 --experimentalDecorators face.ts
打開 face.js 文件,會看到一段被壓縮後的代碼,能夠格式化一下。
先看這段代碼:
__decorate([ propertyDecorator('屬性裝飾器') ], Girl.prototype, "age", void 0); __decorate([ AccessDecorator('訪問符裝飾器') ], Girl.prototype, "city", null); __decorate([ methodDecorator('方法裝飾器'), IncreasingEyes ], Girl.prototype, "faceValue", null); __decorate([ __param(0, paramDecorator('參數裝飾器')) ], Girl.prototype, "getAge", null); Girl = __decorate([ thinFace, classDecorator('類裝飾器') ], Girl);
__decorate 的做用就是執行裝飾器函數,從這段代碼中可以看出不少信息,印證上面獲得的結論。
經過__decorate調用順序,能夠看出來,多個類型的裝飾器一塊兒使用時,順序是,屬性裝飾器 -> 訪問符裝飾器 -> 方法裝飾器 -> 參數裝飾器 -> 類裝飾器。
調用了 __decorate 函數,根據使用的裝飾器類型不一樣,傳入的參數也不相同。
第一個參數傳入的都同樣,爲數組,這樣確保和咱們書寫的順序一致,每一項是求值後的裝飾器函數,若是寫的是 @propertyDecorator() 則一上來就執行,獲得裝飾器函數,這跟上面分析的一致。
類裝飾器會把類做爲第二個參數,其餘的裝飾器,把原型對象做爲第二個參數,屬性名做爲第三個,第四個是 null 或 void 0。void 0的值爲undefined,也就等於沒傳參數
要記住傳給 __decorate 函數參數的個數和值,在深刻到 __decorate 源碼中, 會根據這些值來決定執行裝飾器函數時,傳入參數的多少。
好,來看 __decorate 函數實現:
// 已存在此函數,直接使用,不然本身定義 var __decorate = (this && this.__decorate) || // 接收四個參數: //decorators存放裝飾器函數的數組、target原型對象|類, //key屬性名、desc描述(undefined或null) function(decorators, target, key, desc) { var c = arguments.length, // 拿到參數的個數 r = c < 3 // 參數小於三個,說明是類裝飾器,直接拿到類 ? target : desc === null // 第四個參數爲 null,則須要描述對象;屬性裝飾器傳入是 void 0,沒有描述對象。 ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; // 若是提供了Reflect.decorate方法,直接調用;不然本身實現 if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else // 裝飾器函數執行順序和書寫的順序相反,從下至上 執行 for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) // 拿到裝飾器函數 r = (c < 3 // 參數小於3個,說明是類裝飾器,執行裝飾器函數,直接傳入類 ? d(r) : c > 3 // 參數大於三個,是方法裝飾器、訪問符裝飾器、參數裝飾器,則執行傳入描述對象 ? d(target, key, r) : d(target, key) // 爲屬性裝飾器,不傳入描述對象 ) || r; // 給被裝飾的屬性,設置獲得的描述對象,主要是針對,方法、屬性來講的 /*** * r 的值分兩種狀況, * 一種是經過上面的 Object.getOwnPropertyDescriptor 獲得的值 * 另外一種,是裝飾器函數執行後的返回值,做爲描述對象。 * 通常不給裝飾器函數返回值。 */ return c > 3 && r && Object.defineProperty(target, key, r),r; };
上面的參數裝飾器,調用了一個函數爲 __params,
var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } };
目的是,要給裝飾器函數傳入參數的位置 paramIndex。
看了編譯後的源碼,相信會對裝飾器的理解更深入。
以上若有誤差歡迎指正學習,謝謝。~~~~
github博客地址:https://github.com/WYseven/blog,歡迎star。
若是對你有幫助,請關注【前端技能解鎖】: