隨著 ES6 和 TypeScript 中類的引入,在某些場景需要在不改變原有類和類屬性的基礎上擴充套件些功能,這也是裝飾器出現的原因。
裝飾器簡介
作為一種可以動態增刪功能模組的模式(比如 redux 的中介軟體機制),裝飾器同樣具有很強的動態靈活性,只需在類或類屬性之前加上 @方法名
就完成了相應的類或類方法功能的變化。
不過裝飾器模式仍處於第 2 階段提案中,使用它之前需要使用 babel 模組 transform-decorators-legacy
編譯成 ES5 或 ES6。
在 TypeScript 的 lib.es5.d.ts 中,定義了 4 種不同裝飾器的介面,其中裝飾類以及裝飾類方法的介面定義如下所示:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
複製程式碼
下面對這兩種情況進行解析。
作用於類的裝飾器
當裝飾的物件是類時,我們操作的就是這個類本身
。
@log
class MyClass { }
function log(target) { // 這個 target 在這裡就是 MyClass 這個類
target.prototype.logger = () => `${target.name} 被呼叫`
}
const test = new MyClass()
test.logger() // MyClass 被呼叫
複製程式碼
由於裝飾器是表示式,我們也可以在裝飾器後面再新增提個引數:
@log('hi')
class MyClass { }
function log(text) {
return function(target) {
target.prototype.logger = () => `${text},${target.name} 被呼叫`
}
}
const test = new MyClass()
test.logger() // hello,MyClass 被呼叫
複製程式碼
在使用 redux 中,我們最常使用 react-redux 的寫法如下:
@connect(mapStateToProps, mapDispatchToProps)
export default class MyComponent extends React.Component {}
複製程式碼
經過上述分析,我們知道了上述寫法等價於下面這種寫法:
class MyComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
複製程式碼
作用於類方法的裝飾器
與裝飾類不同,對類方法的裝飾本質是操作其描述符。可以把此時的裝飾器理解成是 Object.defineProperty(obj, prop, descriptor)
的語法糖,看如下程式碼:
class C {
@readonly(false)
method() { console.log('cat') }
}
function readonly(value) {
return function (target, key, descriptor) { // 此處 target 為 C.prototype; key 為 method;
// 原 descriptor 為:{ value: f, enumarable: false, writable: true, configurable: true }
descriptor.writable = value
return descriptor
}
}
const c = new C()
c.method = () => console.log('dog')
c.method() // cat
複製程式碼
可以看到裝飾器函式接收的三個引數與 Object.defineProperty 是完全一樣的,具體實現可以看 babel 轉化後的程式碼,主要實現如下所示:
var C = (function() {
class C {
method() { console.log('cat') }
}
var temp
temp = readonly(false)(C.prototype, 'method',
temp = Object.getOwnPropertyDescriptor(C.prototype, 'method')) || temp // 通過 Object.getOwnPropertyDescriptor 獲取到描述符傳入到裝飾器函式中
if (temp) Object.defineProperty(C.prototype, 'method', temp)
return C
})()
複製程式碼
再將再來看看如果有多個裝飾器作用於同一個方法上呢?
class C {
@readonly(false)
@log
method() { }
}
複製程式碼
經 babel 轉化後的程式碼如下:
desc = [readonly(false), log]
.slice()
.reverse()
.reduce(function(desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
複製程式碼
可以清晰地看出,經過 reverse 倒序後,裝飾器方法會至裡向外執行。