一個以@開頭的描述性詞語,英語的decorator動詞是decorate,裝飾的意思,其中詞根dek(dec發音)原始印歐語系中意思是“接受”,即,原來的某個事物接受一些新東西(而變得更好),從另外一個角度描述,裝飾器主要是在被裝飾物件的外部起作用,而非入侵其內部發生什么改變,裝飾器模式同時也是一種開發模式,其地位雖然弱于MVC、IoC等,但不失為一種優秀的模式,
JavaScript的裝飾器可能是借鑒自Python也或許是Java,較為明顯的不同的是大部分語言的裝飾器必須是一行行分開,而JS的裝飾器可以在一行中,
裝飾器存在的意義
會偷懶的程式員,才是優秀的程式員,
舉個例子:我拿著員工卡進入公司總部大樓,因為每個員工所屬的部門、級別不同,并不能進入大樓的任何房間,每個房間都有一扇門;那么,公司需要安排每個辦公室里至少一個人關于驗證來訪者的作業:
- 先登記來訪者
- 驗證是否有權限進入,如果沒有則要求其離開
- 記錄其離開時間
還有一個選擇方式,就是安裝電子門鎖,門鎖只是將員工卡的資訊傳輸給機房,由特定的程式驗證,
前者暫且稱之為笨模式,代碼如下:
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 init
npm 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
@classDecorator
class 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 = https://www.cnblogs.com/anuoxiang/archive/2021/01/08/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 = https://www.cnblogs.com/anuoxiang/archive/2021/01/08/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");
}
- class decorator
- parameter decorator
- property decoator
- method decorator
- accessor decorator
還可以通過閱讀轉換后的源代碼得到執行順序:
- 類裝飾器(在最外層)
- 引數裝飾器(在生成建構式最里層)
- 按照出現的先后順序的:屬性、方法和存取器
總結
裝飾器是一種優雅的開發模式,極大的方便了開發者編碼程序,同時提升了代碼的可讀性,我們在使用裝飾器開發時,還是非常有必要了解其運行機理,
本文由博客群發一文多發等運營工具平臺 OpenWrite 發布
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/246487.html
標籤:其他
上一篇:z-index的特點
下一篇:JavaScript => ?
