this可以說(shuō)是Javascript里最難理解的特性之一了,Typescript里的 this 似乎更加復(fù)雜了,Typescript里的 this 有三中場(chǎng)景,不同的場(chǎng)景都有不同意思。
- this 參數(shù): 限制調(diào)用函數(shù)時(shí)的 this 類(lèi)型
- this 類(lèi)型: 用于支持鏈?zhǔn)秸{(diào)用,尤其支持 class 繼承的鏈?zhǔn)秸{(diào)用
- ThisType: 用于構(gòu)造復(fù)雜的 factory 函數(shù)
this 參數(shù)
由于 javascript 支持靈活的函數(shù)調(diào)用方式,不同的調(diào)用場(chǎng)景,this 的指向也有所不同
- 作為對(duì)象的方法調(diào)用
- 作為普通函數(shù)調(diào)用
- 作為構(gòu)造器調(diào)用
- 作為 Function.prototype.call 和 Function.prototype.bind 調(diào)用
對(duì)象方法調(diào)用
這也是絕大部分 this 的使用場(chǎng)景,當(dāng)函數(shù)作為對(duì)象的 方法調(diào)用時(shí),this 指向該對(duì)象
1
2
3
4
5
6
7
|
const obj = { name: "yj" , getName() { return this .name // 可以自動(dòng)推導(dǎo)為{ name:string, getName():string}類(lèi)型 }, } obj.getName() // string類(lèi)型 |
這里有個(gè)坑就是如果對(duì)象定義時(shí)對(duì)象方法是使用箭頭函數(shù)進(jìn)行定義,則 this 指向的并不是對(duì)象而是全局的 window,Typescript 也自動(dòng)的幫我推導(dǎo)為 window
1
2
3
4
5
6
7
|
const obj2 = { name: "yj" , getName: () => { return this .name // check 報(bào)錯(cuò),這里的this指向的是window }, } obj2.getName() // 運(yùn)行時(shí)報(bào)錯(cuò) |
普通函數(shù)調(diào)用
即使是通過(guò)非箭頭函數(shù)定義的函數(shù),當(dāng)將其賦值給變量,并直接通過(guò)變量調(diào)用時(shí),其運(yùn)行時(shí) this 執(zhí)行的并非對(duì)象本身
1
2
3
4
5
6
7
8
|
const obj = { name: "yj" , getName() { return this .name }, } const fn1 = obj.getName fn1() // this指向的是window,運(yùn)行時(shí)報(bào)錯(cuò) |
很不幸,上述代碼在編譯期間并未檢查出來(lái),我們可以通過(guò)為getName添加this的類(lèi)型標(biāo)注解決該問(wèn)題
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
interface Obj { name: string // 限定getName調(diào)用時(shí)的this類(lèi)型 getName( this : Obj): string } const obj: Obj = { name: "yj" , getName() { return this .name }, } obj.getName() // check ok const fn1 = obj.getName fn1() // check error |
這樣我們就能報(bào)保證調(diào)用時(shí)的 this 的類(lèi)型安全
構(gòu)造器調(diào)用
在 class 出現(xiàn)之前,一直是把 function 當(dāng)做構(gòu)造函數(shù)使用,當(dāng)通過(guò) new 調(diào)用 function 時(shí),構(gòu)造器里的 this 就指向返回對(duì)象
1
2
3
4
5
6
7
|
function People(name: string) { this .name = name // check error } People.prototype.getName = function () { return this .name } const people = new People() // check error |
很不幸,Typescript 暫時(shí)對(duì) ES5 的 constructor function 的類(lèi)型推斷暫時(shí)并未支持 https://github.com/microsoft/TypeScript/issues/18171), 沒(méi)辦法推導(dǎo)出 this 的類(lèi)型和 people 可以作為構(gòu)造函數(shù)調(diào)用,因此需要顯示的進(jìn)行類(lèi)型標(biāo)注
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 // 類(lèi)型不兼容,二次轉(zhuǎn)型 ctor.prototype.getName = function () { return this .name } const people = new ctor( "yj" ) console.log( "people:" , people) console.log(people.getName()) |
當(dāng)然最簡(jiǎn)潔的方式,還是使用 class
1
2
3
4
5
6
7
8
9
10
11
|
class People { name: string constructor(name: string) { this .name = name // check ok } getName() { return this .name } } const people = new People( "yj" ) // check ok |
這里還有一個(gè)坑,即在 class 里 public field method 和 method 有這本質(zhì)的區(qū)別 考慮如下三種 method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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 |
雖然上述三個(gè)代碼都能成功的輸出 1,但是有這本質(zhì)的區(qū)別
- method1: 原型方法,動(dòng)態(tài) this,異步回調(diào)場(chǎng)景下需要自己手動(dòng) bind this
- method2: 實(shí)例方法,類(lèi)型報(bào)錯(cuò), 異步場(chǎng)景下需要手動(dòng) bind this
- method3: 實(shí)例方法,靜態(tài) this, 異步場(chǎng)景下不需要手動(dòng) bind this
在我們編寫(xiě) React 應(yīng)用時(shí),大量的使用了 method3 這種自動(dòng)綁定 this 的方式, 但實(shí)際上這種做法存在較大的問(wèn)題
- 每個(gè)實(shí)例都會(huì)創(chuàng)建一個(gè)實(shí)例方法,造成了浪費(fèi)
- 在處理繼承時(shí),會(huì)導(dǎo)致違反直覺(jué)的現(xiàn)象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
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 |
在處理繼承的時(shí)候,如果 superclass 調(diào)用了示例方法而非原型方法,那么是無(wú)法在 subclass 里進(jìn)行 override 的,這與其他語(yǔ)言處理繼承的 override 的行為向左,很容出問(wèn)題。 因此更加合理的方式應(yīng)該是不要使用實(shí)例方法,但是如何處理 this 的綁定問(wèn)題呢。 目前較為合理的方式要么手動(dòng) bind,或者使用 decorator 來(lái)做 bind
1
2
3
4
5
6
7
8
|
import autobind from "autobind-decorator" class Test { name = 1 @autobind method1() { return this .name } } |
call 和 apply 調(diào)用
call 和 apply 調(diào)用沒(méi)有什么本質(zhì)區(qū)別,主要區(qū)別就是 arguments 的傳遞方式,不分別討論。和普通的函數(shù)調(diào)用相比,call 調(diào)用可以動(dòng)態(tài)的改變傳入的 this, 幸運(yùn)的是 Typescript 借助 this 參數(shù)也支持對(duì) call 調(diào)用的類(lèi)型檢查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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 的實(shí)現(xiàn)也非常有意思,可以簡(jiǎn)單研究下其實(shí)現(xiàn), 我們的實(shí)現(xiàn)就叫做 call2 首先需要確定 call 里 第一個(gè)參數(shù)的類(lèi)型,很明顯 第一個(gè)參數(shù) 的類(lèi)型對(duì)應(yīng)的是函數(shù)里的 this 參數(shù)的類(lèi)型,我們可以通過(guò) ThisParameterType 工具來(lái)獲取一個(gè)函數(shù)的 this 參數(shù)類(lèi)型
1
2
3
4
5
6
7
8
9
10
11
12
|
interface People { name: string } function ctor( this : People) {} type ThisArg = ThisParameterType< typeof ctor> // 為People類(lèi)型 ThisParameterType 的實(shí)現(xiàn)也很簡(jiǎn)單,借助 infer type 即可 type ThisParameterType<T> = T extends ( this : unknown, ...args: any[]) => any T extends ( this : infer U, ...args: any[]) => any ? U : unknown |
但是我們?cè)趺传@取當(dāng)前函數(shù)的類(lèi)型呢, 通過(guò)泛型實(shí)例化和泛型約束
1
2
3
4
5
6
7
8
|
interface CallableFunction { call2<T>( this : ( this : T) => any, thisArg: T): any } interface People { name: string } function ctor( this : People) {} ctor.call2() // |
在進(jìn)行 ctor.call 調(diào)用時(shí),根據(jù) CallableFunction 的定義其 this 參數(shù)類(lèi)型為 (this:T) => any, 而此時(shí)的 this 即為 ctor, 而根據(jù) ctro 的類(lèi)型定義,其類(lèi)型為 (this:People) => any,實(shí)例化即可得此時(shí)的 T 實(shí)例化類(lèi)型為 People, 即 thisArg 的類(lèi)型為 People
進(jìn)一步的添加返回值和其余參數(shù)類(lèi)型
1
2
3
4
5
6
7
|
interface CallableFunction { call<T, A extends any[], R>( this : ( this : T, ...args: A) => R, thisArg: T, ...args: A ): R } |
This Types
為了支持 fluent interface, 需要支持方法的返回類(lèi)型由調(diào)用示例確定,這實(shí)際上需要類(lèi)型系統(tǒng)的額外至此。考慮如下代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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() // 不報(bào)錯(cuò) a.A1().B1() // 報(bào)錯(cuò) type M1 = ReturnType< typeof b.A1> // B type M2 = ReturnType< typeof a.A1> // A |
仔細(xì)觀察上述代碼發(fā)現(xiàn),在不同的情況下,A1 的返回類(lèi)型實(shí)際上是和調(diào)用對(duì)象有關(guān)的而非固定,只有這樣才能支持如下的鏈?zhǔn)秸{(diào)用,保證每一步調(diào)用都是類(lèi)型安全
1
2
3
4
|
b.A1() .B1() .A2() .B2() // check ok |
this 的處理還有其特殊之處,大部分語(yǔ)言對(duì) this 的處理,都是將其作為隱式的參數(shù)處理,但是對(duì)于函數(shù)來(lái)講其參數(shù)應(yīng)該是逆變的,但是 this 的處理實(shí)際上是當(dāng)做協(xié)變處理的。考慮如下代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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() { // 不報(bào)錯(cuò),this特殊處理,視為協(xié)變 return this .A2( new Parent()) } A2(arg: Child) {} // flow下報(bào)錯(cuò),typescript沒(méi)報(bào)錯(cuò) A3(arg: number) {} // flow和typescript下均報(bào)錯(cuò) } |
這里還要提的一點(diǎn)是 Typescript 處于兼容考慮,對(duì)方法進(jìn)行了雙變處理,但是函數(shù)還是采用了逆變,相比之下 flow 則安全了許多,方法也采用了逆變處理
ThisType
Vue2.x 最令人詬病的一點(diǎn)就是對(duì) Typescript 的羸弱支持,其根源也在于 vue2.x 的 api 大量使用了 this,造成其類(lèi)型難以推斷,Vue2.5 通過(guò) ThisType 對(duì) vue 的 typescript 支持進(jìn)行了一波增強(qiáng),但還是有不足之處,Vue3 的一個(gè)大的賣(mài)點(diǎn)也是改進(jìn)了增強(qiáng)了對(duì) Typescript 的支持。下面我們就研究下下 ThisType 和 vue 中是如何利用 ThisType 改進(jìn) Typescript 的支持的。
先簡(jiǎn)單說(shuō)一下 This 的決斷規(guī)則,推測(cè)對(duì)象方法的 this 類(lèi)型規(guī)則如下,優(yōu)先級(jí)由低到高
對(duì)象字面量方法的 this 類(lèi)型為該對(duì)象字面量本身
1
2
3
4
5
6
7
|
// containing object literal type let foo = { x: "hello" , f(n: number) { this //this: {x: string;f(n: number):void } }, } |
如果對(duì)象字面量進(jìn)行了類(lèi)型標(biāo)注了,則 this 類(lèi)型為標(biāo)注的對(duì)象類(lèi)型
1
2
3
4
5
6
7
8
9
10
11
12
13
|
type Point = { x: number y: number moveBy(dx: number, dy: number): void } let p: Point = { x: 10, y: 20, moveBy(dx, dy) { this // Point }, } |
如果對(duì)象字面量的方法有 this 類(lèi)型標(biāo)注了,則為標(biāo)注的 this 類(lèi)型
1
2
3
4
5
6
|
let bar = { x: "hello" , f( this : { message: string }) { this // { message: string } }, } |
如果對(duì)象字面量的即進(jìn)行了類(lèi)型標(biāo)注,同時(shí)方法也標(biāo)注了類(lèi)型,則方法的標(biāo)注 this 類(lèi)型優(yōu)先
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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} ,方法類(lèi)型標(biāo)注優(yōu)先級(jí)高于對(duì)象類(lèi)型標(biāo)注 }, } |
如果對(duì)象字面量進(jìn)行了類(lèi)型標(biāo)注,且該類(lèi)型標(biāo)注里包含了 ThisType,那么 this 類(lèi)型為 T
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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} }, } |
如果對(duì)象字面量進(jìn)行了類(lèi)型標(biāo)注,且類(lèi)型標(biāo)注里指明了 this 類(lèi)型, 則使用該標(biāo)注類(lèi)型
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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ī)則按從高到低排列如下
- 如果方法里顯示標(biāo)注了 this 類(lèi)型,這是用該標(biāo)注類(lèi)型
- 如果上述沒(méi)標(biāo)注,但是對(duì)象標(biāo)注的類(lèi)型里的方法類(lèi)型標(biāo)注了 this 類(lèi)型,則使用該 this 類(lèi)型
- 如果上述都沒(méi)標(biāo)注,但對(duì)象標(biāo)注的類(lèi)型里包含了 ThisType, 那么 this 類(lèi)型為 T
- 如果上述都沒(méi)標(biāo)注,this 類(lèi)型為對(duì)象的標(biāo)注類(lèi)型
- 如果上述都沒(méi)標(biāo)注,this 類(lèi)型為對(duì)象字面量類(lèi)型
這里的一條重要規(guī)則就是在沒(méi)有其他類(lèi)型標(biāo)注的情況下,如果對(duì)象標(biāo)注的類(lèi)型里如果包含了 ThisType, 那么 this 類(lèi)型為 T, 這意味著我們可以通過(guò)類(lèi)型計(jì)算為我們的對(duì)象字面量添加字面量里沒(méi)存在的屬性,這對(duì)于 Vue 極其重要。 我們來(lái)看一下 Vue 的 api
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import Vue from 'vue' ; export const Component = Vue.extend({ data(){ return { msg: 'hello' } } methods:{ greet(){ return this .msg + 'world' ; } } }) |
這里的一個(gè)主要問(wèn)題是 greet 是 methods 的方法,其 this 默認(rèn)是 methods 這個(gè)對(duì)象字面量的類(lèi)型,因此無(wú)法從中區(qū)獲取 data 的類(lèi)型,所以主要難題是如何在 methods.greet 里類(lèi)型安全的訪問(wèn)到 data 里的 msg。 借助于泛型推導(dǎo)和 ThisType 可以很輕松的實(shí)現(xiàn),下面讓我們自己實(shí)現(xiàn)一些這個(gè) api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 }, }, }) |
其推導(dǎo)規(guī)則如下 首先根據(jù)對(duì)象字面量的類(lèi)型和泛型約束對(duì)比, 可得到類(lèi)型參數(shù) T 和 M 的實(shí)例化類(lèi)型結(jié)果
1
2
3
4
|
D: { msg: string} M: { greet(): todo } |
接著推導(dǎo) ObjectDescriptor 類(lèi)型為
1
2
3
4
5
6
|
{ data(): { msg: string}, methods: { greet(): string } & ThisType<{msg:string} & {greet(): todo}> } |
接著借助推導(dǎo)出來(lái)的 ObjectDescriptor 推導(dǎo)出 greet 里的 this 類(lèi)型為
1
|
{ msg: string} & { greet(): todo} |
因此推導(dǎo)出 this.msg 類(lèi)型為 string,進(jìn)一步推導(dǎo)出 greet 的類(lèi)型為 string,至此所有類(lèi)型推完。 另外為了減小 Typescript 的類(lèi)型推倒難度,應(yīng)該盡可能的顯示的標(biāo)注類(lèi)型,防止出現(xiàn)循環(huán)推導(dǎo)或者造成推導(dǎo)復(fù)雜度變高等導(dǎo)致編譯速度過(guò)慢甚至出現(xiàn)死循環(huán)或者內(nèi)存耗盡的問(wèn)題。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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 { // 顯示的標(biāo)注返回類(lèi)型,簡(jiǎn)化推導(dǎo) return this .msg + "world" // check }, }, }) |
到此這篇關(guān)于詳解Typescript里的This的使用方法的文章就介紹到這了,更多相關(guān)Typescript This內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://juejin.cn/post/6914853359350448142