This in Typescript

this能夠說是Javascript裏最難理解的特性之一了,Typescript裏的 this 彷佛更加複雜了,Typescript裏的 this 有三中場景,不一樣的場景都有不一樣意思。javascript

  • this 參數: 限制調用函數時的 this 類型
  • this 類型: 用於支持鏈式調用,尤爲支持 class 繼承的鏈式調用
  • ThisType:用於構造複雜的 factory 函數

this 參數

因爲 javascript 支持靈活的函數調用方式,不一樣的調用場景,this 的指向也有所不一樣vue

  • 做爲對象的方法調用
  • 做爲普通函數調用
  • 做爲構造器調用
  • 做爲 Function.prototype.call 和 Function.prototype.bind 調用

對象方法調用

這也是絕大部分 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,可是有這本質的區別

  • 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 }, }, }) 複製代碼
相關文章
相關標籤/搜索