详解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 相关文章推荐
文本链接逐个出现的js脚本
Dec 12 Javascript
JS event使用方法详解
Apr 28 Javascript
javascript开发随笔二 动态加载js和文件
Nov 25 Javascript
jquery在Chrome下获取图片的长宽问题解决
Mar 20 Javascript
jquery实现焦点图片随机切换效果的方法
Mar 12 Javascript
jQuery选择id属性带有点符号元素的方法
Mar 17 Javascript
全国省市二级联动下拉菜单 js版
May 10 Javascript
JavaScript每天必学之数组和对象部分
Sep 17 Javascript
bootstrap table小案例
Oct 21 Javascript
详解vue-Resource(与后端数据交互)
Jan 16 Javascript
vue-router 2.0 跳转之router.push()用法说明
Aug 12 Javascript
vue+swiper实现左右滑动的测试题功能
Oct 30 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
用文本作数据处理
2006/10/09 PHP
简单易用的计数器(数据库)
2006/10/09 PHP
xml在joomla表单中的应用详解分享
2012/07/19 PHP
php读取文件内容的方法汇总
2015/01/24 PHP
PHP实现的回溯算法示例
2017/08/15 PHP
PHP中通过getopt解析GNU C风格命令行选项
2019/11/18 PHP
jquery移动节点实例
2015/01/14 Javascript
JS实现仿google、百度搜索框输入信息智能提示的实现方法
2015/04/20 Javascript
九种原生js动画效果
2015/11/11 Javascript
JS折半插入排序算法实例
2015/12/02 Javascript
一波JavaScript日期判断脚本分享
2016/03/06 Javascript
深入理解$.each和$(selector).each
2016/05/15 Javascript
Three.js学习之网格
2016/08/10 Javascript
手机移动端实现 jquery和HTML5 Canvas的幸运大奖盘特效
2016/12/06 Javascript
AngularJS 异步解决实现方法
2017/06/12 Javascript
详解vue 模版组件的三种用法
2017/07/21 Javascript
vue2手机APP项目添加开屏广告或者闪屏广告
2017/11/28 Javascript
babel7.x和webpack4.x配置vue项目的方法步骤
2019/05/12 Javascript
[02:28]DOTA2英雄基础教程 灰烬之灵
2013/12/19 DOTA
python 计算数组中每个数字出现多少次--“Bucket”桶的思想
2017/12/19 Python
Python实现合并两个有序链表的方法示例
2019/01/31 Python
Python中判断子串存在的性能比较及分析总结
2019/06/23 Python
Python初学者常见错误详解
2019/07/02 Python
Django使用uwsgi部署时的配置以及django日志文件的处理方法
2019/08/30 Python
Python3之乱码\xe6\x97\xa0\xe6\xb3\x95处理方式
2020/05/11 Python
Python+OpenCV图像处理—— 色彩空间转换
2020/10/22 Python
Python 实现图片转字符画的示例(静态图片,gif皆可)
2020/11/05 Python
一个入门级python爬虫教程详解
2021/01/27 Python
解决方案设计综合面试题
2015/08/31 面试题
爱情寄语大全
2014/04/09 职场文书
爱心捐助倡议书
2014/05/19 职场文书
财务管理专业自荐书
2014/09/02 职场文书
二手车交易协议书标准版
2014/11/16 职场文书
2014年乡镇工作总结
2014/11/21 职场文书
2015元旦文艺汇演主持稿(开场白+结束语)
2014/12/14 职场文书
2016父亲节感恩话语
2015/12/09 职场文书