您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關如何在Typescript中使用This,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
this可以說是Javascript里最難理解的特性之一了,Typescript里的 this 似乎更加復雜了,Typescript里的 this 有三中場景,不同的場景都有不同意思。
this 參數(shù): 限制調(diào)用函數(shù)時的 this 類型
this 類型: 用于支持鏈式調(diào)用,尤其支持 class 繼承的鏈式調(diào)用
ThisType: 用于構(gòu)造復雜的 factory 函數(shù)
由于 javascript 支持靈活的函數(shù)調(diào)用方式,不同的調(diào)用場景,this 的指向也有所不同
作為對象的方法調(diào)用
作為普通函數(shù)調(diào)用
作為構(gòu)造器調(diào)用
作為 Function.prototype.call 和 Function.prototype.bind 調(diào)用
這也是絕大部分 this 的使用場景,當函數(shù)作為對象的 方法調(diào)用時,this 指向該對象
const obj = { name: "yj", getName() { return this.name // 可以自動推導為{ name:string, getName():string}類型 }, } obj.getName() // string類型
這里有個坑就是如果對象定義時對象方法是使用箭頭函數(shù)進行定義,則 this 指向的并不是對象而是全局的 window,Typescript 也自動的幫我推導為 window
const obj2 = { name: "yj", getName: () => { return this.name // check 報錯,這里的this指向的是window }, } obj2.getName() // 運行時報錯
即使是通過非箭頭函數(shù)定義的函數(shù),當將其賦值給變量,并直接通過變量調(diào)用時,其運行時 this 執(zhí)行的并非對象本身
const obj = { name: "yj", getName() { return this.name }, } const fn1 = obj.getName fn1() // this指向的是window,運行時報錯
很不幸,上述代碼在編譯期間并未檢查出來,我們可以通過為getName添加this的類型標注解決該問題
interface Obj { name: string // 限定getName調(diào)用時的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
這樣我們就能報保證調(diào)用時的 this 的類型安全
在 class 出現(xiàn)之前,一直是把 function 當做構(gòu)造函數(shù)使用,當通過 new 調(diào)用 function 時,構(gòu)造器里的 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 可以作為構(gòu)造函數(shù)調(diào)用,因此需要顯示的進行類型標注
interface People { name: string getName(): string } interface PeopleConstructor { new (name: string): People // 聲明可以作為構(gòu)造函數(shù)調(diào)用 prototype: People // 聲明prototype,支持后續(xù)修改prototype } const ctor = (function(this: People, name: string) { this.name = name } as unknown) as PeopleConstructor // 類型不兼容,二次轉(zhuǎn)型 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 有這本質(zhì)的區(qū)別 考慮如下三種 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,但是有這本質(zhì)的區(qū)別
method1: 原型方法,動態(tài) this,異步回調(diào)場景下需要自己手動 bind this
method2: 實例方法,類型報錯, 異步場景下需要手動 bind this
method3: 實例方法,靜態(tài) this, 異步場景下不需要手動 bind this
在我們編寫 React 應用時,大量的使用了 method3 這種自動綁定 this 的方式, 但實際上這種做法存在較大的問題
每個實例都會創(chuàng)建一個實例方法,造成了浪費
在處理繼承時,會導致違反直覺的現(xiàn)象
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 調(diào)用了示例方法而非原型方法,那么是無法在 subclass 里進行 override 的,這與其他語言處理繼承的 override 的行為向左,很容出問題。 因此更加合理的方式應該是不要使用實例方法,但是如何處理 this 的綁定問題呢。 目前較為合理的方式要么手動 bind,或者使用 decorator 來做 bind
import autobind from "autobind-decorator" class Test { name = 1 @autobind method1() { return this.name } }
call 和 apply 調(diào)用沒有什么本質(zhì)區(qū)別,主要區(qū)別就是 arguments 的傳遞方式,不分別討論。和普通的函數(shù)調(diào)用相比,call 調(diào)用可以動態(tài)的改變傳入的 this, 幸運的是 Typescript 借助 this 參數(shù)也支持對 call 調(diào)用的類型檢查
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 的實現(xiàn)也非常有意思,可以簡單研究下其實現(xiàn), 我們的實現(xiàn)就叫做 call2 首先需要確定 call 里 第一個參數(shù)的類型,很明顯 第一個參數(shù) 的類型對應的是函數(shù)里的 this 參數(shù)的類型,我們可以通過 ThisParameterType 工具來獲取一個函數(shù)的 this 參數(shù)類型
interface People { name: string } function ctor(this: People) {} type ThisArg = ThisParameterType<typeof ctor> // 為People類型 ThisParameterType 的實現(xiàn)也很簡單,借助 infer type 即可 type ThisParameterType<T> = T extends (this: unknown, ...args: any[]) => any T extends (this: infer U, ...args: any[]) => any ? U : unknown
但是我們怎么獲取當前函數(shù)的類型呢, 通過泛型實例化和泛型約束
interface CallableFunction { call2<T>(this: (this: T) => any, thisArg: T): any } interface People { name: string } function ctor(this: People) {} ctor.call2() //
在進行 ctor.call 調(diào)用時,根據(jù) CallableFunction 的定義其 this 參數(shù)類型為 (this:T) => any, 而此時的 this 即為 ctor, 而根據(jù) ctro 的類型定義,其類型為 (this:People) => any,實例化即可得此時的 T 實例化類型為 People, 即 thisArg 的類型為 People
進一步的添加返回值和其余參數(shù)類型
interface CallableFunction { call<T, A extends any[], R>( this: (this: T, ...args: A) => R, thisArg: T, ...args: A ): R }
為了支持 fluent interface, 需要支持方法的返回類型由調(diào)用示例確定,這實際上需要類型系統(tǒng)的額外至此。考慮如下代碼
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
仔細觀察上述代碼發(fā)現(xiàn),在不同的情況下,A1 的返回類型實際上是和調(diào)用對象有關的而非固定,只有這樣才能支持如下的鏈式調(diào)用,保證每一步調(diào)用都是類型安全
b.A1() .B1() .A2() .B2() // check ok
this 的處理還有其特殊之處,大部分語言對 this 的處理,都是將其作為隱式的參數(shù)處理,但是對于函數(shù)來講其參數(shù)應該是逆變的,但是 this 的處理實際上是當做協(xié)變處理的??紤]如下代碼
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特殊處理,視為協(xié)變 return this.A2(new Parent()) } A2(arg: Child) {} // flow下報錯,typescript沒報錯 A3(arg: number) {} // flow和typescript下均報錯 }
這里還要提的一點是 Typescript 處于兼容考慮,對方法進行了雙變處理,但是函數(shù)還是采用了逆變,相比之下 flow 則安全了許多,方法也采用了逆變處理
Vue2.x 最令人詬病的一點就是對 Typescript 的羸弱支持,其根源也在于 vue2.x 的 api 大量使用了 this,造成其類型難以推斷,Vue2.5 通過 ThisType 對 vue 的 typescript 支持進行了一波增強,但還是有不足之處,Vue3 的一個大的賣點也是改進了增強了對 Typescript 的支持。下面我們就研究下下 ThisType 和 vue 中是如何利用 ThisType 改進 Typescript 的支持的。
先簡單說一下 This 的決斷規(guī)則,推測對象方法的 this 類型規(guī)則如下,優(yōu)先級由低到高
對象字面量方法的 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 類型優(yōu)先
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} ,方法類型標注優(yōu)先級高于對象類型標注 }, }
如果對象字面量進行了類型標注,且該類型標注里包含了 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} }, }
將規(guī)則按從高到低排列如下
如果方法里顯示標注了 this 類型,這是用該標注類型
如果上述沒標注,但是對象標注的類型里的方法類型標注了 this 類型,則使用該 this 類型
如果上述都沒標注,但對象標注的類型里包含了 ThisType, 那么 this 類型為 T
如果上述都沒標注,this 類型為對象的標注類型
如果上述都沒標注,this 類型為對象字面量類型
這里的一條重要規(guī)則就是在沒有其他類型標注的情況下,如果對象標注的類型里如果包含了 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 這個對象字面量的類型,因此無法從中區(qū)獲取 data 的類型,所以主要難題是如何在 methods.greet 里類型安全的訪問到 data 里的 msg。 借助于泛型推導和 ThisType 可以很輕松的實現(xiàn),下面讓我們自己實現(xiàn)一些這個 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 }, }, })
其推導規(guī)則如下 首先根據(jù)對象字面量的類型和泛型約束對比, 可得到類型參數(shù) T 和 M 的實例化類型結(jié)果
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 的類型推倒難度,應該盡可能的顯示的標注類型,防止出現(xiàn)循環(huán)推導或者造成推導復雜度變高等導致編譯速度過慢甚至出現(xiàn)死循環(huán)或者內(nèi)存耗盡的問題。
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,小編相信有部分知識點可能是我們?nèi)粘9ぷ鲿姷交蛴玫降?。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業(yè)資訊頻道。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。