this
能够说是Javascript
里最难理解的特性之一了,Typescript
里的 this 彷佛更加复杂了,Typescript
里的 this 有三中场景,不一样的场景都有不一样意思。javascript
因为 javascript 支持灵活的函数调用方式,不一样的调用场景,this 的指向也有所不一样vue
这也是绝大部分 this 的使用场景,当函数做为对象的 方法调用时,this 指向该对象java
const obj = {
name: "yj",
getName() {
return this.name // 能够自动推导为{ name:string, getName():string}类型
},
}
obj.getName() // string类型
复制代码
这里有个坑就是若是对象定义时对象方法是使用箭头函数进行定义,则 this 指向的并非对象而是全局的 window,Typescript 也自动的帮我推导为 windowgit
const obj2 = {
name: "yj",
getName: () => {
return this.name // check 报错,这里的this指向的是window
},
}
obj2.getName() // 运行时报错
复制代码
即便是经过非箭头函数定义的函数,当将其赋值给变量,并直接经过变量调用时,其运行时 this 执行的并不是对象自己github
const obj = {
name: "yj",
getName() {
return this.name
},
}
const fn1 = obj.getName
fn1() // this指向的是window,运行时报错
复制代码
很不幸,上述代码在编译期间并未检查出来,咱们能够经过为getName
添加this
的类型标注解决该问题typescript
interface Obj {
name: string
// 限定getName调用时的this类型
getName(this: Obj): string
}
const obj: Obj = {
name: "yj",
getName() {
return this.name
},
}
obj.getName() // check ok
const fn1 = obj.getName
fn1() // check error
复制代码
这样咱们就能报保证调用时的 this 的类型安全api
在 class 出现以前,一直是把 function 当作构造函数使用,当经过 new 调用 function 时,构造器里的 this 就指向返回对象安全
function People(name: string) {
this.name = name // check error
}
People.prototype.getName = function() {
return this.name
}
const people = new People() // check error
复制代码
很不幸,Typescript 暂时对 ES5 的 constructor function 的类型推断暂时并未支持(github.com/microsoft/T… this 的类型和 people 能够做为构造函数调用,所以须要显示的进行类型标注markdown
interface People {
name: string
getName(): string
}
interface PeopleConstructor {
new (name: string): People // 声明能够做为构造函数调用
prototype: People // 声明prototype,支持后续修改prototype
}
const ctor = (function(this: People, name: string) {
this.name = name
} as unknown) as PeopleConstructor // 类型不兼容,二次转型
ctor.prototype.getName = function() {
return this.name
}
const people = new ctor("yj")
console.log("people:", people)
console.log(people.getName())
复制代码
固然最简洁的方式,仍是使用 classapp
class People {
name: string
constructor(name: string) {
this.name = name // check ok
}
getName() {
return this.name
}
}
const people = new People("yj") // check ok
复制代码
这里还有一个坑,即在 class 里 public field method 和 method 有这本质的区别 考虑以下三种 method
class Test {
name = 1
method1() {
return this.name
}
method2 = function() {
return this.name // check error
}
method3 = () => {
return this.name
}
}
const test = new Test()
console.log(test.method1()) // 1
console.log(test.method2()) // 1
console.log(test.method3()) // 1
复制代码
虽然上述三个代码都能成功的输出 1,可是有这本质的区别
在咱们编写 React 应用时,大量的使用了 method3 这种自动绑定 this 的方式, 但实际上这种作法存在较大的问题
class Parent {
constructor() {
this.setup()
}
setup = () => {
console.log("parent")
}
}
class Child extends Parent {
constructor() {
super()
}
setup = () => {
console.log("child")
}
}
const child = new Child() // parent
class Parent2 {
constructor() {
this.setup()
}
setup() {
console.log("parent")
}
}
class Child2 extends Parent2 {
constructor() {
super()
}
setup() {
console.log("child")
}
}
const child2 = new Child2() // child
复制代码
在处理继承的时候,若是 superclass 调用了示例方法而非原型方法,那么是没法在 subclass 里进行 override 的,这与其余语言处理继承的 override 的行为向左,很容出问题。 所以更加合理的方式应该是不要使用实例方法,可是如何处理 this 的绑定问题呢。 目前较为合理的方式要么手动 bind,或者使用 decorator 来作 bind
import autobind from "autobind-decorator"
class Test {
name = 1
@autobind
method1() {
return this.name
}
}
复制代码
call 和 apply 调用没有什么本质区别,主要区别就是 arguments 的传递方式,不分别讨论。和普通的函数调用相比,call 调用能够动态的改变传入的 this, 幸运的是 Typescript 借助 this 参数也支持对 call 调用的类型检查
interface People {
name: string
}
const obj1 = {
name: "yj",
getName(this: People) {
return this.name
},
}
const obj2 = {
name: "zrj",
}
const obj3 = {
name2: "zrj",
}
obj1.getName.call(obj2)
obj1.getName.call(obj3) // check error
复制代码
另外 call 的实现也很是有意思,能够简单研究下其实现,咱们的实现就叫作 call2 首先须要肯定 call 里 第一个参数的类型,很明显 第一个参数 的类型对应的是函数里的 this 参数的类型,咱们能够经过 ThisParameterType 工具来获取一个函数的 this 参数类型
interface People {
name: string
}
function ctor(this: People) {}
type ThisArg = ThisParameterType<typeof ctor> // 为People类型
复制代码
ThisParameterType 的实现也很简单,借助 infer type 便可
type ThisParameterType<T> = T extends (this: unknown, ...args: any[]) => any
T extends (this: infer U, ...args: any[]) => any
? U
: unknown
复制代码
可是咱们怎么获取当前函数的类型呢,经过泛型实例化和泛型约束
interface CallableFunction {
call2<T>(this: (this: T) => any, thisArg: T): any
}
interface People {
name: string
}
function ctor(this: People) {}
ctor.call2() //
复制代码
在进行 ctor.call 调用时,根据 CallableFunction 的定义其 this 参数类型为(this:T) => any,而此时的 this 即为 ctor,而根据 ctro 的类型定义,其类型为(this:People) => any,实例化便可得此时的 T 实例化类型为 People,即 thisArg 的类型为 People
进一步的添加返回值和其他参数类型
interface CallableFunction {
call<T, A extends any[], R>(
this: (this: T, ...args: A) => R,
thisArg: T,
...args: A
): R
}
复制代码
为了支持fluent interface,须要支持方法的返回类型由调用示例肯定,这实际上须要类型系统的额外支持。考虑以下代码
class A {
A1() {
return this
}
A2() {
return this
}
}
class B extends A {
B1() {
return this
}
B2() {
return this
}
}
const b = new B()
const a = new A()
b.A1().B1() // 不报错
a.A1().B1() // 报错
type M1 = ReturnType<typeof b.A1> // B
type M2 = ReturnType<typeof a.A1> // A
复制代码
仔细观察上述代码发现,在不一样的状况下,A1 的返回类型其实是和调用对象有关的而非固定,只有这样才能支持以下的链式调用,保证每一步调用都是类型安全
b.A1()
.B1()
.A2()
.B2() // check ok
复制代码
this 的处理还有其特殊之处,大部分语言对 this 的处理,都是将其做为隐式的参数处理,可是对于函数来说其参数应该是逆变的,可是 this 的处理其实是当作协变处理的。考虑以下代码
class Parent {
name: string
}
class Child extends Parent {
age: number
}
class A {
A1() {
return this.A2(new Parent())
}
A2(arg: Parent) {}
A3(arg: string) {}
}
class B extends A {
A1() {
// 不报错,this特殊处理,视为协变
return this.A2(new Parent())
}
A2(arg: Child) {} // flow下报错,typescript没报错
A3(arg: number) {} // flow和typescript下均报错
}
复制代码
这里还要提的一点是 Typescript 处于兼容考虑,对方法进行了双变处理,可是函数仍是采用了逆变,相比之下 flow 则安全了许多,方法也采用了逆变处理
Vue2.x 最使人诟病的一点就是对 Typescript 的羸弱支持,其根源也在于 vue2.x 的 api 大量使用了 this,形成其类型难以推断,Vue2.5 经过 ThisType 对 vue 的 typescript 支持进行了一波加强,但仍是有不足之处,Vue3 的一个大的卖点也是改进了加强了对 Typescript 的支持。下面咱们就研究下下 ThisType 和 vue 中是如何利用 ThisType 改进 Typescript 的支持的。
先简单说一下 This 的决断规则,推测对象方法的 this 类型规则以下,优先级由低到高
// containing object literal type
let foo = {
x: "hello",
f(n: number) {
this //this: {x: string;f(n: number):void }
},
}
复制代码
type Point = {
x: number
y: number
moveBy(dx: number, dy: number): void
}
let p: Point = {
x: 10,
y: 20,
moveBy(dx, dy) {
this // Point
},
}
复制代码
let bar = {
x: "hello",
f(this: { message: string }) {
this // { message: string }
},
}
复制代码
type Point = {
x: number
y: number
moveBy(dx: number, dy: number): void
}
let p: Point = {
x: 10,
y: 20,
moveBy(this: { message: string }, dx, dy) {
this // {message:string} ,方法类型标注优先级高于对象类型标注
},
}
复制代码
type Point = {
x: number
y: number
moveBy: (dx: number, dy: number) => void
} & ThisType<{ message: string }>
let p: Point = {
x: 10,
y: 20,
moveBy(dx, dy) {
this // {message:string}
},
}
复制代码
type Point = {
x: number
y: number
moveBy(this: { message: string }, dx: number, dy: number): void
}
let p: Point = {
x: 10,
y: 20,
moveBy(dx, dy) {
this // { message:string}
},
}
复制代码
将规则按从高到低排列以下
这里的一条重要规则就是在没有其余类型标注的状况下,若是对象标注的类型里若是包含了 ThisType,那么 this 类型为 T,这意味着咱们能够经过类型计算为咱们的对象字面量添加字面量里没存在的属性,这对于 Vue 极其重要。 咱们来看一下 Vue 的 api
import Vue from 'vue';
export const Component = Vue.extend({
data(){
return {
msg: 'hello'
}
}
methods:{
greet(){
return this.msg + 'world';
}
}
})
复制代码
这里的一个主要问题是 greet 是 methods 的方法,其 this 默认是 methods 这个对象字面量的类型,所以没法从中区获取 data 的类型,因此主要难题是如何在 methods.greet 里类型安全的访问到 data 里的 msg。 借助于泛型推导和 ThisType 能够很轻松的实现,下面让咱们本身实现一些这个 api
type ObjectDescriptor<D, M> = {
data: () => D
methods: M & ThisType<D & M>
}
declare function extend<D, M>(obj: ObjectDescriptor<D, M>): D & M const x = extend({ data() { return { msg: "hello", } }, methods: { greet() { return this.msg + "world" // check }, }, }) 复制代码
其推导规则以下 首先根据对象字面量的类型和泛型约束对比,可获得类型参数 T 和 M 的实例化类型结果
D: { msg: string}
M: {
greet(): todo
}
复制代码
接着推导 ObjectDescriptor 类型为
{
data(): { msg: string},
methods: {
greet(): string
} & ThisType<{msg:string} & {greet(): todo}>
}
复制代码
接着借助推导出来的 ObjectDescriptor 推导出 greet 里的 this 类型为
{ msg: string} & { greet(): todo}
复制代码
所以推导出 this.msg 类型为 string,进一步推导出 greet 的类型为 string,至此全部类型推完。 另外为了减少 Typescript 的类型推倒难度,应该尽量的显示的标注类型,防止出现循环推导或者形成推导复杂度变高等致使编译速度过慢甚至出现死循环或者内存耗尽的问题。
type ObjectDescriptor<D, M> = {
data: () => D
methods: M & ThisType<D & M>
}
declare function extend<D, M>(obj: ObjectDescriptor<D, M>): D & M const x = extend({ data() { return { msg: "hello", } }, methods: { greet(): string { // 显示的标注返回类型,简化推导 return this.msg + "world" // check }, }, }) 复制代码