详解Typescript里的This的使用方法

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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 关于TypeScript模块导入的那些事

    前言 模块在其自身的作用域里执行,而不是在全局作用域里:这意味着定义在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export之一导出它们. 相反,如果想使用其它模块导出的变量,函数,类,接口等的时候,你必须要导入它们,可以使用import之一. 模块是自声明的.在TypeScript里,两个模块之间的关系是通过在文件级别上使用import和export建立的. 下面话不多说了,来一起看看详细的介绍吧 ES6 模块导入的限制 我们先来看一个具体的例子: 在 Node 项目

  • 详解在Vue中使用TypeScript的一些思考(实践)

    Vue.extend or vue-class-component 使用 TypeScript 写 Vue 组件时,有两种推荐形式: Vue.extend():使用基础 Vue 构造器,创建一个"子类".此种写法与 Vue 单文件组件标准形式最为接近,唯一不同仅是组件选项需要被包裹在 Vue.extend() 中. vue-class-component:通常与 vue-property-decorator 一起使用,提供一系列装饰器,能让我们书写类风格的 Vue 组件. 两种形式输出

  • TypeScript类型声明书写详解

    本文总结一下TypeScript类型声明的书写,很多时候写TypeScript不是问题,写类型就特别纠结,我总结下,我在使用TypeScript中遇到的问题.如果你遇到类型声明不会写的时候,多看看lodash的声明,因为lodash对数据进行各种变形操作,所以你能遇到的,都有参考示例. 基本类型 // 变量 const num: number = 1; const str: string = 'str'; const bool: boolean = true; const nulls: null

  • Typescript的三种运行方式(小结)

    一.在线complier 这种方式最简单,不需在本地做任何配置安装,只需进入Typescript的官网,点击里面的playground就可以直接写代码了.但这种方式只适用于测试而不适用于开发. 二.本地命令行编译 1.在本地编译运行Typescript需要使用npm下载typescript npm install -g typescript 至于npm,就是node的包管理工具,下载node后就自动带了. 2.下载完成后可以使用 tsc -v 查看版本 3.使用:如在本地创建Hello.ts e

  • 详解Vue3.0 前的 TypeScript 最佳入门实践

    前言 我个人对更严格类型限制没有积极的看法,毕竟各类转类型的骚写法写习惯了. 然鹅最近的一个项目中,是 TypeScript + Vue ,毛计喇,学之...-真香! 注意此篇标题的"前",本文旨在讲Ts混入框架的使用,不讲Class API 1. 使用官方脚手架构建 npm install -g @vue/cli # OR yarn global add @vue/cli 新的 Vue CLI 工具允许开发者 使用 TypeScript 集成环境 创建新项目. 只需运行 vue cr

  • TypeScript学习之强制类型的转换

    前言 使用强类型变量常常需要从一种类型向另一种类型转换,通常使用ToString或ParseInt可以来实现一些简单的转换,但是有时候需要像.NET语言中那样将一种类型显示的转换为另一种类型,在TypeScript规范中,被称为"类型断言",它仍然是类型转换,只是语法是有些不同.下面来详细看看TypeScript的强制类型转换. TypeScript强制类型转换 在 TypeScript 中将一个 number 转换成 string ,这样做会报错: var a:number = 12

  • js捆绑TypeScript声明文件的方法教程

    前话 TypeScript是JavaScript类型的超集,这是TypeScript的文档介绍的一句话,那么他们存在联系呢? 我的理解是,TypeScript在JavaScript基础上引入强类型语言的特性.开发者使用TypeScript语法进行编程开发,最终通过转换工具将TypeScript转换成JavaScript. 使用TypeScript能够避免在原生JavaScript上开发所带来的弱类型语言的坑.(我该输入啥?调用后返回啥?哎还是看看源码吧...) 嗯!很好,强类型的JavaScri

  • 在Vue组件中使用 TypeScript的方法

    注意:此文并不是把vue改为全部替换为ts,而是可以在原来的项目中植入ts文件,目前只是实践阶段,向ts转化过程中的过渡. ts有什么用? 类型检查.直接编译到原生js.引入新的语法糖 为什么用ts? TypeScript的设计目的应该是解决JavaScript的"痛点":弱类型和没有命名空间,导致很难模块化,不适合开发大型程序.另外它还提供了一些语法糖来帮助大家更方便地实践面向对象的编程. typescript不仅可以约束我们的编码习惯,还能起到注释的作用,当我们看到一函数后我们立马

  • 详解Typescript里的This的使用方法

    this可以说是Javascript里最难理解的特性之一了,Typescript里的 this 似乎更加复杂了,Typescript里的 this 有三中场景,不同的场景都有不同意思. this 参数: 限制调用函数时的 this 类型 this 类型: 用于支持链式调用,尤其支持 class 继承的链式调用 ThisType: 用于构造复杂的 factory 函数 this 参数 由于 javascript 支持灵活的函数调用方式,不同的调用场景,this 的指向也有所不同 作为对象的方法调用

  • 详解 TypeScript 枚举类型

    目录 1. 数字枚举 2. 字符串枚举 3. 反向映射 4. 异构枚举 5. 常量枚举 6. 枚举成员类型和联合枚举类型 (1)枚举成员类型 (2)联合枚举类型 7. 枚举合并 前言: TypeScript 在 ES 原有类型基础上加入枚举类型,使得在 TypeScript 中也可以给一组数值赋予名字,这样对开发者比较友好,可以理解枚举就是一个字典. 枚举类型使用enum来定义: enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, F

  • 一文详解typeScript的extends关键字

    目录 前言 extends 的几个语义 extends 与 类型组合/类继承 extends 与类型约束 extends 与条件类型 extends 与 {} extends 与 any extends 与 never extends 与 联合类型 extends 判断类型严格相等 extends 与类型推导 总结 前言 声明: 以下文章所包含的结论都是基于 typeScript@4.9.4 版本所取得的. extends 是 typeScript 中的关键字.在 typeScript 的类型编

  • 详解python里使用正则表达式的分组命名方式

    详解python里使用正则表达式的分组命名方式 分组匹配的模式,可以通过groups()来全部访问匹配的元组,也可以通过group()函数来按分组方式来访问,但是这里只能通过数字索引来访问,如果某一天产品经理需要修改需求,让你在它们之中添加一个分组,这样一来,就会导致匹配的数组的索引的变化,作为开发人员的你,必须得一行一行代码地修改.因此聪明的开发人员又想到一个好方法,把这些分组进行命名,只需要对名称进行访问分组,不通过索引来访问了,就可以避免这个问题.那么怎么样来命名呢?可以采用(?P<nam

  • 详解python里使用正则表达式的全匹配功能

    详解python里使用正则表达式的全匹配功能 python中很多匹配,比如搜索任意位置的search()函数,搜索边界的match()函数,现在还需要学习一个全匹配函数,就是搜索的字符与内容全部匹配,它就是fullmatch()函数. 例子如下: #python 3.6 #蔡军生 #http://blog.csdn.net/caimouse/article/details/51749579 # import re text = 'This is some text -- with punctua

  • 详解Python里使用正则表达式的ASCII模式

    ASCII ASCII(American Standard Code for Information Interchange),是一种单字节的编码.计算机世界里一开始只有英文,而单字节可以表示256个不同的字符,可以表示所有的英文字符和许多的控制符号.不过ASCII只用到了其中的一半(\x80以下),这也是MBCS得以实现的基础. 目前,基本上都使用python3来开发了,但是有时为了兼容旧的python2的代码,在正则表达式里主要是字符串的表示方式不同了,在python3里是使用Unicode

  • 详解Typescript 内置的模块导入兼容方式

    一.前言 前端的模块化规范包括 commonJS.AMD.CMD 和 ES6.其中 AMD 和 CMD 可以说是过渡期的产物,目前较为常见的是commonJS 和 ES6.在 TS 中这两种模块化方案的混用,往往会出现一些意想不到的问题. 二.import * as 考虑到兼容性,我们一般会将代码编译为 es5 标准,于是 tsconfig.json 会有以下配置: { "compilerOptions": { "module": "commonjs&qu

  • 详解TypeScript中的类型保护

    概述 在 TypeScript 中使用联合类型时,往往会碰到这种尴尬的情况: interface Bird { // 独有方法 fly(); // 共有方法 layEggs(); } interface Fish { // 独有方法 swim(); // 共有方法 layEggs(); } function getSmallPet(): Fish | Bird { // ... } let pet = getSmallPet(); pet.layEggs(); // 正常 pet.swim();

  • 详解TypeScript映射类型和更好的字面量类型推断

    概述 TypeScript 2.1 引入了映射类型,这是对类型系统的一个强大的补充.本质上,映射类型允许w咱们通过映射属性类型从现有类型创建新类型.根据咱们指定的规则转换现有类型的每个属性.转换后的属性组成新的类型. 使用映射类型,可以捕获类型系统中类似Object.freeze()等方法的效果.冻结对象后,就不能再添加.更改或删除其中的属性.来看看如何在不使用映射类型的情况下在类型系统中对其进行编码: interface Point { x: number; y: number; } inte

  • 详解Vue里循环form表单项实例

    有的时候我们可能会遇到这种需求,用户点击某个按钮就可以增加一个同样的表单出来,点击一次增加一次.然后要用到深拷贝,Vue.js+ElementUI等等.效果大概如下,就是一个表单有下拉框和两个输入框,现在点击"添加表单"按钮之后就会多一个表单出来,点击"提交表单"后就同时提交两个表单的value值. 代码如下: <template> <div> <div style="margin: 10px 0"> <

随机推荐