一個以@開頭的描述性詞語。英語的 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中,裝飾器的類型有:
由于目前裝飾器概念還處于提案階段,不是一個正式可用的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 創建一個 { "presets": ["@babel/preset-env"], "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", { "loose": true }], "babel-plugin-parameter-decorator" ] } 利用下面的轉換命令,我們可以得到ES5的轉換程序:
類裝飾器創建一個使用裝飾器的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========="); } 然后轉換代碼,可以發現,這次代碼量突然增大了很多。排除掉 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個參數, 存取器(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========="); } 轉換后的代碼,還是與上述屬性、存取器的代碼非常接近。但除了 參數裝飾器參數裝飾器的使用位置較之前集中裝飾器略有不同,它被使用在行內。 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; }(); 裝飾器應用使用參數——閉包以上所有的案例,裝飾器本身均沒有使用任何參數。然實際應用中,經常會需要有特定的參數需求。我們再回到一開頭的例子中 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"); }
還可以通過閱讀轉換后的源代碼得到執行順序:
總結
|
|