裝飾器
ts這個裝飾器從本質上來說仍然是一個高階函式,和py的裝飾器在使用場景上是差不多的:當我需要給某個物件進行一個「包裝」,但又不想整個重寫這個原有的方法時,那我就要用裝飾器。比如給已有的方法加log,這在py裡面也是典型的@場景。
function logger(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value // 拿到原方法
descriptor.value = function(...args: any[]) {
console.log(`调用 ${key}`)
return original.apply(this, args)
}
// 不需要 return,直接改 descriptor.value
}
class Greeter {
@logger
greet(name: string) {
console.log(`hello ${name}`)
}
}
這裡的 target, key, descriptor 是ts的特色,或者說js的特色,本質上就是 prototype 機制需要的內容。target 直接指向這個方法或者類的原型鏈;而 key 比較簡單,就是這個方法大的名字;descriptor 是這個方法的「設定」,而我們一般只關心 descriptor.value,也就是這個方法本身:在這裡等價於py的func。
這也是ts麻煩的點。在py我可以直接return新的函式,裡面的內容就是單純func(),然後在它前面加我需要的邏輯包裝就行了。但是在ts這裡,我就得用一個很麻煩的機制重寫這個方法。
function logger(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value // 保存原方法
descriptor.value = function(...args: any[]) {
console.log(`调用 ${key},参数:`, args)
const result = original.apply(this, args) // 调用原方法
console.log(`${key} 返回:`, result)
return result
}
// 不需要 return,直接改了 descriptor 就生效
}
從上面的整個邏輯就能看出來,想理解ts的@,關鍵就是理解js的原型鏈機制,一切方法的都是掛在原型鏈上的,你需要保證這個方法的正常性,就得把它的整個「全套」原封不動的帶回去。比如 this和function,不用箭頭函式的原因就是這裡會丟this,如果丟了,那我原先方法裡面的this操作就全沒——其實這裡就算不寫裝飾器也是一樣的。
this本身就是指向自己的上級例項,也就是py裡的self,我在類裡面呼叫屬性就要 self.xxx ,js這裡就是 this.xxx,所有方法都是掛在自己的上級原型鏈之下的,而上級原型鏈的屬性也是一樣。this.xxx 指的就是上級例項下的xxx,js預設會順著原型鏈查詢。而箭頭函式會丟失這個this上下文,它的this會指向定義時的外層物件,而不是當前呼叫物件。比如在這種包裝場景下,我要是用箭頭函式,指向的就是 logger,如果包裝物件是某個類下的方法,那他就會完全吃不到自己所屬類的屬性,因為這裡的this根本就沒指向它。所以這裡才得選擇 function,this直接指向呼叫方(被包裝物件)的上下文,然後在裡面傳的this才是指向正確的呼叫物件。
original.apply(this, args) 這一步在做的就是this上下文傳遞,保證this在最終結果上和包裝物件的一致,而不會因為我對它的重寫行為而丟失。