JS裝飾者模式和TypeScript裝飾器
裝飾者模式(Decorator Pattern)也稱為裝飾器模式,在不改變對(duì)象自身的基礎(chǔ)上,動(dòng)態(tài)增加額外的職責(zé)。屬于結(jié)構(gòu)型模式的一種。
使用裝飾者模式的優(yōu)點(diǎn):把對(duì)象核心職責(zé)和要裝飾的功能分開了。非侵入式的行為修改。
舉個(gè)例子來說,原本長(zhǎng)相一般的女孩,借助美顏功能,也能拍出逆天的顏值。只要善于運(yùn)用輔助的裝飾功能,開啟瘦臉,增大眼睛,來點(diǎn)磨皮后,咔嚓一拍,驚艷無比。
經(jīng)過這一系列疊加的裝飾,你還是你,長(zhǎng)相不增不減,卻能在鏡頭前增加了多重美。如果你愿意,還可以嘗試不同的裝飾風(fēng)格,只要裝飾功能做的好,你就能成為“百變星君”。
可以用代碼表示,把每個(gè)功能抽象成一個(gè)類:
// 女孩子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(); //
從代碼的表現(xiàn)來看,將一個(gè)對(duì)象嵌入到另一個(gè)對(duì)象中,相當(dāng)于通過一個(gè)對(duì)象對(duì)另一個(gè)對(duì)象進(jìn)行包裝,形成一條包裝鏈。調(diào)用后,隨著包裝的鏈條傳遞給每一個(gè)對(duì)象,讓每個(gè)對(duì)象都有處理的機(jī)會(huì)。
這種方式在增加刪除裝飾功能上都有極大的靈活性,假如你有勇氣展示真實(shí)的臉,去掉瘦臉的包裝即可,這對(duì)其他功能毫無影響;假如要增加磨皮,再來個(gè)功能類,繼續(xù)裝飾下去,對(duì)其他功能也無影響,可以并存運(yùn)行。
在JavaScript中增加小功能使用類,顯的有點(diǎn)笨重,JavaScript的優(yōu)點(diǎn)是靈活,可以使用對(duì)象來表示:
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();
在不改變?cè)瓉泶a的基礎(chǔ)上,通過先保留原來函數(shù),重新改寫,在重寫的代碼中調(diào)用原來保留的函數(shù)。
用一張圖來表示裝飾者模式的原理:
從圖中可以看出來,通過一層層的包裝,增加了原先對(duì)象的功能。
TypeScript中的裝飾器TypeScript 中的裝飾器使用 @expression 這種形式,expression 求值后為一個(gè)函數(shù),它在運(yùn)行時(shí)被調(diào)用,被裝飾的聲明信息會(huì)被做為參數(shù)傳入。
Javascript規(guī)范里的裝飾器目前處在建議征集的第二階段,也就意味著不能在原生代碼中直接使用,瀏覽器暫不支持。
可以通過babel或TypeScript工具在編譯階段,把裝飾器語法轉(zhuǎn)換成瀏覽器可執(zhí)行的代碼。(最后會(huì)有編譯后的源碼分析)
以下主要討論TypeScript中裝飾器的使用。
TypeScript 中的裝飾器可以被附加到類聲明、方法、 訪問符(getter/setter)、屬性和參數(shù)上。
開啟對(duì)裝飾器的支持,命令行編譯文件時(shí):
tsc --target ES5 --experimentalDecorators test.ts
配置文件tsconfig.json
{ 'compilerOptions': {'target': 'ES5','experimentalDecorators': true }}裝飾器的使用
裝飾器實(shí)際上就是一個(gè)函數(shù),在使用時(shí)前面加上@符號(hào),寫在要裝飾的聲明之前,多個(gè)裝飾器同時(shí)作用在一個(gè)聲明時(shí),可以寫一行或換行寫:
// 換行寫@test1@test2declaration//寫一行@test1 @test2 ...declaration
定義face.ts文件:
function thinFace() { console.log(’開啟瘦臉’)}@thinFaceclass Girl {}
編譯成js代碼,在運(yùn)行時(shí),會(huì)直接調(diào)用thinFace函數(shù)。這個(gè)裝飾器作用在類上,稱之為類裝飾器。
如果需要附加多個(gè)功能,可以組合多個(gè)裝飾器一起使用:
function thinFace() { console.log(’開啟瘦臉’)}function IncreasingEyes() { console.log(’增大眼睛’)}@thinFace@IncreasingEyesclass Girl {}
多個(gè)裝飾器組合在一起,在運(yùn)行時(shí),要注意,調(diào)用順序是從下至上依次調(diào)用,正好和書寫的順序相反。例子中給出的運(yùn)行結(jié)果是:
’增大眼睛’
’開啟瘦臉’
如果你要在一個(gè)裝飾器中給類添加屬性,在其他的裝飾器中使用,那就要寫在最后一個(gè)裝飾器中,因?yàn)樽詈髮懙难b飾器最先調(diào)用。
裝飾器工廠有時(shí)需要給裝飾器傳遞一些參數(shù),這要借助于裝飾器工廠函數(shù)。裝飾器工廠函數(shù)實(shí)際上就是一個(gè)高階函數(shù),在調(diào)用后返回一個(gè)函數(shù),返回的函數(shù)作為裝飾器函數(shù)。
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 {}
@符號(hào)后為調(diào)用工廠函數(shù),依次從上到下執(zhí)行,目的是求得裝飾器函數(shù)。裝飾器函數(shù)的運(yùn)行順序依然是從下到上依次執(zhí)行。
運(yùn)行的結(jié)果為:
1-瘦臉工廠方法
2-增大眼睛工廠方法
3-我是增大眼睛的裝飾器,要增大一倍
4-我是瘦臉的裝飾器,要瘦臉50%
總結(jié)一下:
寫了工廠函數(shù),從上到下依次執(zhí)行,求得裝飾器函數(shù)。 裝飾器函數(shù)的執(zhí)行順序是從下到上依次執(zhí)行。類裝飾器作用在類聲明上的裝飾器,可以給我們改變類的機(jī)會(huì)。在執(zhí)行裝飾器函數(shù)時(shí),會(huì)把類構(gòu)造函數(shù)傳遞給裝飾器函數(shù)。
function classDecorator(value: string){ return function(constructor){ console.log(’接收一個(gè)構(gòu)造函數(shù)’) }}function thinFace(constructor){ constructor.prototype.thinFaceFeature = function() { console.log(’瘦臉功能’) }}@thinFace@classDecorator(’類裝飾器’)class Girl {}let g = new Girl();g.thinFaceFeature(); // ’瘦臉功能’
上面的例子中,拿到傳遞構(gòu)造函數(shù)后,就可以給構(gòu)造函數(shù)原型上增加新的方法,甚至也可以繼承別的類。
方法裝飾器作用在類的方法上,有靜態(tài)方法和原型方法。作用在靜態(tài)方法上,裝飾器函數(shù)接收的是類構(gòu)造函數(shù);作用在原型方法上,裝飾器函數(shù)接收的是原型對(duì)象。這里拿作用在原型方法上舉例。
function methodDecorator(value: string, Girl){ return function(prototype, key, descriptor){ console.log(’接收原型對(duì)象,裝飾的屬性名,屬性描述符’, Girl.prototype === prototype) }}function thinFace(prototype, key, descriptor){ // 保留原來的方法邏輯 let originalMethod = descriptor.value; // 改寫,增加邏輯,并執(zhí)行原有邏輯 descriptor.value = function(){ originalMethod.call(this); // 注意修改this的指向 console.log(’開啟瘦臉模式’) }}class Girl { @thinFace @methodDecorator(’方式裝飾器’, Girl) faceValue(){ console.log(’我是原本的面目’) }}let g = new Girl();g.faceValue();
從代碼中可以看出,裝飾器函數(shù)接收三個(gè)參數(shù),原型對(duì)象、方法名、描述對(duì)象。對(duì)描述對(duì)象陌生的,可以參考這里;
要增強(qiáng)功能,可以先保留原來的函數(shù),改寫描述對(duì)象的value為另一函數(shù)。
當(dāng)使用g.faceValue()訪問方法時(shí),訪問的就是描述對(duì)象value對(duì)應(yīng)的值。
在改寫的函數(shù)中增加邏輯,并執(zhí)行原來保留的原函數(shù)。注意原函數(shù)要用call或apply將this指向原型對(duì)象。
屬性裝飾器作用在類中定義的屬性上,這些屬性不是原型上的屬性,而是通過類實(shí)例化得到的實(shí)例對(duì)象上的屬性。
裝飾器同樣會(huì)接受兩個(gè)參數(shù),原型對(duì)象,和屬性名。而沒有屬性描述對(duì)象,為什么呢?這與TypeScript是如何初始化屬性裝飾器的有關(guān)。 目前沒有辦法在定義一個(gè)原型對(duì)象的成員時(shí)描述一個(gè)實(shí)例屬性。
function propertyDecorator(value: string, Girl){ return function(prototype, key){ console.log(’接收原型對(duì)象,裝飾的屬性名,屬性描述符’, 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其他裝飾器的寫法
下面組合多個(gè)裝飾器寫在一起,出了上面提到的三種,還有 訪問符裝飾器、參數(shù)裝飾器。這些裝飾器在一起時(shí),會(huì)有執(zhí)行順序。
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(’參數(shù)裝飾器’) name: string){}}
運(yùn)行了這段編譯后的代碼,會(huì)發(fā)現(xiàn)這些訪問器的順序是,屬性裝飾器 -> 訪問符裝飾器 -> 方法裝飾器 -> 參數(shù)裝飾器 -> 類裝飾器。
更詳細(xì)的用法可以參考官網(wǎng)文檔:https://www.tslang.cn/docs/handbook/decorators.html#decorator-factories
裝飾器運(yùn)行時(shí)代碼分析裝飾器在瀏覽器中不支持,沒辦法直接使用,需要經(jīng)過工具編譯成瀏覽器可執(zhí)行的代碼。
分析一下通過工具編譯后的代碼。
生成face.js文件:
tsc --target ES5 --experimentalDecorators face.ts
打開face.js文件,會(huì)看到一段被壓縮后的代碼,可以格式化一下。
先看這段代碼:
__decorate([ propertyDecorator(’屬性裝飾器’)], Girl.prototype, 'age', void 0);__decorate([ AccessDecorator(’訪問符裝飾器’)], Girl.prototype, 'city', null);__decorate([ methodDecorator(’方法裝飾器’), IncreasingEyes], Girl.prototype, 'faceValue', null);__decorate([ __param(0, paramDecorator(’參數(shù)裝飾器’))], Girl.prototype, 'getAge', null);Girl = __decorate([ thinFace, classDecorator(’類裝飾器’)], Girl);
__decorate的作用就是執(zhí)行裝飾器函數(shù),從這段代碼中能夠看出很多信息,印證上面得到的結(jié)論。
通過__decorate調(diào)用順序,可以看出來,多個(gè)類型的裝飾器一起使用時(shí),順序是,屬性裝飾器 -> 訪問符裝飾器 -> 方法裝飾器 -> 參數(shù)裝飾器 -> 類裝飾器。
調(diào)用了__decorate函數(shù),根據(jù)使用的裝飾器類型不同,傳入的參數(shù)也不相同。
第一個(gè)參數(shù)傳入的都一樣,為數(shù)組,這樣確保和我們書寫的順序一致,每一項(xiàng)是求值后的裝飾器函數(shù),如果寫的是@propertyDecorator()則一上來就執(zhí)行,得到裝飾器函數(shù),這跟上面分析的一致。
類裝飾器會(huì)把類作為第二個(gè)參數(shù),其他的裝飾器,把原型對(duì)象作為第二個(gè)參數(shù),屬性名作為第三個(gè),第四個(gè)是null或void 0。void 0的值為undefined,也就等于沒傳參數(shù)
要記住傳給__decorate函數(shù)參數(shù)的個(gè)數(shù)和值,在深入到__decorate源碼中, 會(huì)根據(jù)這些值來決定執(zhí)行裝飾器函數(shù)時(shí),傳入?yún)?shù)的多少。
好,來看__decorate函數(shù)實(shí)現(xiàn):
// 已存在此函數(shù),直接使用,否則自己定義var __decorate = (this && this.__decorate) ||// 接收四個(gè)參數(shù): //decorators存放裝飾器函數(shù)的數(shù)組、target原型對(duì)象|類,//key屬性名、desc描述(undefined或null)function(decorators, target, key, desc) { var c = arguments.length, // 拿到參數(shù)的個(gè)數(shù) r = c < 3 // 參數(shù)小于三個(gè),說明是類裝飾器,直接拿到類 ? target : desc === null // 第四個(gè)參數(shù)為 null,則需要描述對(duì)象;屬性裝飾器傳入是 void 0,沒有描述對(duì)象。? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; // 如果提供了Reflect.decorate方法,直接調(diào)用;否則自己實(shí)現(xiàn) if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else // 裝飾器函數(shù)執(zhí)行順序和書寫的順序相反,從下至上 執(zhí)行 for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) // 拿到裝飾器函數(shù) r = (c < 3 // 參數(shù)小于3個(gè),說明是類裝飾器,執(zhí)行裝飾器函數(shù),直接傳入類 ? d(r) : c > 3 // 參數(shù)大于三個(gè),是方法裝飾器、訪問符裝飾器、參數(shù)裝飾器,則執(zhí)行傳入描述對(duì)象 ? d(target, key, r) : d(target, key) // 為屬性裝飾器,不傳入描述對(duì)象 ) || r; // 給被裝飾的屬性,設(shè)置得到的描述對(duì)象,主要是針對(duì),方法、屬性來說的 /*** * r 的值分兩種情況, * 一種是通過上面的 Object.getOwnPropertyDescriptor 得到的值 * 另一種,是裝飾器函數(shù)執(zhí)行后的返回值,作為描述對(duì)象。 * 一般不給裝飾器函數(shù)返回值。 */ return c > 3 && r && Object.defineProperty(target, key, r),r;};
上面的參數(shù)裝飾器,調(diào)用了一個(gè)函數(shù)為__params,
var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }};
目的是,要給裝飾器函數(shù)傳入?yún)?shù)的位置paramIndex。
看了編譯后的源碼,相信會(huì)對(duì)裝飾器的理解更深刻。
以上就是JS裝飾者模式和TypeScript裝飾器的詳細(xì)內(nèi)容,更多關(guān)于JS的資料請(qǐng)關(guān)注好吧啦網(wǎng)其它相關(guān)文章!
相關(guān)文章:
1. Java之JSP教程九大內(nèi)置對(duì)象詳解(中篇)2. 基于python計(jì)算滾動(dòng)方差(標(biāo)準(zhǔn)差)talib和pd.rolling函數(shù)差異詳解3. CSS自定義滾動(dòng)條樣式案例詳解4. JS繪圖Flot如何實(shí)現(xiàn)動(dòng)態(tài)可刷新曲線圖5. 詳解CSS不定寬溢出文本適配滾動(dòng)6. 基于android studio的layout的xml文件的創(chuàng)建方式7. 使用ProcessBuilder調(diào)用外部命令,并返回大量結(jié)果8. 詳解Python中openpyxl模塊基本用法9. Java發(fā)送http請(qǐng)求的示例(get與post方法請(qǐng)求)10. springboot基于Redis發(fā)布訂閱集群下WebSocket的解決方案
