跳到主要内容

this

众所周知,在 JavaScript 中,函数 this 的指向一直是一个令人头痛的问题。因为 this 的值需要等到函数被调用时才能被确定,更别说通过一些方法还可以改变 this 的指向。也就是说 this 的类型不固定,它取决于执行时的上下文。

但是,使用了 TypeScript 后,我们就不用担心这个问题了。通过指定 this 的类型(严格模式下,必须显式指定 this 的类型),当我们错误使用了 this,TypeScript 就会提示我们,如下代码所示:

function say() {
console.log(this.name); // ts(2683) 'this' implicitly has type 'any' because it does not have a type annotation
}
say();

在上述代码中,如果我们直接调用 say 函数,this 应该指向全局 window 或 global(Node 中)。但是,在 strict 模式下的 TypeScript 中,它会提示 this 的类型是 any,此时就需要我们手动显式指定类型了。

那么,在 TypeScript 中,我们应该如何声明 this 的类型呢?

在 TypeScript 中,我们只需要在函数的第一个参数中声明 this 指代的对象(即函数被调用的方式)即可,比如最简单的作为对象的方法的 this 指向,如下代码所示:

function say(this: Window, name: string) {
console.log(this.name);
}
window.say = say;
window.say("hi");
const obj = {
say,
};
obj.say("hi"); // ts(2684) The 'this' context of type '{ say: (this: Window, name: string) => void; }' is not assignable to method's 'this' of type 'Window'.

在上述代码中,我们在 window 对象上增加 say 的属性为函数 say。那么调用 window.say()时,this 指向即为 window 对象。

调用 obj.say()后,此时 TypeScript 检测到 this 的指向不是 window,于是抛出了如下所示的一个 ts(2684) 错误。

say("captain"); // ts(2684) The 'this' context of type 'void' is not assignable to method's 'this' of type 'Window'

需要注意的是,如果我们直接调用 say(),this 实际上应该指向全局变量 window,但是因为 TypeScript 无法确定 say 函数被谁调用,所以将 this 的指向默认为 void,也就提示了一个 ts(2684) 错误。

此时,我们可以通过调用 window.say() 来避免这个错误,这也是一个安全的设计。因为在 JavaScript 的严格模式下,全局作用域函数中 this 的指向是 undefined。

同样,定义对象的函数属性时,只要实际调用中 this 的指向与指定的 this 指向不同,TypeScript 就能发现 this 指向的错误,示例代码如下:

interface Person {
name: string;
say(this: Person): void;
}
const person: Person = {
name: "captain",
say() {
console.log(this.name);
},
};
const fn = person.say;
fn(); // ts(2684) The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'

注意:显式注解函数中的 this 类型,它表面上占据了第一个形参的位置,但并不意味着函数真的多了一个参数,因为 TypeScript 转译为 JavaScript 后,“伪形参” this 会被抹掉,这算是 TypeScript 为数不多的特有语法。

前边的 say 函数转译为 JavaScript 后,this 就会被抹掉,如下代码所示:

function say(name) {
console.log(this.name);
}

同样,我们也可以显式限定类函数属性中的 this 类型,TypeScript 也能检查出错误的使用方式,如下代码所示:

class Component {
onClick(this: Component) {}
}
const component = new Component();
interface UI {
addClickListener(onClick: (this: void) => void): void;
}
const ui: UI = {
addClickListener() {},
};
ui.addClickListener(new Component().onClick); // ts(2345)

上面示例中,我们定义的 Component 类的 onClick 函数属性(方法)显式指定了 this 类型是 Component,在第 11 行作为入参传递给 ui 的 addClickListener 方法中,它指定的 this 类型是 void,两个 this 类型不匹配,所以抛出了一个 ts(2345) 错误。

此外,在链式调用风格的库中,使用 this 也可以很方便地表达出其类型,如下代码所示:

class Container {
private val: number;
constructor(val: number) {
this.val = val;
}
map(cb: (x: number) => number): this {
this.val = cb(this.val);
return this;
}
log(): this {
console.log(this.val);
return this;
}
}
const instance = new Container(1)
.map((x) => x + 1)
.log() // => 2
.map((x) => x * 3)
.log(); // => 6

因为 Container 类中 map、log 等函数属性(方法)未显式指定 this 类型,默认类型是 Container,所以以上方法在被调用时返回的类型也是 Container,this 指向一直是类的实例,它可以一直无限地被链式调用。