詳解及案例介紹Javascript裝飾器原理

2022-02-21 19:01:04
本篇文章給大家帶來了關於裝飾器原理的相關知識,JavaScript的裝飾器可能是借鑑自Python也或許是Java,較為明顯的不同的是大部分語言的裝飾器必須是一行行分開,而js的裝飾器可以在一行中,希望對大家有幫助。

一個以@開頭的描述性詞語。英語的decorator動詞是decorate,裝飾的意思。其中詞根dek(dec發音)原始印歐語系中意思是「接受」。即,原來的某個事物接受一些新東西(而變得更好)。

從另外一個角度描述,裝飾器主要是在被裝飾物件的外部起作用,而非入侵其內部發生什麼改變。裝飾器模式同時也是一種開發模式,其地位雖然弱於MVC、IoC等,但不失為一種優秀的模式。

JavaScript的裝飾器可能是借鑑自Python也或許是Java。較為明顯的不同的是大部分語言的裝飾器必須是一行行分開,而js的裝飾器可以在一行中。

裝飾器存在的意義

舉個例子:我拿著員工卡進入公司總部大樓。因為每個員工所屬的部門、級別不同,並不能進入大樓的任何房間。每個房間都有一扇門;那麼,公司需要安排每個辦公室裡至少一個人關於驗證來訪者的工作:

  1. 先登記來訪者

  2. 驗證是否有許可權進入,如果沒有則要求其離開

  3. 記錄其離開時間

還有一個選擇方式,就是安裝電子門鎖,門鎖只是將員工卡的資訊傳輸給機房,由特定的程式驗證。

前者暫且稱之為笨模式,程式碼如下:

function A101(who){  
record(who,new Date(),'enter');  
if (!permission(who)) {    
record(who,new Date(),'no permission')    
return void;  
}  // 繼續執行  
doSomeWork();  
record(who,new Date(),'leave')}
function A102(who){record(who,new Date(),'enter');  
if (!permission(who)) {    
record(who,new Date(),'no permission')    
return void;  }  
// 繼續執行  
doSomeWork();  
record(who,new Date(),'leave')}
// ...

有經驗的大家肯定第一時間想到了,把那些重複語句封裝為一個方法,並統一呼叫。是的,這樣可以解決大部分問題,但是還不夠「優雅」。同時還有另外一個問題,如果「房間」特別多,又或者只有大樓奇數號房間要驗證偶數不驗證,那豈不是很「變態」?如果使用裝飾器模式來做,程式碼會如下面這樣的:

@verify(who)class Building {  
@verify(who)  A101(){/*...*/}  
@verify(who)  A102(){/*...*/}  
//...}

verify是驗證的裝飾器,而其本質就是一組函數。

JavaScript裝飾器

正如先前的那個例子,裝飾器其實本身就是一個函數,它在執行被裝飾的物件之前先被執行。

在JavaScript中,裝飾器的型別有:

  • 存取方法(屬性的get和set)

  • 欄位

  • 方法

  • 引數

由於目前裝飾器概念還處於提案階段,不是一個正式可用的js功能,所以想要使用這個功能,不得不借助翻譯器工具,例如Babel工具或者TypeScript編譯JS程式碼轉後才能被執行。我們需要先搭建執行環境,設定一些引數。(以下過程,假設已經正確安裝了NodeJS開發環境以及包管理工具)

cd project && npm initnpm i -D @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env babel-plugin-parameter-decorator

建立一個.babelrc組態檔,如下:

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

利用下面的轉換命令,我們可以得到ES5的轉換程式:

npx babel source.js --out-file target.js

類裝飾器

建立一個使用裝飾器的JS程式decorate-class.js

@classDecoratorclass Building {  
constructor() {    
this.name = "company";  
}}
const building = new Building();
function classDecorator(target) {  
console.log("target", target);}

以上是最最簡單的裝飾器程式,我們利用babel將其「翻譯」為ES5的程式,然後再美化一下後得到如下程式。

"use strict";
var _class;
function _classCallCheck(instance, Constructor) {  
if (!(instance instanceof Constructor)) {    
throw new TypeError("Cannot call a class as a function");  
}}
var Building =  classDecorator(    
(_class = function Building() {      
_classCallCheck(this, Building);
      this.name = "company";    
      })  ) || _class;
var building = new Building();
function classDecorator(target) {  
console.log("target", target);}

第12行就是在類生成過程中,呼叫函數形態的裝飾器,並將建構函式(類本身)送入其中。同樣揭示了裝飾器的第一個引數是類別建構函式的由來。

方法 (method)裝飾器

稍微修改一下程式碼,依舊是儘量保持最簡單:

class Building {  constructor() {    
this.name = "company";  
}  
@methodDecorator  
openDoor() {    
console.log("The door being open");  
}}
const building = new Building();
function methodDecorator(target, property, descriptor) {  
console.log("target", target);  
if (property) {    
console.log("property", property);  
}  
if (descriptor) {    
console.log("descriptor", descriptor);  
}  
console.log("=====end of decorator=========");
}

然後轉換程式碼,可以發現,這次程式碼量突然增大了很多。排除掉_classCallCheck、_defineProperties和_createClass三個函數,關注_applyDecoratedDescriptor函數:

function _applyDecoratedDescriptor(  target,  property,  decorators,  descriptor,  context) {  
var desc = {};  
Object.keys(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.defineProperty(target, property, desc);    
desc = null;  
}  
return desc;}

它在生成建構函式之後,執行了這個函數,特別注意,這個裝飾器函數是以陣列形式的引數傳遞的。然後到上述程式碼的17~22行,將裝飾器逐個應用,其中對裝飾器的呼叫就在第21行。

它傳送了3個引數,target指類本身。property指方法名(或者屬性名),desc是可能被先前裝飾器被處理過的descriptor,如果是第一次迴圈或只有一個裝飾器,那麼就是方法或屬性本身的descriptor。

存取器(accessor)裝飾

JS關於類的定義中,支援get和set關鍵字針對設定某個欄位的讀寫操作邏輯,裝飾器也同樣支援這類方法的操作。

class Building {  
constructor() {    
this.name = "company";  
}  
@propertyDecorator  get roomNumber() {    
return this._roomNumber;  
}
  _roomNumber = "";  
  openDoor() {    
  console.log("The door being open");  
  }}

有心的讀者可能已經發現了,存取器裝飾的程式碼與上面的方法裝飾程式碼非常接近。關於屬性 get和set方法,其本身也是一種方法的特殊形態。所以他們之間的程式碼就非常接近了。

屬性裝飾器

繼續修改原始碼:

class Building {  
constructor() {    
this.name = "company";  
}  
@propertyDecorator  roomNumber = "";
}
const building = new Building();
function propertyDecorator(target, property, descriptor) {  
console.log("target", target);  
if (property) {    
console.log("property", property);  
}  
if (descriptor) {    
console.log("descriptor", descriptor);  
}  
console.log("=====end of decorator=========");
}

轉換後的程式碼,還是與上述屬性、存取器的程式碼非常接近。但除了_applyDecoratedDescriptor外,還多了一個_initializerDefineProperty函數。這個函數在生成建構函式時,將宣告的各種欄位繫結給物件。

引數裝飾器

引數裝飾器的使用位置較之前集中裝飾器略有不同,它被使用在行內。

class Building {  
constructor() {    
this.name = "company";  
}  
openDoor(@parameterDecorator num, @parameterDecorator zoz) {    
console.log(`${num} door being open`);  
}}
const building = new Building();
function parameterDecorator(target, property, key) {  
console.log("target", target);  
if (property) {    
console.log("property", property);  
}  
if (key) {    
console.log("key", key);  
}  
console.log("=====end of decorator=========");
}

轉換後的程式碼區別就比較明顯了,babel並沒有對其生成一個特定的函數對其進行特有的操作,而只在建立完類(建構函式)以及相關屬性、方法後直接呼叫了開發者自己編寫的裝飾器函數:

var Building = /*#__PURE__*/function () {  
function Building() {    
_classCallCheck(this, Building);
    this.name = "company";  
    }
  _createClass(Building, [{    key: "openDoor",    value: function openDoor(num, zoz) {      console.log("".concat(num, " door being open"));    }  }]);
  parameterDecorator(Building.prototype, "openDoor", 1);  parameterDecorator(Building.prototype, "openDoor", 0);  return Building;}();

裝飾器應用

使用引數——閉包

以上所有的案例,裝飾器本身均沒有使用任何引數。然實際應用中,經常會需要有特定的引數需求。我們再回到一開頭的例子中verify(who),其中需要傳入一個身份變數。哪又怎麼做?我們少許改變一下類裝飾器的程式碼:

const who = "Django";@classDecorator(who)class Building {
  constructor() { 
     this.name = "company"; 
      }}

轉換後得到

// ...var who = "Django";var Building =  ((_dec = classDecorator(who)),  _dec(    
(_class = function Building() {      
_classCallCheck(this, Building);
      this.name = "company";    
      })  
      ) || _class);
      // ...

請注意第4第5行,它先執行了裝飾器,然後再用返回值將類(建構函式)送入。相對應的,我們就應該將建構函式寫成下面這樣:

function classDecorator(people) {  
console.log(`hi~ ${people}`);  
return function (target) {    
console.log("target", target);  
};
}

同樣的,方法、存取器、屬性和引數裝飾器均是如此。

裝飾器包裹方法

到此,我們已經可以將裝飾器引數與目標物件結合起來,進行一些邏輯類的操作。那麼再回到文章的開頭的例子中:需求中要先驗證來訪者許可權,然後記錄,最後在來訪者離開時再做一次記錄。此時需要監管物件方法被呼叫的整個過程。

請大家留意那個方法裝飾器的descriptor,我們可以利用這個物件來「重寫」這個方法。

class Building {  
constructor() {    
this.name = "company";  
}
  @methodDecorator("Gate")  
  openDoor(firstName, lastName) {    
  return `The door will be open, when ${firstName} ${lastName} is walking into the ${this.name}.`;  
  }}
let building = new Building();console.log(building.openDoor("django", "xiang"));
function methodDecorator(door) {  
return function (target, property, descriptor) {    
let fn = descriptor.value;    
descriptor.value = function (...args) {      
let [firstName, lastName] = args;      
console.log(`log: ${firstName}, who are comming.`);      
// verify(firstName,lastName)      
let result = Reflect.apply(fn, this, [firstName, lastName]);      
console.log(`log: ${result}`);  
    console.log(`log: ${firstName}, who are leaving.`);      
    return result;    
    };    
    return descriptor;  
    };}

程式碼第17行,將原方法暫存;18行定義一個新的方法,20~25行,記錄、驗證和記錄離開的動作。

log: Django, who are comming.log: The door will be open, when Django Xiang is walking in to the company.log: Django, who are leaving.The door will be open, when Django Xiang is walking in to the company

裝飾順序

通過閱讀轉換後的程式碼,我們知道裝飾器工作的時刻是在類被範例化之前,在生成之中完成裝飾函數的動作。那麼,如果不同型別的多個裝飾器同時作用,其過程是怎樣的?我們將先前的案例全部整合到一起看看:

const who = "Django";@classDecorator(who)class Building {  
constructor() {    
this.name = "company";  
}
  @propertyDecorator  roomNumber = "";
  @methodDecorator  openDoor(@parameterDecorator num) {    
  console.log(`${num} door being open`);  
  }
  @accessorDecorator  get roomNumber() {    
  return this._roomNumber;  
  }}
const building = new Building();
function classDecorator(people) {  
console.log(`class decorator`);  
return function (target) {    
console.log("target", target);  
};}
function methodDecorator(target, property, descriptor) {  
console.log("method decorator");}
function accessorDecorator(target, property, descriptor) {  
console.log("accessor decorator");}
function propertyDecorator(target, property, descriptor) {  
console.log("property decoator");}
function parameterDecorator(target, property, key) {  
console.log("parameter decorator");}
  1. class decorator

  2. parameter decorator

  3. property decoator

  4. method decorator

  5. accessor decorator

還可以通過閱讀轉換後的原始碼得到執行順序:

  1. 類裝飾器(在最外層)

  2. 引數裝飾器(在生成建構函式最裡層)

  3. 按照出現的先後順序的:屬性、方法和存取器

總結

裝飾器是一種優雅的開發模式,極大的方便了開發者編碼過程,同時提升了程式碼的可讀性。我們在使用裝飾器開發時,還是非常有必要了解其執行機理。

相關推薦:

以上就是詳解及案例介紹Javascript裝飾器原理的詳細內容,更多請關注TW511.COM其它相關文章!