详解Typescript里的This的使用方法


Posted in Javascript onJanuary 08, 2021

this可以说是Javascript里最难理解的特性之一了,Typescript里的 this 似乎更加复杂了,Typescript里的 this 有三中场景,不同的场景都有不同意思。

  • this 参数: 限制调用函数时的 this 类型
  • this 类型: 用于支持链式调用,尤其支持 class 继承的链式调用
  • ThisType: 用于构造复杂的 factory 函数

this 参数

由于 javascript 支持灵活的函数调用方式,不同的调用场景,this 的指向也有所不同

  • 作为对象的方法调用
  • 作为普通函数调用
  • 作为构造器调用
  • 作为 Function.prototype.call 和 Function.prototype.bind 调用

对象方法调用

这也是绝大部分 this 的使用场景,当函数作为对象的 方法调用时,this 指向该对象

const obj = {
 name: "yj",
 getName() {
 return this.name // 可以自动推导为{ name:string, getName():string}类型
 },
}
obj.getName() // string类型

这里有个坑就是如果对象定义时对象方法是使用箭头函数进行定义,则 this 指向的并不是对象而是全局的 window,Typescript 也自动的帮我推导为 window

const obj2 = {
 name: "yj",
 getName: () => {
 return this.name // check 报错,这里的this指向的是window
 },
}
obj2.getName() // 运行时报错

普通函数调用

即使是通过非箭头函数定义的函数,当将其赋值给变量,并直接通过变量调用时,其运行时 this 执行的并非对象本身

const obj = {
 name: "yj",
 getName() {
 return this.name
 },
}
const fn1 = obj.getName
fn1() // this指向的是window,运行时报错

很不幸,上述代码在编译期间并未检查出来,我们可以通过为getName添加this的类型标注解决该问题

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 的类型安全

构造器调用

在 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 的类型推断暂时并未支持 https://github.com/microsoft/TypeScript/issues/18171), 没办法推导出 this 的类型和 people 可以作为构造函数调用,因此需要显示的进行类型标注

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())

当然最简洁的方式,还是使用 class

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,但是有这本质的区别

  • method1: 原型方法,动态 this,异步回调场景下需要自己手动 bind this
  • method2: 实例方法,类型报错, 异步场景下需要手动 bind this
  • method3: 实例方法,静态 this, 异步场景下不需要手动 bind this

在我们编写 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 调用

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
}

This Types

为了支持 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 则安全了许多,方法也采用了逆变处理

ThisType

Vue2.x 最令人诟病的一点就是对 Typescript 的羸弱支持,其根源也在于 vue2.x 的 api 大量使用了 this,造成其类型难以推断,Vue2.5 通过 ThisType 对 vue 的 typescript 支持进行了一波增强,但还是有不足之处,Vue3 的一个大的卖点也是改进了增强了对 Typescript 的支持。下面我们就研究下下 ThisType 和 vue 中是如何利用 ThisType 改进 Typescript 的支持的。

先简单说一下 This 的决断规则,推测对象方法的 this 类型规则如下,优先级由低到高

对象字面量方法的 this 类型为该对象字面量本身

// containing object literal type
let foo = {
 x: "hello",
 f(n: number) {
 this //this: {x: string;f(n: number):void }
 },
}

如果对象字面量进行了类型标注了,则 this 类型为标注的对象类型

type Point = {
 x: number
 y: number
 moveBy(dx: number, dy: number): void
}

let p: Point = {
 x: 10,
 y: 20,
 moveBy(dx, dy) {
 this // Point
 },
}

如果对象字面量的方法有 this 类型标注了,则为标注的 this 类型

let bar = {
 x: "hello",
 f(this: { message: string }) {
 this // { message: string }
 },
}

如果对象字面量的即进行了类型标注,同时方法也标注了类型,则方法的标注 this 类型优先

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} ,方法类型标注优先级高于对象类型标注
 },
}

如果对象字面量进行了类型标注,且该类型标注里包含了 ThisType,那么 this 类型为 T

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}
 },
}

如果对象字面量进行了类型标注,且类型标注里指明了 this 类型, 则使用该标注类型

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}
 },
}

将规则按从高到低排列如下

  • 如果方法里显示标注了 this 类型,这是用该标注类型
  • 如果上述没标注,但是对象标注的类型里的方法类型标注了 this 类型,则使用该 this 类型
  • 如果上述都没标注,但对象标注的类型里包含了 ThisType, 那么 this 类型为 T
  • 如果上述都没标注,this 类型为对象的标注类型
  • 如果上述都没标注,this 类型为对象字面量类型

这里的一条重要规则就是在没有其他类型标注的情况下,如果对象标注的类型里如果包含了 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
 },
 },
})

到此这篇关于详解Typescript里的This的使用方法的文章就介绍到这了,更多相关Typescript This内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木! 

Javascript 相关文章推荐
javascript contains和compareDocumentPosition 方法来确定是否HTML节点间的关系
Feb 04 Javascript
jQuery 工具函数学习资料
Apr 29 Javascript
JavaScript游戏之优化篇
Nov 08 Javascript
js判断屏幕分辨率的代码
Jul 16 Javascript
JS随机生成不重复数据的实例方法
Jul 17 Javascript
javascript去掉前后空格的实例
Nov 07 Javascript
在JavaScript中操作时间之getMonth()方法的使用
Jun 10 Javascript
Angular实现购物车计算示例代码
Feb 21 Javascript
如何给ss bash 写一个 WEB 端查看流量的页面
Mar 23 Javascript
js保留两位小数方法总结
Jan 31 Javascript
jQuery+Datatables实现表格批量删除功能【推荐】
Oct 24 jQuery
koa2服务端使用jwt进行鉴权及路由权限分发的流程分析
Jul 22 Javascript
Node.js 中如何收集和解析命令行参数
Jan 08 #Javascript
vue编写简单的购物车功能
Jan 08 #Vue.js
three.js中多线程的使用及性能测试详解
Jan 07 #Javascript
解决vue使用vant轮播组件swipe + flex时文字抖动问题
Jan 07 #Vue.js
vuex的使用和简易实现
Jan 07 #Vue.js
vue watch监控对象的简单方法示例
Jan 07 #Vue.js
vue.js watch经常失效的场景与解决方案
Jan 07 #Vue.js
You might like
Search Engine Friendly的URL设计
2006/10/09 PHP
PHP调用Twitter的RSS的实现代码
2010/03/10 PHP
10条PHP高级技巧[修正版]
2011/08/02 PHP
php实现文件下载更能介绍
2012/11/23 PHP
Symfony2实现在doctrine中内置数据的方法
2016/02/05 PHP
PHP http请求超时问题解决方案
2020/11/13 PHP
javascript第一课
2007/02/27 Javascript
jQuery获取浏览器中的分辨率实现代码
2013/04/23 Javascript
Javascript Ajax异步读取RSS文档具体实现
2013/12/12 Javascript
jquery fancybox ie6不显示关闭按钮的解决办法
2013/12/25 Javascript
javascript使用正则表达式实现去掉空格之后的字符
2015/02/15 Javascript
JQuery节点元素属性操作方法
2015/06/11 Javascript
基于JavaScript操作DOM常用的API小结
2015/12/01 Javascript
js表单验证实例讲解
2016/03/31 Javascript
Jquery组件easyUi实现手风琴(折叠面板)示例
2016/08/23 Javascript
ionic隐藏tabs的方法
2016/08/29 Javascript
分类解析jQuery选择器
2016/11/23 Javascript
JavaScript使用delete删除数组元素用法示例【数组长度不变】
2017/01/17 Javascript
vue-cli 组件的导入与使用教程详解
2018/04/11 Javascript
微信小程序利用canvas 绘制幸运大转盘功能
2018/07/06 Javascript
深入了解js原型模式
2019/05/30 Javascript
jQuery pager.js 插件动态分页功能实例分析
2019/08/02 jQuery
[03:13]DOTA2-DPC中国联赛1月25日Recap集锦
2021/03/11 DOTA
[08:08]DOTA2-DPC中国联赛2月28日Recap集锦
2021/03/11 DOTA
python3 selenium 切换窗口的几种方法小结
2018/05/21 Python
使用python写的opencv实时监测和解析二维码和条形码
2019/08/14 Python
Python pip 常用命令汇总
2020/10/19 Python
世界上最全面的汽车零部件和配件集合:JC Whitney
2016/09/04 全球购物
英国时尚优质的女装:Hope Fashion
2018/08/14 全球购物
欢迎领导标语
2014/06/27 职场文书
交通事故起诉书
2015/05/19 职场文书
医院中层管理人员培训心得体会
2016/01/11 职场文书
感谢信的技巧及范例
2019/05/15 职场文书
MySQL分区表实现按月份归类
2021/11/01 MySQL
PHP遍历数组的6种方式总结
2021/11/17 PHP
Unicode中的CJK(中日韩统一表意文字)字符小结
2021/12/06 HTML / CSS