裝飾者模式和TypeScript裝飾器
導讀
本文主要為三方面的內容:
- 裝飾者模式的概念和使用
- Typescript裝飾器的使用、執行順序
- 編譯後的原始碼分析
學習的目的是對裝飾者模式模式有進一步的理解,並運用在自己的專案中;對TypeScript裝飾器的理解,更好的使用裝飾器,例如在nodejsweb框架中、vue-property-decorator中,或者是自定義裝飾器,能熟練運用並掌握其基本的實現原理。
裝飾者模式介紹
裝飾者模式(Decorator Pattern)也稱為裝飾器模式,在不改變物件自身的基礎上,動態增加額外的職責。屬於結構型模式的一種。
使用裝飾者模式的優點:把物件核心職責和要裝飾的功能分開了。非侵入式的行為修改。
舉個例子來說,原本長相一般的女孩,藉助美顏功能,也能拍出逆天的顏值。只要善於運用輔助的裝飾功能,開啟瘦臉,增大眼睛,來點磨皮後,咔嚓一拍,驚豔無比。
經過這一系列疊加的裝飾,你還是你,長相不增不減,卻能在鏡頭前增加了多重美。如果你願意,還可以嘗試不同的裝飾風格,只要裝飾功能做的好,你就能成為“百變星君”。
可以用程式碼表示,把每個功能抽象成一個類:
// 女孩子
class Girl {
faceValue() {
console.log('我原本的臉')
}
}
class ThinFace {
constructor(girl) {
this.girl = girl;
}
faceValue() {
this.girl.faceValue();
console.log('開啟瘦臉')
}
}
class IncreasingEyes {
const ructor(girl) {
this.girl = girl;
}
faceValue() {
this.girl.faceValue();
console.log('增大眼睛')
}
}
let girl = new Girl();
girl = new ThinFace(girl);
girl = new IncreasingEyes(girl);
// 閃瞎你的眼
girl.faceValue(); //
從程式碼的表現來看,將一個物件嵌入到另一個物件中,相當於通過一個物件對另一個物件進行包裝,形成一條包裝鏈。呼叫後,隨著包裝的鏈條傳遞給每一個物件,讓每個物件都有處理的機會。
這種方式在增加刪除裝飾功能上都有極大的靈活性,假如你有勇氣展示真實的臉,去掉瘦臉的包裝即可,這對其他功能毫無影響;假如要增加磨皮,再來個功能類,繼續裝飾下去,對其他功能也無影響,可以並存執行。
在JavaScript中增加小功能使用類,顯的有點笨重,JavaScript的優點是靈活,可以使用物件來表示:
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中的裝飾器
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
廣州設計公司https://www.houdianzi.com 我的007辦公資源網站https://www.wode007.com
裝飾器執行時程式碼分析
裝飾器在瀏覽器中不支援,沒辦法直接使用,需要經過工具編譯成瀏覽器可執行的程式碼。
分析一下通過工具編譯後的程式碼。
生成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。
看了編譯後的原始碼,相信會對裝飾器的理解更深刻。