Sumirer

TypeScript中方法装饰器的this指向问题

Word count: 647Reading time: 2 min
2024/10/12

起因

在ts中,装饰器是一种对class中属性处理的方式,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function format() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const targetValue = descriptor.value;
descriptor.value = (...args: any[]) => {
console.log('format', ...args)
targetValue.call(...args);
};
};
}

class Foo {

@format()
public log(log: string){
console.log('foo log', log)
}
}

装饰器

这种做法可以处理成员函数的一些执行处理,上述做法可以在执行 Foo.log 时获取到输入的参数,并对参数进行处理,可以更好地处理函数中的一些全局操作,例如日志记录之类

但是在前端编写代码时,非常容易碰到一些this指向问题,因为在js中,function需要指定this或者例如箭头函数,没有this,但是在一些情况下,又必需使用箭头函数来进行获取所在函数的上下文this

这样就会存在问题,就是在使用装饰器对class中的箭头函数进行处理时,无法达到理想效果,但是改成普通函数时又会获取不到this

因为在class中的箭头函数是被当初属性申明的,装饰器无法获取 PropertyDescriptor.value,所以不能当作普通函数进行处理,例如以下写法,会在 output中无法获取this的指向导致报错,但是使用箭头函数的写法
。例如outputArrow函数,又无法让format生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function format() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const targetValue = descriptor.value;
descriptor.value = (...args: any[]) => {
console.log('format', ...args)
targetValue.call(...args);
};
};
}

class Foo {

public log(log: string){
console.log('log', log)
}

@format()
public output(data: string){
this.log(data);
}

@format()
public outputArrow = (data: string) => {
this.log(data)
}
}

const foo = new Foo();
// error, this is undefined
foo.output('log');

// format not working
foo.outputArrow('log');

解决方法

PropertyDescriptor中,我们可以改写PropertyDescriptor.get属性来进行属性劫持,因为在获取get的值的时候,当前函数的this即为目标对象本身,所以获取的this一定是对的

改写所有需要装饰的方法为普通函数,不使用箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function format() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const targetValue = descriptor.value;
function createFn(callTarget: any,...args: any[]) {
console.log('format', ...args);
callTarget.call(...args);
};

return {
configurable: true,
get() {
const bindTarget = targetValue.bind(this);
return (...args: any[]) => createFn.bind(this)(bindTarget, ...args);
},
}
};
}

class Foo {

public log(log: string){
console.log('log', log)
}

@format()
public output(data: string){
this.log(data);
}

@format()
public outputArrow(data: string) {
this.log(data)
}
}

const foo = new Foo();
// success
foo.output('log');

// success
foo.outputArrow('log');
CATALOG
  1. 1. 起因
  2. 2. 解决方法