详解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 相关文章推荐
JSONP 跨域共享信息
Aug 16 Javascript
浮动的div自适应居中显示的js代码
Dec 23 Javascript
jQuery获取iframe的document对象的方法
Oct 10 Javascript
jQuery带箭头提示框tooltips插件集锦
Nov 17 Javascript
15个jquery常用方法、小技巧分享
Jan 13 Javascript
jQuery标签编辑插件Tagit使用指南
Apr 21 Javascript
js显示文本框提示文字的方法
May 07 Javascript
jQueryUI DatePicker 添加时分秒
Jun 04 Javascript
AngularJs篇:使用AngularJs打造一个简易权限系统的实现代码
Dec 26 Javascript
JS实现点击生成UUID的方法完整实例【基于jQuery】
Jun 12 jQuery
vue实现数字滚动效果
Jun 29 Javascript
Openlayers显示瓦片网格信息的方法
Sep 28 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
PHP如何将XML转成数组
2016/04/04 PHP
PHP array_key_exists检查键名或索引是否存在于数组中的实现方法
2016/06/13 PHP
thinkphp3.2.3版本的数据库增删改查实现代码
2016/09/22 PHP
php实现页面纯静态的实例代码
2017/06/21 PHP
使用JQuery进行跨域请求
2010/01/25 Javascript
DD_belatedPNG,IE6下PNG透明解决方案(国外)
2010/12/06 Javascript
在JavaScript并非所有的一切都是对象
2013/04/11 Javascript
页面向下滚动ajax获取数据的实现方法(兼容手机)
2016/05/24 Javascript
jQuery获取table行数并输出单元格内容的实现方法
2016/06/30 Javascript
js替换字符串中所有指定的字符(实现代码)
2016/08/17 Javascript
Vue2实现组件props双向绑定
2016/12/02 Javascript
angular过滤器实现排序功能
2017/06/27 Javascript
Vue.js 点击按钮显示/隐藏内容的实例代码
2018/02/08 Javascript
jquery 实现拖动文件上传加载进度条功能
2018/03/18 jQuery
vue-cli 默认路由再子路由选中下的选中状态问题及解决代码
2018/09/06 Javascript
详解关于vue2.0工程发布上线操作步骤
2018/09/27 Javascript
从零开始封装自己的自定义Vue组件
2018/10/09 Javascript
vue-cli中使用高德地图的方法示例
2019/03/28 Javascript
使用Python编写爬虫的基本模块及框架使用指南
2016/01/20 Python
flask中主动抛出异常及统一异常处理代码示例
2018/01/18 Python
django的登录注册系统的示例代码
2018/05/14 Python
python pands实现execl转csv 并修改csv指定列的方法
2018/12/12 Python
pycharm新建一个python工程步骤
2019/07/16 Python
浅谈python3中input输入的使用
2019/08/02 Python
彻底搞懂 python 中文乱码问题(深入分析)
2020/02/28 Python
详解Python中pyautogui库的最全使用方法
2020/04/01 Python
一款超酷的js+css3实现的3D标签云特效兼容ie7/8/9
2013/11/18 HTML / CSS
css3中背景尺寸background-size详解
2014/09/02 HTML / CSS
世界上最大的冷却器制造商:Igloo Coolers
2019/07/23 全球购物
美国牙科折扣计划:DentalPlans.com
2019/08/26 全球购物
统计每一学生的平均成绩
2014/06/06 面试题
新教师个人工作总结
2015/02/06 职场文书
Java比较两个对象中全部属性值是否相等的方法
2021/08/07 Java/Android
vue实现滑动解锁功能
2022/03/03 Vue.js
python运算符之与用户交互
2022/04/13 Python
Nginx文件已经存在全局反向代理问题排查记录
2022/07/15 Servers