淺談js中的裝飾器
裝飾器模式(Decorator Pattern)
是一種結構型設計模式,旨在促進程式碼複用,可以用於修改現有的系統,希望在系統中為物件新增額外的功能,同時又不需要大量修改原有的程式碼。
JS中的裝飾器是ES7中的一個新語法,可以對類
、方法
、屬性
進行修飾,從而進行一些相關功能定製, 它的寫法與Java的註解(Annotation)
類似,但是功能有比較大的區別。
大家可能聽說過 組合函式 和 高階函式 的概念,也可以這麼理解。
我們先來看一下以下程式碼:
function doSomething(name) { console.log('Hi, I\'' + name); } funtion useLogging(func, name) { console.log('Starting'); func(name); console.log('Finished'); } 複製程式碼
以上邏輯不難理解,給原有的函式加一個打日誌的功能,但是這樣的話,每次都要傳引數給useLogging
,而且破壞了之前的程式碼結構,之前直接doSomething
就好了,現在要改成useLogging(doSomething, 'Jiang')
。
那有沒有更好的方式呢,當然有啦。
簡單裝飾器:
function useLogging(func) { return function() { console.log('Starting'); const result = func.apply(this, arguments) console.log('Done'); return result; } } const wrapped = useLogging(doSomething); 複製程式碼
以上程式碼返回了一個新的函式 wrapped , 呼叫方式和doSomething
相同,在原來的基礎上能做多一點事情。
doSomething('angry'); // Hi, I'angry const wrapped = useLogging(doSomething); wrapped('angry'); // Starting // Hi, I'angry // Done 複製程式碼
怎麼使用裝飾器?
裝飾器主要有兩種用法:
- 裝飾類方法或屬性(類成員)
class MyClass { @readonly method() { } } function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor; } 複製程式碼
- 裝飾類
@annotation class MyClass { } function annotation(target) { target.annotated = true; } 複製程式碼
類成員裝飾器
類成員裝飾器用來裝飾類裡面的屬性、方法、getter
和setter
。這個裝飾器函式呼叫三個引數調:
-
target
: 被裝飾的類的原型 -
name
: 被裝飾的類、屬性、方法的名字 -
descriptor
: 被裝飾的類、屬性、方法的descriptor
,將傳遞給Object.defineProperty
我們來寫幾個裝飾器,程式碼如下:
寫一個@readonly
裝飾器,簡單版實現:
class Example { @log add(a, b) { return a + b; } @unenumerable @readonly name = "alibaba" } function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor; } function unenumerable(target, name, descriptor) { descriptor.enumerable = false; return descriptor; } function log(target, name, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments: ${args}`); try { const result = original.apply(this, args); console.log(`Result: ${result}`); return result; } catch (e) { console.log(`Error: ${e}`); throw e; } } } return descriptor; } const e = new Example(); // Calling add with [2, 4] e.add(2, 4); e.name = 'antd'; // Error 複製程式碼
我們可以通過Babel
檢視編譯後的程式碼,也可以在本地編譯。
npm i @babel/core @babel/cli npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D 複製程式碼
.babelrc
檔案
{ "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], ["@babel/plugin-proposal-class-properties", {"loose": true}] ] } 複製程式碼
編譯 ES6 語法輸出到檔案
因為沒用全域性安裝@babel/cli
, 建議用 npx 命令來執行,或者./node_modules/.bin/babel
,關於npx
命令,可以看下官方文件
npx babel decorator.js --out-file complied.js 複製程式碼
編譯後的程式碼:
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; // 拷貝屬性 Object['ke' + 'ys'](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['define' + 'Property'](target, property, desc); desc = null; } return desc; } _applyDecoratedDescriptor(_class.prototype, "add", [log], Object.getOwnPropertyDescriptor(_class.prototype, "add"), _class.prototype) 複製程式碼
Babel 構建了一個_applyDecoratedDescriptor
函式,用於裝飾類成員
Object.getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor()
方法返回指定物件上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該物件的屬性,不需要從原型鏈上進行查詢的屬性),不是原型鏈上的這點很關鍵。
詳情可以檢視官方文件,這裡就不細說了。
var desc = {}; // 這裡對 descriptor 屬性做了一層拷貝 Object['ke' + 'ys'](descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; // 沒有 value 或者 initializer 屬性的,都是 get 和 set 方法 if ('value' in desc || desc.initializer) { desc.writable = true; } 複製程式碼
這裡的 initializer 是 Babel 為了配合 decorator 而產生的一個屬性,就比方說對於上面程式碼中的 name 屬性,被編譯成:
_descriptor = _applyDecoratedDescriptor(_class.prototype, "name", [unenumerable, readonly], { configurable: true, enumerable: true, writable: true, initializer: function initializer() { return "alibaba"; } }) 複製程式碼
desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); 複製程式碼
處理多個 decorator 的情況,這裡執行了slice()和reverse(),所以我們可以得出,一個類成員有多個裝飾器,會由內向外執行。
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['define' + 'Property'](target, property, desc); desc = null; } return desc; 複製程式碼
最後無論是裝飾方法還是屬性,都會執行:
Object["define" + "Property"](target, property, desc); 複製程式碼
由此可見,裝飾方法本質上還是使用 Object.defineProperty() 來實現的。
類裝飾器
類裝飾器相對簡單
function log(Class) { return (...args) => { console.log(args); return new Class(...args); }; } 複製程式碼
@log class Example { constructor(name, age) { } } const e = new Example('Graham', 34); // [ 'Graham', 34 ] console.log(e); // Example {} 複製程式碼
裝飾器中傳入引數:
function log(name) { return function decorator(Class) { return (...args) => { console.log(`Arguments for ${name}: args`); return new Class(...args); }; } } @log('Demo') class Example { constructor(name, age) {} } const e = new Example('Graham', 34); // Arguments for Demo: args console.log(e); // Example {} 複製程式碼
應用
在 React 中,經常會用到 redux 或者高階元件。
class A extends React.Component {} export default connect()(A); 複製程式碼
裝飾器寫法:
@connect() export default connect()(A); 複製程式碼