沐鳴平台首頁_裝飾者模式和TypeScript裝飾器

導讀

本文主要為三方面的內容:

  1. 裝飾者模式的概念和使用
  2. Typescript裝飾器的使用、執行順序
  3. 編譯后的源碼分析

學習的目的是對裝飾者模式模式有進一步的理解,並運用在自己的項目中;對TypeScript裝飾器的理解,更好的使用裝飾器,例如在 nodejs web 框架中、 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  {
  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(); // 

從代碼的表現來看,將一個對象嵌入到另一個對象中,相當於通過一個對象對另一個對象進行包裝,形成一條包裝鏈。調用后,隨着包裝的鏈條傳遞給每一個對象,讓每個對象都有處理的機會。

這種方式在增加刪除裝飾功能上都有極大的靈活性,假如你有勇氣展示真實的臉,去掉瘦臉的包裝即可,這對其他功能毫無影響;假如要增加磨皮,再來個功能類,繼續裝飾下去,對其他功能也無影響,可以並存運行。

在 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%

總結一下:

  1. 寫了工廠函數,從上到下依次執行,求得裝飾器函數。
  2. 裝飾器函數的執行順序是 從下到上 依次執行。

類裝飾器

作用在類聲明上的裝飾器,可以給我們改變類的機會。在執行裝飾器函數時,會把類構造函數傳遞給裝飾器函數。

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 0void 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。

如果對你有幫助,請關注【前端技能解鎖】

站長推薦

1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲

2.廣告聯盟: 整理了目前主流的廣告聯盟平台,如果你有流量,可以作為參考選擇適合你的平台點擊進入

鏈接: http://www.fly63.com/article/detial/8720