装饰器
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在最终结果上和包装对象的一致,而不会因为我对它的重写行为而丢失。