dw怎么做網(wǎng)站地圖奶茶店推廣軟文500字
TS 與 JS 的區(qū)別
TypeScript[4] 是一種由微軟開發(fā)的自由和開源的編程語言。它是 JavaScript 的一個(gè)超集,而且本質(zhì)上向這個(gè)語言添加了可選的靜態(tài)類型和基于類的面向?qū)ο缶幊獭?- 官方文檔
說人話就是 TS 拓展了 JS 的一些功能,解決了 JS 的一些缺點(diǎn),可以總結(jié)在下面的表格里,
TypeScript | JavaScript |
---|---|
JavaScript 的超集,用于解決大型項(xiàng)目的代碼復(fù)雜性 | 一種腳本語言,用于創(chuàng)建動態(tài)網(wǎng)頁。 |
強(qiáng)類型,支持靜態(tài)和動態(tài)類型 | 動態(tài)弱類型語言 |
可以在編譯期間發(fā)現(xiàn)并糾正錯誤 | 只能在運(yùn)行時(shí)發(fā)現(xiàn)錯誤 |
不允許改變變量的數(shù)據(jù)類型 | 變量可以被賦予不同類型的值 |
關(guān)于強(qiáng)類型、弱類型、靜態(tài)類型和動態(tài)類型語言,可以看我的這篇文章[5]。
用一張圖來描述一下 TS 和 JS 的關(guān)系,
image.png
JS 有的, TS 都有, JS 沒有的, TS 也有,畢竟 TS 是 JS 的超集嘛。
TS 的缺點(diǎn):
- 不能被瀏覽器理解,需要被編譯成 JS
- 有學(xué)習(xí)成本,寫習(xí)慣了 JS 的我們要上手需要花時(shí)間去理解,而且 TS 中有一些概念還是有點(diǎn)難,比如泛型。
TS 基礎(chǔ)
這一部分的內(nèi)容是比較簡單的,有 JS 基礎(chǔ)的同學(xué)把例子寫一遍就理解了。
基礎(chǔ)類型
boolean、number 和 string 類型
boolean
let isHandsome: boolean = true
復(fù)制代碼
賦值與定義的不一致,會報(bào)錯,靜態(tài)類型語言的優(yōu)勢就體現(xiàn)出來了,可以幫助我們提前發(fā)現(xiàn)代碼中的錯誤。number
let age: number = 18 復(fù)制代碼
string
let realName: string = 'lin' let fullName: string = `A ${realName}` // 支持模板字符串 復(fù)制代碼
undefined 和 null 類型
let u:undefined = undefined // undefined 類型let n:null = null // null 類型復(fù)制代碼
默認(rèn)情況下 null
和 undefined
是所有類型的子類型。就是說你可以把 null 和 undefined 賦值給 number 類型的變量。
let age: number = nulllet realName: string = undefined復(fù)制代碼
但是如果指定了 --strictNullChecks
標(biāo)記,null 和 undefined 只能賦值給 void 和它們各自,不然會報(bào)錯。
image.png
any、unknown 和 void 類型
any
不清楚用什么類型,可以使用 any 類型。這些值可能來自于動態(tài)的內(nèi)容,比如來自用戶輸入或第三方代碼庫
let notSure: any = 4
notSure = "maybe a string" // 可以是 string 類型
notSure = false // 也可以是 boolean 類型
notSure.name // 可以隨便調(diào)用屬性和方法
notSure.getName()
不建議使用 any,不然就喪失了 TS 的意義。
unknown 類型
不建議使用 any,當(dāng)我不知道一個(gè)類型具體是什么時(shí),該怎么辦?
可以使用 unknown
類型
unknown
類型代表任何類型,它的定義和 any
定義很像,但是它是一個(gè)安全類型,使用 unknown
做任何事情都是不合法的。
比如,這樣一個(gè) divide 函數(shù),
function divide(param: any) {return param / 2;
}
把 param 定義為 any 類型,TS 就能編譯通過,沒有把潛在的風(fēng)險(xiǎn)暴露出來,萬一傳的不是 number 類型,不就沒有達(dá)到預(yù)期了嗎。
把 param 定義為 unknown 類型 ,TS 編譯器就能攔住潛在風(fēng)險(xiǎn),如下圖,
function divide(param: unknown) {return param / 2;
}
image.png
因?yàn)椴恢?param 的類型,使用運(yùn)算符 /
,導(dǎo)致報(bào)錯。
再配合類型斷言,即可解決這個(gè)問題,
function divide(param: unknown) {return param as number / 2;
}
void
void
類型與 any
類型相反,它表示沒有任何類型。
比如函數(shù)沒有明確返回值,默認(rèn)返回 Void 類型
function welcome(): void {console.log('hello')
}
never 類型
never
類型表示的是那些永不存在的值的類型。
有些情況下值會永不存在,比如,
- 如果一個(gè)函數(shù)執(zhí)行時(shí)拋出了異常,那么這個(gè)函數(shù)永遠(yuǎn)不存在返回值,因?yàn)閽伋霎惓苯又袛喑绦蜻\(yùn)行。
- 函數(shù)中執(zhí)行無限循環(huán)的代碼,使得程序永遠(yuǎn)無法運(yùn)行到函數(shù)返回值那一步。
// 異常function fn(msg: string): never { throw new Error(msg)}// 死循環(huán) 千萬別這么寫,會內(nèi)存溢出
function fn(): never { while (true) {}}
never 類型是任何類型的子類型,也可以賦值給任何類型。
沒有類型是 never 的子類型,沒有類型可以賦值給 never 類型(除了 never 本身之外)。即使 any
也不可以賦值給 never 。
let test1: never;test1 = 'lin' // 報(bào)錯,Type 'string' is not assignable to type 'never'let test1: never;let test2: any;test1 = test2 // 報(bào)錯,Type 'any' is not assignable to type 'never'
數(shù)組類型
let list: number[] = [1, 2, 3]list.push(4) // 可以調(diào)用數(shù)組上的方法
數(shù)組里的項(xiàng)寫錯類型會報(bào)錯
image.png
push 時(shí)類型對不上會報(bào)錯
image.png
如果數(shù)組想每一項(xiàng)放入不同數(shù)據(jù)怎么辦?用元組類型
元組類型
元組類型允許表示一個(gè)已知元素?cái)?shù)量和類型的數(shù)組,各元素的類型不必相同。
let tuple: [number, string] = [18, 'lin']
寫錯類型會報(bào)錯:
image.png
越界會報(bào)錯:
image.png
可以對元組使用數(shù)組的方法,比如使用 push 時(shí),不會有越界報(bào)錯
let tuple: [number, string] = [18, 'lin']tuple.push(100) // 但是只能 push 定義的 number 或者 string 類型
push 一個(gè)沒有定義的類型,報(bào)錯
函數(shù)類型
TS 定義函數(shù)類型需要定義輸入?yún)?shù)類型和輸出類型。
輸出類型也可以忽略,因?yàn)?TS 能夠根據(jù)返回語句自動推斷出返回值類型。
function add(x:number, y:number):number {return x + y
}
add(1,2)
函數(shù)沒有明確返回值,默認(rèn)返回 Void 類型
function welcome(): void {console.log('hello');
}
函數(shù)表達(dá)式寫法
let add2 = (x: number, y: number): number => {return x + y
}
可選參數(shù)
參數(shù)后加個(gè)問號,代表這個(gè)參數(shù)是可選的
function add(x:number, y:number, z?:number):number {return x + y
}
add(1,2,3)
add(1,2)
注意可選參數(shù)要放在函數(shù)入?yún)⒌淖詈竺?#xff0c;不然會導(dǎo)致編譯錯誤。
image.png
默認(rèn)參數(shù)
function add(x:number, y:number = 100):number {return x + y
}
add(100) // 200
跟 JS 的寫法一樣,在入?yún)⒗锒x初始值。
和可選參數(shù)不同的是,默認(rèn)參數(shù)可以不放在函數(shù)入?yún)⒌淖詈竺?#xff0c;
function add(x:number = 100, y:number):number {return x + y
}
add(100)
看上面的代碼,add 函數(shù)只傳了一個(gè)參數(shù),如果理所當(dāng)然地覺得 x 有默認(rèn)值,只傳一個(gè)就傳的是 y 的話,就會報(bào)錯,
image.png
編譯器會判定你只傳了 x,沒傳 y。
如果帶默認(rèn)值的參數(shù)不是最后一個(gè)參數(shù),用戶必須明確的傳入 undefined
值來獲得默認(rèn)值。
add(undefined,100) // 200
函數(shù)賦值
JS 中變量隨便賦值沒問題,
image.png
但在 TS 中函數(shù)不能隨便賦值,會報(bào)錯的,
image.png
也可以用下面這種方式定義一個(gè)函數(shù) add3,把 add2 賦值給 add3
let add2 = (x: number, y: number): number => {return x + y
}
const add3:(x: number, y: number) => number = add2
有點(diǎn)像 es6 中的箭頭函數(shù),但不是箭頭函數(shù),TS 遇到 :
就知道后面的代碼是寫類型用的。
當(dāng)然,不用定義 add3 類型直接賦值也可以,TS 會在變量賦值的過程中,自動推斷類型,如下圖:
image.png
interface
基本概念
interface
(接口) 是 TS 設(shè)計(jì)出來用于定義對象類型的,可以對對象的形狀進(jìn)行描述。
定義 interface 一般首字母大寫,代碼如下:
interface Person {name: stringage: number
}
const p1: Person = {name: 'lin',age: 18
}
屬性必須和類型定義的時(shí)候完全一致。
少寫了屬性,報(bào)錯:
image.png
多寫了屬性,報(bào)錯:
image.png
類型提示,顯著提升開發(fā)效率:
image.png
注意:interface 不是 JS 中的關(guān)鍵字,所以 TS 編譯成 JS 之后,這些 interface 是不會被轉(zhuǎn)換過去的,都會被刪除掉,interface 只是在 TS 中用來做靜態(tài)檢查。
可選屬性
跟函數(shù)的可選參數(shù)是類似的,在屬性上加個(gè) ?
,這個(gè)屬性就是可選的,比如下面的 age 屬性
interface Person {name: stringage?: number
}
const p1: Person = {name: 'lin',
}
只讀屬性
如果希望某個(gè)屬性不被改變,可以這么寫:
interface Person {readonly id: numbername: stringage: number}
改變這個(gè)只讀屬性時(shí)會報(bào)錯。
image.png
interface 描述函數(shù)類型
interface 也可以用來描述函數(shù)類型,代碼如下:
interface ISum {(x:number,y:number):number}const add:ISum = (num1, num2) => {return num1 + num2
}
自定義屬性(可索引的類型)
上文中,屬性必須和類型定義的時(shí)候完全一致,如果一個(gè)對象上有多個(gè)不確定的屬性,怎么辦?
可以這么寫。
interface RandomKey {[propName: string]: string}
const obj: RandomKey = {a: 'hello',b: 'lin',c: 'welcome',
}
如果把屬性名定義為 number 類型,就是一個(gè)類數(shù)組了,看上去和數(shù)組一模一樣。
interface LikeArray {[propName: number]: string
}
const arr: LikeArray = ['hello', 'lin']
arr[0] // 可以使用下標(biāo)來訪問值
當(dāng)然,不是真的數(shù)組,數(shù)組上的方法它是沒有的。
image.png
duck typing(鴨子類型[6])
看到這里,你會發(fā)現(xiàn),interface 的寫法非常靈活,它不是教條主義。
用 interface 可以創(chuàng)造一系列自定義的類型。
事實(shí)上, interface 還有一個(gè)響亮的名稱:duck typing
(鴨子類型)。
當(dāng)看到一只鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那么這只鳥就可以被稱為鴨子。
– James Whitcomb Riley[7]
這句話完美地詮釋了 interface 的含義,只要數(shù)據(jù)滿足了 interface 定義的類型,TS 就可以編譯通過。
舉個(gè)例子:
interface FunctionWithProps {(x: number): numberfnName: string}
FunctionWithProps 接口描述了一個(gè)函數(shù)類型,還向這個(gè)函數(shù)類型添加了 name 屬性,這看上去完全是四不像,但是這個(gè)定義是完全可以工作的。
const fn: FunctionWithProps = (x) => {return x
}fn.fnName = 'hello world'
這就是 duck typing 和 interface,非常的靈活。
類
我們知道, JS 是靠原型和原型鏈來實(shí)現(xiàn)面向?qū)ο缶幊痰?#xff0c;es6 新增了語法糖 class。
TS 通過 public
、private
、protected
三個(gè)修飾符來增強(qiáng)了 JS 中的類。
在 TS 中,寫法和 JS 差不多,只是要定義一些類型而已,我們通過下面幾個(gè)例子來復(fù)習(xí)一下類的封裝、繼承和多態(tài)。
基本寫法
定義一個(gè) Person 類,有屬性 name 和 方法 speak
class Person {name: stringconstructor(name: string) {this.name = name}speak() {console.log(`${this.name} is speaking`)}
}
const p1 = new Person('lin') // 新建實(shí)例
p1.name // 訪問屬性和方法
p1.speak()
繼承
使用 extends 關(guān)鍵字實(shí)現(xiàn)繼承,定義一個(gè) Student 類繼承自 Person 類。
class Student extends Person {study() {console.log(`${this.name} needs study`)}
}
const s1 = new Student('lin')
s1.study()
繼承之后,Student 類上的實(shí)例可以訪問 Person 類上的屬性和方法。
image.png
super關(guān)鍵字
注意,上例中 Student 類沒有定義自己的屬性,可以不寫 super ,但是如果 Student 類有自己的屬性,就要用到 super 關(guān)鍵字來把父類的屬性繼承過來。
比如,Student 類新增一個(gè) grade(成績) 屬性,就要這么寫:
class Student extends Person {grade: numberconstructor(name: string,grade:number) {super(name)this.grade = grade}
}
const s1 = new Student('lin', 100)
不寫 super 會報(bào)錯。
image.png
多態(tài)
子類對父類的方法進(jìn)行了重寫,子類和父類調(diào)同一個(gè)方法時(shí)會不一樣。
class Student extends Person {speak() {return `Student ${super.speak()}`}}
TS 中一般對抽象方法實(shí)現(xiàn)多態(tài),詳細(xì)見后文抽象類。
public
public
,公有的,一個(gè)類里默認(rèn)所有的方法和屬性都是 public。
比如上文中定義的 Person 類,其實(shí)是這樣的:
class Person {public name: stringpublic constructor(name: string) {this.name = name}public speak() {console.log(`${this.name} is speaking`)}
}復(fù)制代碼
public 可寫可不寫,不寫默認(rèn)也是 public。
private
private
,私有的,只屬于這個(gè)類自己,它的實(shí)例和繼承它的子類都訪問不到。
將 Person 類的 name 屬性改為 private。
class Person {private name: stringpublic constructor(name: string) {this.name = name}public speak() {console.log(`${this.name} is speaking`)}
}
實(shí)例訪問 name 屬性,會報(bào)錯:
image.png
繼承它的子類 訪問 name 屬性,會報(bào)錯:
image.png
protected
protected
受保護(hù)的,繼承它的子類可以訪問,實(shí)例不能訪問。
將 Person 類的 name 屬性改為 protected。
class Person {protected name: stringpublic constructor(name: string) {this.name = name}public speak() {console.log(`${this.name} is speaking`)}
}
實(shí)例訪問 name 屬性,會報(bào)錯:
image.png
子類可以訪問。
class Studeng extends Person {study() {console.log(`${this.name} needs study`)}
}
static
static
是靜態(tài)屬性,可以理解為是類上的一些常量,實(shí)例不能訪問。
比如一個(gè) Circle 類,圓周率是 3.14,可以直接定義一個(gè)靜態(tài)屬性。
class Circle {static pi = 3.14public radius: numberpublic constructor(radius: number) {this.radius = radius}public calcLength() {return Circle.pi * this.radius * 2 // 計(jì)算周長,直接訪問 Circle.pi}
}
實(shí)例訪問,會報(bào)錯:
image.png
抽象類
抽象類,聽名字似乎是非常難理解的概念,但其實(shí)非常簡單。
TS 通過 public
、private
、protected
三個(gè)修飾符來增強(qiáng)了 JS 中的類。
其實(shí) TS 還對 JS 擴(kuò)展了一個(gè)新概念——抽象類
。
所謂抽象類,是指只能被繼承,但不能被實(shí)例化的類,就這么簡單。
抽象類有兩個(gè)特點(diǎn):
- 抽象類不允許被實(shí)例化
- 抽象類中的抽象方法必須被子類實(shí)現(xiàn)
抽象類用一個(gè) abstract
關(guān)鍵字來定義,我們通過兩個(gè)例子來感受一下抽象類的兩個(gè)特點(diǎn)。
抽象類不允許被實(shí)例化
abstract class Animal {}
const a = new Animal()
定義一個(gè)抽象類 Animal,初始化一個(gè) Animal 的實(shí)例,直接報(bào)錯,
image.png
抽象類中的抽象方法必須被子類實(shí)現(xiàn)
abstract class Animal {constructor(name:string) {this.name = name}public name: stringpublic abstract sayHi():void
}class Dog extends Animal {constructor(name:string) {super(name)}
}復(fù)制代碼
定義一個(gè) Dog 類,繼承自 Animal 類,但是卻沒有實(shí)現(xiàn) Animal 類上的抽象方法 sayHi
,報(bào)錯,
image.png
正確的用法如下,
abstract class Animal {constructor(name:string) {this.name = name}public name: stringpublic abstract sayHi():void}class Dog extends Animal {constructor(name:string) {super(name)}public sayHi() {console.log('wang')}
}復(fù)制代碼
為什么叫抽象類?
很顯然,抽象類是一個(gè)廣泛和抽象的概念,不是一個(gè)實(shí)體,就比如上文的例子,動物這個(gè)概念是很廣泛的,貓、狗、獅子都是動物,但動物卻不好是一個(gè)實(shí)例,實(shí)例只能是貓、狗或者獅子。
官方一點(diǎn)的說法是,在面向?qū)ο蟮母拍钪?#xff0c;所有的對象都是通過類來描繪的,但是反過來,并不是所有的類都是用來描繪對象的,如果一個(gè)類中沒有包含足夠的信息來描繪一個(gè)具體的對象,這樣的類就是抽象類。
比如 Animal 類只是具有動物都有的一些屬性和方法,但不會具體到包含貓或者狗的屬性和方法。
所以抽象類的用法是用來定義一個(gè)基類,聲明共有屬性和方法,拿去被繼承。
抽象類的好處是可以抽離出事物的共性,有利于代碼的復(fù)用。
抽象方法和多態(tài)
多態(tài)是面向?qū)ο蟮娜蠡咎卣髦弧?/p>
多態(tài)指的是,父類定義一個(gè)抽象方法,在多個(gè)子類中有不同的實(shí)現(xiàn),運(yùn)行的時(shí)候不同的子類就對應(yīng)不同的操作,比如,
abstract class Animal {constructor(name:string) {this.name = name}public name: stringpublic abstract sayHi():void}class Dog extends Animal {constructor(name:string) {super(name)}public sayHi() {console.log('wang')}}class Cat extends Animal {constructor(name:string) {super(name)}public sayHi() {console.log('miao')}}復(fù)制代碼
Dog 類和 Cat 類都繼承自 Animal 類,Dog 類和 Cat 類都不同的實(shí)現(xiàn)了 sayHi 這個(gè)方法。
interface 和 class 的關(guān)系
上文中我們說過,interface 是 TS 設(shè)計(jì)出來用于定義對象類型的,可以對對象的形狀進(jìn)行描述。
interface 同樣可以用來約束 class,要實(shí)現(xiàn)約束,需要用到 implements
關(guān)鍵字。
implements
implements 是實(shí)現(xiàn)的意思,class 實(shí)現(xiàn) interface。
比如手機(jī)有播放音樂的功能,可以這么寫:
interface MusicInterface {playMusic(): void}class Cellphone implements MusicInterface {playMusic() {}}復(fù)制代碼
定義了約束后,class 必須要滿足接口上的所有條件。
如果 Cellphone 類上不寫 playMusic 方法,會報(bào)錯。
image.png
處理公共的屬性和方法
不同的類有一些共同的屬性和方法,使用繼承很難完成。
比如汽車(Car 類)也有播放音樂的功能,你可以這么做:
- 用 Car 類繼承 Cellphone 類
- 找一個(gè) Car 類和 Cellphone 類的父類,父類有播放音樂的方法,他們倆繼承這個(gè)父類
很顯然這兩種方法都不合常理。
實(shí)際上,使用 implements,問題就會迎刃而解。
interface MusicInterface {playMusic(): void}class Car implements MusicInterface {playMusic() {}}class Cellphone implements MusicInterface {playMusic() {}}復(fù)制代碼
這樣 Car 類和 Cellphone 類都約束了播放音樂的功能。
再比如,手機(jī)還有打電話的功能,就可以這么做,Cellphone 類 implements 兩個(gè) interface。
interface MusicInterface {playMusic(): void}interface CallInterface {makePhoneCall(): void}class Cellphone implements MusicInterface, CallInterface {playMusic() {}makePhoneCall() {}}復(fù)制代碼
這個(gè) CallInterface 也可以用于 iPad 類、手表類上面,畢竟他們也能打電話。
interface 來約束 class,只要 class 實(shí)現(xiàn)了 interface 規(guī)定的屬性或方法,就行了,沒有繼承那么多條條框框,非常靈活。
約束構(gòu)造函數(shù)和靜態(tài)屬性
使用 implements 只能約束類實(shí)例上的屬性和方法,要約束構(gòu)造函數(shù)和靜態(tài)屬性,需要這么寫。
以我們上文提過的 Circl 類為例:
interface CircleStatic {new (radius: number): voidpi: number}const Circle:CircleStatic = class Circle {static pi: 3.14public radius: numberpublic constructor(radius: number) {this.radius = radius}}復(fù)制代碼
未定義靜態(tài)屬性 pi,會報(bào)錯:
image.png
constructor 入?yún)㈩愋筒粚?#xff0c;會報(bào)錯:
image.png
枚舉
在任何項(xiàng)目開發(fā)中,我們都會遇到定義常量的情況,常量就是指不會被改變的值。
TS 中我們使用 const
來聲明常量,但是有些取值是在一定范圍內(nèi)的一系列常量,比如一周有七天,比如方向分為上下左右四個(gè)方向。
這時(shí)就可以使用枚舉(Enum)來定義。
基本使用
enum Direction {Up,Down,Left,Right}復(fù)制代碼
這樣就定義了一個(gè)數(shù)字枚舉,他有兩個(gè)特點(diǎn):
- 數(shù)字遞增
- 反向映射
枚舉成員會被賦值為從 0
開始遞增的數(shù)字,
console.log(Direction.Up) // 0console.log(Direction.Down) // 1console.log(Direction.Left) // 2console.log(Direction.Right) // 3復(fù)制代碼
枚舉會對枚舉值到枚舉名進(jìn)行反向映射,
console.log(Direction[0]) // Upconsole.log(Direction[1]) // Downconsole.log(Direction[2]) // Leftconsole.log(Direction[3]) // Right復(fù)制代碼
如果枚舉第一個(gè)元素賦有初始值,就會從初始值開始遞增,
enum Direction {Up = 6,Down,Left,Right}console.log(Direction.Up) // 6console.log(Direction.Down) // 7console.log(Direction.Left) // 8console.log(Direction.Right) // 9復(fù)制代碼
反向映射的原理
枚舉是如何做到反向映射的呢,我們不妨來看一下被編譯后的代碼,
var Direction;(function (Direction) {Direction[Direction["Up"] = 6] = "Up";Direction[Direction["Down"] = 7] = "Down";Direction[Direction["Left"] = 8] = "Left";Direction[Direction["Right"] = 9] = "Right";})(Direction || (Direction = {}));復(fù)制代碼
主體代碼是被包裹在一個(gè)自執(zhí)行函數(shù)里,封裝了自己獨(dú)特的作用域。
Direction["Up"] = 6復(fù)制代碼
會將 Direction 這個(gè)對象的 Up 屬性賦值為 6,JS 的賦值運(yùn)算符返回的值是被賦予的值。
Direction["Up"] = 6 返回 6復(fù)制代碼
執(zhí)行 Direction[Direction["Up"] = 6] = "Up";相當(dāng)于執(zhí)行Direction["Up"] = 6Direction[6] = "Up"復(fù)制代碼
這樣就實(shí)現(xiàn)了枚舉的反向映射。
手動賦值
定義一個(gè)枚舉來管理外賣狀態(tài),分別有已下單,配送中,已接收三個(gè)狀態(tài)。
可以這么寫,
enum ItemStatus {Buy = 1,Send,Receive}console.log(ItemStatus['Buy']) // 1console.log(ItemStatus['Send']) // 2console.log(ItemStatus['Receive']) // 3復(fù)制代碼
但有時(shí)候后端給你返回的數(shù)據(jù)狀態(tài)是亂的,就需要我們手動賦值。
比如后端說 Buy 是 100,Send 是 20,Receive 是 1,就可以這么寫,
enum ItemStatus {Buy = 100,Send = 20,Receive = 1}console.log(ItemStatus['Buy']) // 100console.log(ItemStatus['Send']) // 20console.log(ItemStatus['Receive']) // 1復(fù)制代碼
別問為什么,實(shí)際開發(fā)中經(jīng)常會有這種情況發(fā)生。
計(jì)算成員
枚舉中的成員可以被計(jì)算,比如經(jīng)典的使用位運(yùn)算合并權(quán)限,可以這么寫,
enum FileAccess {Read = 1 << 1,Write = 1 << 2,ReadWrite = Read | Write,}console.log(FileAccess.Read) // 2 -> 010console.log(FileAccess.Write) // 4 -> 100console.log(FileAccess.ReadWrite) // 6 -> 110復(fù)制代碼
看個(gè)實(shí)例吧,Vue3 源碼中的 patchFlags,用于標(biāo)識節(jié)點(diǎn)更新的屬性。
// packages/shared/src/patchFlags.tsexport const enum PatchFlags {TEXT = 1, // 動態(tài)文本節(jié)點(diǎn)CLASS = 1 << 1, // 動態(tài) classSTYLE = 1 << 2, // 動態(tài) stylePROPS = 1 << 3, // 動態(tài)屬性FULL_PROPS = 1 << 4, // 具有動態(tài) key 屬性,當(dāng) key 改變時(shí),需要進(jìn)行完整的 diff 比較HYDRATE_EVENTS = 1 << 5, // 具有監(jiān)聽事件的節(jié)點(diǎn)STABLE_FRAGMENT = 1 << 6, // 子節(jié)點(diǎn)順序不會被改變的 fragmentKEYED_FRAGMENT = 1 << 7, // 帶有 key 屬或部分子節(jié)點(diǎn)有 key 的 fragmentUNKEYED_FRAGMENT = 1 << 8, // 子節(jié)點(diǎn)沒有 key 的 fragmentNEED_PATCH = 1 << 9, // 非 props 的比較,比如 ref 或指令DYNAMIC_SLOTS = 1 << 10, // 動態(tài)插槽DEV_ROOT_FRAGMENT = 1 << 11, // 僅供開發(fā)時(shí)使用,表示將注釋放在模板根級別的片段HOISTED = -1, // 靜態(tài)節(jié)點(diǎn)BAIL = -2 // diff 算法要退出優(yōu)化模式}復(fù)制代碼
字符串枚舉
字符串枚舉的意義在于,提供有具體語義的字符串,可以更容易地理解代碼和調(diào)試。
enum Direction {Up = "UP",Down = "DOWN",Left = "LEFT",Right = "RIGHT",}const value = 'UP'if (value === Direction.Up) {// do something}復(fù)制代碼
常量枚舉
上文的例子,使用 const 來定義一個(gè)常量枚舉
const enum Direction {Up = "UP",Down = "DOWN",Left = "LEFT",Right = "RIGHT",}const value = 'UP'if (value === Direction.Up) {// do something}復(fù)制代碼
編譯出來的 JS 代碼會簡潔很多,提高了性能。
const value = 'UP';if (value === 'UP' /* Up */) {// do something}復(fù)制代碼
不寫 const 編譯出來是這樣的,
var Direction;(function (Direction) {Direction["Up"] = "UP";Direction["Down"] = "DOWN";Direction["Left"] = "LEFT";Direction["Right"] = "RIGHT";})(Direction || (Direction = {}));const value = 'UP';if (value === Direction.Up) {// do something}復(fù)制代碼
這一堆定義枚舉的邏輯會在編譯階段會被刪除,常量枚舉成員在使用的地方被內(nèi)聯(lián)進(jìn)去。
很顯然,常量枚舉不允許包含計(jì)算成員,不然怎么叫常量呢?
const enum Test {A = "lin".length}復(fù)制代碼
這么寫直接報(bào)錯:
image.png
總結(jié)一下,常量枚舉可以避免在額外生成的代碼上的開銷和額外的非直接的對枚舉成員的訪問。
小結(jié)
枚舉的意義在于,可以定義一些帶名字的常量集合,清晰地表達(dá)意圖和語義,更容易地理解代碼和調(diào)試。
常用于和后端聯(lián)調(diào)時(shí),區(qū)分后端返回的一些代表狀態(tài)語義的數(shù)字或字符串,降低閱讀代碼時(shí)的心智負(fù)擔(dān)。
類型推論
TypeScript里,在有些沒有明確指出類型的地方,類型推論會幫助提供類型。
這種推斷發(fā)生在初始化變量和成員,設(shè)置默認(rèn)參數(shù)值和決定函數(shù)返回值時(shí)。
定義時(shí)不賦值
let aa = 18a = 'lin'復(fù)制代碼
定義時(shí)不賦值,就會被 TS 自動推導(dǎo)成 any 類型,之后隨便怎么賦值都不會報(bào)錯。
image.png
初始化變量
例如:
let userName = 'lin'復(fù)制代碼
image.png
因?yàn)橘x值的時(shí)候賦的是一個(gè)字符串類型,所以 TS 自動推導(dǎo)出 userName 是 string 類型。
這個(gè)時(shí)候,再更改 userName 時(shí),就必須是 string 類型,是其他類型就報(bào)錯,比如:
image.png
設(shè)置默認(rèn)參數(shù)值
函數(shù)設(shè)置默認(rèn)參數(shù)時(shí),也會有自動推導(dǎo)
比如,定義一個(gè)打印年齡的函數(shù),默認(rèn)值是 18
function printAge(num = 18) {console.log(num)return num}復(fù)制代碼
那么 TS 會自動推導(dǎo)出 printAge 的入?yún)㈩愋?#xff0c;傳錯了類型會報(bào)錯。
image.png
決定函數(shù)返回值
決定函數(shù)返回值時(shí), TS 也會自動推導(dǎo)出返回值類型。
比如一個(gè)函數(shù)不寫返回值,
function welcome() {console.log('hello')}復(fù)制代碼
TS 自動推導(dǎo)出返回值是 void 類型
再比如上文的 printAge 函數(shù),TS 會自動推導(dǎo)出返回值是 number 類型。
如果我們給 printAge 函數(shù)的返回值定義為 string 類型,看看會發(fā)生什么。
function printAge(num = 18) {console.log(num)return num}interface PrintAge {(num: number): string}const printAge1: PrintAge = printAge復(fù)制代碼
很顯然,定義的類型和 TS 自動推導(dǎo)出的類型沖突,報(bào)錯:
image.png
最佳通用類型
當(dāng)需要從幾個(gè)表達(dá)式中推斷類型時(shí)候,會使用這些表達(dá)式的類型來推斷出一個(gè)最合適的通用類型。比如,
let arr = [0, 1, null, 'lin'];復(fù)制代碼
image.png
又比如:
let pets = [new Dog(), new Cat()]復(fù)制代碼
image.png
雖然 TS 可以推導(dǎo)出最合適的類型,但最好還是在寫的時(shí)候就定義好類型,上文的例子,我們可以這么寫:
type arrItem = number | string | nulllet arr: arrItem[] = [0, 1, null, 'lin'];let pets: Pets[] = [new Dog(), new Cat()]復(fù)制代碼
小結(jié)
類型推論雖然能為我們提供幫助,但既然寫了 TS,除非是函數(shù)默認(rèn)返回類型為 void 這種大家都知道的,其他的最好每個(gè)地方都定義好類型。
內(nèi)置類型
JavaScript 中有很多內(nèi)置對象[8],它們可以直接在 TypeScript 中當(dāng)做定義好了的類型。
內(nèi)置對象是指根據(jù)標(biāo)準(zhǔn)在全局作用域 global
上存在的對象,這里的標(biāo)準(zhǔn)指的是 ECMAcript
和其他環(huán)境(比如DOM)的標(biāo)準(zhǔn)。
JS 八種內(nèi)置類型
let name: string = "lin";let age: number = 18;let isHandsome: boolean = true;let u: undefined = undefined;let n: null = null;let obj: object = {name: 'lin', age: 18};let big: bigint = 100n;let sym: symbol = Symbol("lin"); 復(fù)制代碼
ECMAScript 的內(nèi)置對象
比如,Array
、Date
、Error
等,
const nums: Array<number> = [1,2,3]const date: Date = new Date()const err: Error = new Error('Error!');const reg: RegExp = /abc/;Math.pow(2, 9)復(fù)制代碼
以 Array
為例,按住 comand/ctrl
,再鼠標(biāo)左鍵點(diǎn)擊一下,就能跳轉(zhuǎn)到類型聲明的地方。
image.png
可以看到,Array 這個(gè)類型是用 interface 定義的,有多個(gè)不同版本的 .d.ts
文件聲明了這個(gè)類型。
在 TS 中,重復(fù)聲明一個(gè) interface,會把所有的聲明全部合并,這里所有的 .d.ts
文件合并出來的 Array 接口,就組合成了 Array 內(nèi)置類型的全部屬性和功能。
再舉個(gè)例子
DOM 和 BOM
比如 HTMLElement
、NodeList
、MouseEvent
等
let body: HTMLElement = document.bodylet allDiv: NodeList = document.querySelectorAll('div');document.addEventListener('click', (e: MouseEvent) => {e.preventDefault()// Do something});復(fù)制代碼
TS 核心庫的定義文件
TypeScript 核心庫的定義文件[9]中定義了所有瀏覽器環(huán)境需要用到的類型,并且是預(yù)置在 TypeScript 中的。
比如 Math.pow
的類型定義如下,
interface Math {/*** Returns the value of a base expression taken to a specified power.* @param x The base value of the expression.* @param y The exponent value of the expression.*/pow(x: number, y: number): number;}復(fù)制代碼
又比如,addEventListener
的類型定義如下,
interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {addEventListener(type: string, listener: (ev: MouseEvent) => any, useCapture?: boolean): void;}復(fù)制代碼
淺嘗輒止,知道在哪里定義就行,真要去分析一些Web Api 的類型實(shí)現(xiàn),是很費(fèi)精力的。
TS 進(jìn)階
這一部分的內(nèi)容就需要費(fèi)點(diǎn)腦細(xì)胞了,畢竟學(xué)習(xí)一門語言,還是沒那么容易的,最好把基礎(chǔ)的內(nèi)容都理解透徹之后再來學(xué)進(jìn)階。
高級類型
高級類型分一和二兩部分,一的部分不需要理解泛型也能理解,二的部分需要理解泛型之后才能理解,所以二被拆分到后面去了。
聯(lián)合類型
如果希望一個(gè)變量可以支持多種類型,就可以用聯(lián)合類型(union types)來定義。
例如,一個(gè)變量既支持 number 類型,又支持 string 類型,就可以這么寫:
let num: number | string
num = 8
num = 'eight'
聯(lián)合類型大大提高了類型的可擴(kuò)展性,但當(dāng) TS 不確定一個(gè)聯(lián)合類型的變量到底是哪個(gè)類型的時(shí)候,只能訪問他們共有的屬性和方法。
比如這里就只能訪問 number 類型和 string 類型共有的方法,如下圖,
image.png
如果直接訪問 length
屬性,string 類型上有,number 類型上沒有,就報(bào)錯了,
image.png
交叉類型
如果要對對象形狀進(jìn)行擴(kuò)展,可以使用交叉類型 &
。
比如 Person 有 name 和 age 的屬性,而 Student 在 name 和 age 的基礎(chǔ)上還有 grade 屬性,就可以這么寫,
interface Person {name: stringage: number
}type Student = Person & { grade: number }
這和類的繼承是一模一樣的,這樣 Student 就繼承了 Person 上的屬性,
image.png
聯(lián)合類型 |
是指可以取幾種類型中的任意一種,而交叉類型 &
是指把幾種類型合并起來。
交叉類型和 interface 的 extends 非常類似,都是為了實(shí)現(xiàn)對象形狀的組合和擴(kuò)展。
類型別名(type)
類型別名(type aliase),聽名字就很好理解,就是給類型起個(gè)別名。
就像 NBA 球員 揚(yáng)尼斯-阿德托昆博,名字太長難記,我們叫他字母哥
。
就像我們項(xiàng)目中配置 alias
,不用寫相對路徑就能很方便地引入文件
import componentA from '../../../../components/componentA/index.vue'import componentA from '@/components/componentA/index.vue
類型別名用 type
關(guān)鍵字來書寫,有了類型別名,我們書寫 TS 的時(shí)候可以更加方便簡潔。
比如下面這個(gè)例子,getName
這個(gè)函數(shù)接收的參數(shù)可能是字符串,可能是函數(shù),就可以這么寫。
type Name = string
type NameResolver = () => string
type NameOrResolver = Name | NameResolver // 聯(lián)合類型
function getName(n: NameOrResolver): Name {if (typeof n === 'string') {return n}else {return n()}
}
這樣調(diào)用時(shí)傳字符串和函數(shù)都可以。
getName('lin')
getName(() => 'lin')
如果傳的格式有問題,就會提示。
image.png
image.png
類型別名會給一個(gè)類型起個(gè)新名字。類型別名有時(shí)和接口很像,但是可以作用于原始值,聯(lián)合類型,元組以及其它任何你需要手寫的類型。-- TS 文檔
類型別名的用法如下,
type Name = string // 基本類型type arrItem = number | string // 聯(lián)合類型
const arr: arrItem[] = [1,'2', 3]
type Person = { name: Name
}
type Student = Person & { grade: number } // 交叉類型type Teacher = Person & { major: string } type StudentAndTeacherList = [Student, Teacher] // 元組類型
const list:StudentAndTeacherList = [{ name: 'lin', grade: 100 }, { name: 'liu', major: 'Chinese' }
]
type 和 interface 的區(qū)別
比如下面這個(gè)例子,可以用 type,也可以用 interface。
interface Person {name: stringage: number}const person: Person = {name: 'lin',age: 18}復(fù)制代碼
type Person = {name: stringage: number}const person: Person = {name: 'lin',age: 18}復(fù)制代碼
那 type 和 interface 難道都可以隨便用,總得有個(gè)區(qū)別吧。
兩者相同點(diǎn):
- 都可以定義一個(gè)對象或函數(shù)
- 都允許繼承
都可以定義一個(gè)對象或函數(shù)
定義對象上文已經(jīng)說了,我們來看一下如何定義函數(shù)。
type addType = (num1:number,num2:number) => numberinterface addType {(num1:number,num2:number):number}// 這兩種寫法都可以定義函數(shù)類型復(fù)制代碼
const add:addType = (num1, num2) => {return num1 + num2}復(fù)制代碼
都允許繼承
我們定義一個(gè) Person 類型和 Student 類型,Student 繼承自 Person,可以有下面四種方式
// interface 繼承 interfaceinterface Person { name: string }interface Student extends Person { grade: number }復(fù)制代碼
const person:Student = {name: 'lin',grade: 100}復(fù)制代碼
// type 繼承 typetype Person = { name: string }type Student = Person & { grade: number } // 用交叉類型復(fù)制代碼
// interface 繼承 typetype Person = { name: string }interface Student extends Person { grade: number }復(fù)制代碼
// type 繼承 interfaceinterface Person { name: string }type Student = Person & { grade: number } // 用交叉類型復(fù)制代碼
interface 使用 extends 實(shí)現(xiàn)繼承, type 使用交叉類型實(shí)現(xiàn)繼承
兩者不同點(diǎn):
- interface(接口) 是 TS 設(shè)計(jì)出來用于定義對象類型的,可以對對象的形狀進(jìn)行描述。
- type 是類型別名,用于給各種類型定義別名,讓 TS 寫起來更簡潔、清晰。
- type 可以聲明基本類型、聯(lián)合類型、交叉類型、元組,interface 不行
- interface可以合并重復(fù)聲明,type 不行
合并重復(fù)聲明:
interface Person {name: string}interface Person { // 重復(fù)聲明 interface,就合并了age: number}const person: Person = {name: 'lin',age: 18}復(fù)制代碼
重復(fù)聲明 type ,就報(bào)錯了
type Person = {name: string}type Person = { // Duplicate identifier 'Person'age: number}const person: Person = {name: 'lin',age: 18}復(fù)制代碼
image.png
這兩者的區(qū)別說了這么多,其實(shí)本不該把這兩個(gè)東西拿來做對比,他們倆是完全不同的概念。
interface 是接口,用于描述一個(gè)對象。
type 是類型別名,用于給各種類型定義別名,讓 TS 寫起來更簡潔、清晰。
只是有時(shí)候兩者都能實(shí)現(xiàn)同樣的功能,才會經(jīng)常被混淆
平時(shí)開發(fā)中,一般使用組合或者交叉類型的時(shí)候,用 type。
一般要用類的 extends 或 implements 時(shí),用 interface。
其他情況,比如定義一個(gè)對象或者函數(shù),就看你心情了。
類型保護(hù)
如果有一個(gè) getLength
函數(shù),入?yún)⑹锹?lián)合類型 number | string
,返回入?yún)⒌?length,
function getLength(arg: number | string): number {return arg.length}復(fù)制代碼
從上文可知,這么寫會報(bào)錯,因?yàn)?number 類型上沒有 length 屬性。
image.png
這個(gè)時(shí)候,類型保護(hù)(Type Guards)出現(xiàn)了,可以使用 typeof
關(guān)鍵字判斷變量的類型。
我們把 getLength 方法改造一下,就可以精準(zhǔn)地獲取到 string 類型的 length 屬性了,
function getLength(arg: number | string): number {if(typeof arg === 'string') {return arg.length} else {return arg.toString().length}}復(fù)制代碼
之所以叫類型保護(hù),就是為了能夠在不同的分支條件中縮小范圍,這樣我們代碼出錯的幾率就大大降低了。
類型斷言
上文的例子也可以使用類型斷言來解決。
類型斷言語法:
值 as 類型復(fù)制代碼
使用類型斷言來告訴 TS,我(開發(fā)者)比你(編譯器)更清楚這個(gè)參數(shù)是什么類型,你就別給我報(bào)錯了,
function getLength(arg: number | string): number {const str = arg as stringif (str.length) {return str.length} else {const number = arg as numberreturn number.toString().length}}復(fù)制代碼
注意,類型斷言不是類型轉(zhuǎn)換,把一個(gè)類型斷言成聯(lián)合類型中不存在的類型會報(bào)錯。
比如,
function getLength(arg: number | string): number {return (arg as number[]).length}復(fù)制代碼
image.png
字面量類型
有時(shí)候,我們需要定義一些常量,就需要用到字面量類型,比如,
type ButtonSize = 'mini' | 'small' | 'normal' | 'large'type Sex = '男' | '女'復(fù)制代碼
這樣就只能從這些定義的常量中取值,亂取值會報(bào)錯,
image.png
泛型
泛型,是 TS 比較難理解的部分,拿下了泛型,對 TS 的理解就又上了一個(gè)臺階,對后續(xù)深入學(xué)習(xí)幫助很大。
為什么需要泛型?
如果你看過 TS 文檔,一定看過這樣兩段話:
軟件工程中,我們不僅要創(chuàng)建一致的定義良好的 API,同時(shí)也要考慮可重用性。組件不僅能夠支持當(dāng)前的數(shù)據(jù)類型,同時(shí)也能支持未來的數(shù)據(jù)類型,這在創(chuàng)建大型系統(tǒng)時(shí)為你提供了十分靈活的功能。
在像 C# 和 Java 這樣的語言中,可以使用泛型來創(chuàng)建可重用的組件,一個(gè)組件可以支持多種類型的數(shù)據(jù)。這樣用戶就可以以自己的數(shù)據(jù)類型來使用組件。
簡直說的就不是人話,你確定初學(xué)者看得懂?
我覺得初學(xué)者應(yīng)該要先明白為什么需要泛型這個(gè)東西,它解決了什么問題?而不是看這種拗口的定義。
我們還是先來看這樣一個(gè)例子,體會一下泛型解決的問題吧。
定義一個(gè) print 函數(shù),這個(gè)函數(shù)的功能是把傳入的參數(shù)打印出來,再返回這個(gè)參數(shù),傳入?yún)?shù)的類型是 string,函數(shù)返回類型為 string。
function print(arg:string):string {console.log(arg)return arg}復(fù)制代碼
現(xiàn)在需求變了,我還需要打印 number 類型,怎么辦?
可以使用聯(lián)合類型來改造:
function print(arg:string | number):string | number {console.log(arg)return arg}復(fù)制代碼
現(xiàn)在需求又變了,我還需要打印 string 數(shù)組、number 數(shù)組,甚至任何類型,怎么辦?
有個(gè)笨方法,支持多少類型就寫多少聯(lián)合類型。
或者把參數(shù)類型改成 any。
function print(arg:any):any {console.log(arg)return arg}復(fù)制代碼
且不說寫 any 類型不好,畢竟在 TS 中盡量不要寫 any。
而且這也不是我們想要的結(jié)果,只能說傳入的值是 any 類型,輸出的值是 any 類型,傳入和返回并不是統(tǒng)一的。
這么寫甚至還會出現(xiàn)bug
const res:string = print(123) 復(fù)制代碼
定義 string 類型來接收 print 函數(shù)的返回值,返回的是個(gè) number 類型,TS 并不會報(bào)錯提示我們。
這個(gè)時(shí)候,泛型就出現(xiàn)了,它可以輕松解決輸入輸出要一致的問題。
注意:泛型不是為了解決這一個(gè)問題設(shè)計(jì)出來的,泛型還解決了很多其他問題,這里是通過這個(gè)例子來引出泛型。
泛型基本使用
處理函數(shù)參數(shù)
我們使用泛型來解決上文的問題。
泛型的語法是 <>
里寫類型參數(shù),一般可以用 T
來表示。
function print<T>(arg:T):T {console.log(arg)return arg}復(fù)制代碼
這樣,我們就做到了輸入和輸出的類型統(tǒng)一,且可以輸入輸出任何類型。
如果類型不統(tǒng)一,就會報(bào)錯:
image.png
泛型中的 T 就像一個(gè)占位符、或者說一個(gè)變量,在使用的時(shí)候可以把定義的類型像參數(shù)一樣傳入,它可以原封不動地輸出。
泛型的寫法對前端工程師來說是有些古怪,比如
<>
T
,但記住就好,只要一看到<>
,就知道這是泛型。
我們在使用的時(shí)候可以有兩種方式指定類型。
- 定義要使用的類型
- TS 類型推斷,自動推導(dǎo)出類型
print<string>('hello') // 定義 T 為 stringprint('hello') // TS 類型推斷,自動推導(dǎo)類型為 string復(fù)制代碼
我們知道,type 和 interface 都可以定義函數(shù)類型,也用泛型來寫一下,type 這么寫:
type Print = <T>(arg: T) => Tconst printFn:Print = function print(arg) {console.log(arg)return arg}復(fù)制代碼
interface 這么寫:
interface Iprint<T> {(arg: T): T}function print<T>(arg:T) {console.log(arg)return arg}const myPrint: Iprint<number> = print復(fù)制代碼
默認(rèn)參數(shù)
如果要給泛型加默認(rèn)參數(shù),可以這么寫:
interface Iprint<T = number> {(arg: T): T}function print<T>(arg:T) {console.log(arg)return arg}const myPrint: Iprint = print復(fù)制代碼
這樣默認(rèn)就是 number 類型了,怎么樣,是不是感覺 T
就如同函數(shù)參數(shù)一樣呢?
處理多個(gè)函數(shù)參數(shù)
現(xiàn)在有這么一個(gè)函數(shù),傳入一個(gè)只有兩項(xiàng)的元組,交換元組的第 0 項(xiàng)和第 1 項(xiàng),返回這個(gè)元組。
function swap(tuple) {return [tuple[1], tuple[0]]}復(fù)制代碼
這么寫,我們就喪失了類型,用泛型來改造一下。
我們用 T 代表第 0 項(xiàng)的類型,用 U 代表第 1 項(xiàng)的類型。
function swap<T, U>(tuple: [T, U]): [U, T]{return [tuple[1], tuple[0]]}復(fù)制代碼
這樣就可以實(shí)現(xiàn)了元組第 0 項(xiàng)和第 1 項(xiàng)類型的控制。
image.png
傳入的參數(shù)里,第 0 項(xiàng)為 string 類型,第 1 項(xiàng)為 number 類型。
在交換函數(shù)的返回值里,第 0 項(xiàng)為 number 類型,第 1 項(xiàng)為 string 類型。
第 0 項(xiàng)上全是 number 的方法。
image.png
第 1 項(xiàng)上全是 string 的方法。
image.png
函數(shù)副作用操作
泛型不僅可以很方便地約束函數(shù)的參數(shù)類型,還可以用在函數(shù)執(zhí)行副作用操作的時(shí)候。
比如我們有一個(gè)通用的異步請求方法,想根據(jù)不同的 url 請求返回不同類型的數(shù)據(jù)。
function request(url:string) {return fetch(url).then(res => res.json())}復(fù)制代碼
調(diào)一個(gè)獲取用戶信息的接口:
request('user/info').then(res =>{console.log(res)})復(fù)制代碼
這時(shí)候的返回結(jié)果 res 就是一個(gè) any 類型,非常討厭。
image.png
我們希望調(diào)用 API 都清晰的知道返回類型是什么數(shù)據(jù)結(jié)構(gòu),就可以這么做:
interface UserInfo {name: stringage: number}function request<T>(url:string): Promise<T> {return fetch(url).then(res => res.json())}request<UserInfo>('user/info').then(res =>{console.log(res)})復(fù)制代碼
這樣就能很舒服地拿到接口返回的數(shù)據(jù)類型,開發(fā)效率大大提高:
image.png
約束泛型
假設(shè)現(xiàn)在有這么一個(gè)函數(shù),打印傳入?yún)?shù)的長度,我們這么寫:
function printLength<T>(arg: T): T {console.log(arg.length)return arg}復(fù)制代碼
因?yàn)椴淮_定 T 是否有 length 屬性,會報(bào)錯:
image.png
那么現(xiàn)在我想約束這個(gè)泛型,一定要有 length 屬性,怎么辦?
可以和 interface 結(jié)合,來約束類型。
interface ILength {length: number}function printLength<T extends ILength>(arg: T): T {console.log(arg.length)return arg}復(fù)制代碼
這其中的關(guān)鍵就是 <T extends ILength>
,讓這個(gè)泛型繼承接口 ILength
,這樣就能約束泛型。
我們定義的變量一定要有 length 屬性,比如下面的 str、arr 和 obj,才可以通過 TS 編譯。
const str = printLength('lin')const arr = printLength([1,2,3])const obj = printLength({ length: 10 })復(fù)制代碼
這個(gè)例子也再次印證了 interface 的 duck typing
。
只要你有 length 屬性,都符合約束,那就不管你是 str,arr 還是obj,都沒問題。
當(dāng)然,我們定義一個(gè)不包含 length 屬性的變量,比如數(shù)字,就會報(bào)錯:
image.png
泛型的一些應(yīng)用
使用泛型,可以在定義函數(shù)、接口或類的時(shí)候,不預(yù)先指定具體類型,而是在使用的時(shí)候再指定類型。
泛型約束類
定義一個(gè)棧,有入棧和出棧兩個(gè)方法,如果想入棧和出棧的元素類型統(tǒng)一,就可以這么寫:
class Stack<T> {private data: T[] = []push(item:T) {return this.data.push(item)}pop():T | undefined {return this.data.pop()}}復(fù)制代碼
在定義實(shí)例的時(shí)候?qū)戭愋?#xff0c;比如,入棧和出棧都要是 number 類型,就這么寫:
const s1 = new Stack<number>()復(fù)制代碼
這樣,入棧一個(gè)字符串就會報(bào)錯:
image.png
這是非常靈活的,如果需求變了,入棧和出棧都要是 string 類型,在定義實(shí)例的時(shí)候改一下就好了:
const s1 = new Stack<string>()復(fù)制代碼
這樣,入棧一個(gè)數(shù)字就會報(bào)錯:
image.png
特別注意的是,泛型無法約束類的靜態(tài)成員。
給 pop 方法定義 static
關(guān)鍵字,就報(bào)錯了
image.png
泛型約束接口
使用泛型,也可以對 interface 進(jìn)行改造,讓 interface 更靈活。
interface IKeyValue<T, U> {key: Tvalue: U}const k1:IKeyValue<number, string> = { key: 18, value: 'lin'}const k2:IKeyValue<string, number> = { key: 'lin', value: 18}復(fù)制代碼
泛型定義數(shù)組
定義一個(gè)數(shù)組,我們之前是這么寫的:
const arr: number[] = [1,2,3]復(fù)制代碼
現(xiàn)在這么寫也可以:
const arr: Array<number> = [1,2,3]復(fù)制代碼
數(shù)組項(xiàng)寫錯類型,報(bào)錯
image.png
小結(jié)
泛型
(Generics),從字面上理解,泛型就是一般的,廣泛的。
泛型是指在定義函數(shù)、接口或類的時(shí)候,不預(yù)先指定具體類型,而是在使用的時(shí)候再指定類型。
泛型中的 T
就像一個(gè)占位符、或者說一個(gè)變量,在使用的時(shí)候可以把定義的類型像參數(shù)一樣傳入,它可以原封不動地輸出。
泛型在成員之間提供有意義的約束,這些成員可以是:函數(shù)參數(shù)、函數(shù)返回值、類的實(shí)例成員、類的方法等。
用一張圖來總結(jié)一下泛型的好處:
image.png
TS 實(shí)戰(zhàn)
語法學(xué)得差不多了,不實(shí)戰(zhàn)一下怎么行。
Vue3 todoList
通過編寫一個(gè) Vue3 todoList,熟悉日常工作中用得最多的增刪改查的寫法,熟悉最基本的 Vue3 語法,體會比較簡單的 TS 結(jié)合 Vue3 的寫法
涉及知識
- Vue3
- script setup
- ref
- computed
- 條件渲染和列表渲染
- 數(shù)據(jù)綁定和 v-model
- 事件
- TS
- 基礎(chǔ)類型
- 接口
- 泛型
- TS 結(jié)合 Vue3
要實(shí)現(xiàn)的功能
- 新增待辦事項(xiàng)
- 刪除待辦事項(xiàng)
- 全選和取消全選功能
- 清理已做事項(xiàng)
todolist.gif
項(xiàng)目安裝
使用 pnpm 新建一個(gè) vite 項(xiàng)目
pnpm create vite my-v3-app -- --template vue
然后進(jìn)入項(xiàng)目,安裝依賴,啟動
cd my-v3-app
pnpm i
pnpm dev
image.png
真的太快了,全程不到一分鐘,就可以開始開發(fā) todoList 了。
代碼實(shí)現(xiàn)
<template><input type="text" v-model="todoMsg"><button @click="add">添加</button><button @click="clearHasDone">清理</button><div v-if="lists.length"><div v-for="(item, index) in lists" :key="item.msg"><input type="checkbox" v-model="item.done"><span :class="{done: item.done}">{{item.msg}}</span><span @click="deleteItem(index)">?</span></div><div><span>全選</span><input type="checkbox" v-model="isAllDone"><span>{{hasDown}} / {{lists.length}}</span></div></div><div v-else>暫無數(shù)據(jù)</div>
</template>
<script lang='ts' setup>
import { ref, computed } from 'vue';
interface TodoItem { // 定義 todo 事項(xiàng)類型msg: string // 要做的事done: boolean // 是否完成
}const todoMsg = ref<string>('') // 用一個(gè)ref來定義 todo 事項(xiàng),并雙向綁定
const lists = ref<TodoItem[]>([ // 定義 todo 列表,初始化一些數(shù)據(jù){ msg: '吃飯', done: true},{ msg: '睡覺', done: false},{ msg: '打游戲', done: false}
])
const hasDown = computed(() => lists.value.filter(item => item.done).length) // 已經(jīng)做了的事項(xiàng)列表
const isAllDone = computed<boolean>({ // 所有的事項(xiàng)是否完成,雙向綁定到全選按鈕get() { // isAllDone 的獲取方法,用于雙向綁定數(shù)據(jù)return hasDown.value === lists.value.length},set(value:boolean) { // isAllDone 的更改方法,用于實(shí)現(xiàn)全選 和 取消全選功能lists.value.forEach(item => {item.done = value})}
})
const add = () => { // 新增事項(xiàng)if (todoMsg.value) { // 有值才新增lists.value.push({msg: todoMsg.value,done: false})todoMsg.value = '' // 新增完了,輸入框的值清空}
}
const deleteItem = (index:number) => { // 刪除事項(xiàng)lists.value.splice(index, 1)
}
const clearHasDone = () => { // 清理所有已完成的事項(xiàng)lists.value = lists.value.filter(item => !item.done)
}
</script>
<style>.done {text-decoration: line-through; // 刪除線樣式color: gray;
}
</style>
小結(jié)
別看 todoList 的實(shí)現(xiàn)很簡單,但日常開發(fā)中我們寫的就是這些東西,俗稱搬磚,用到的 TS 知識也不多,所以說啊,如果只是為了應(yīng)付日常開發(fā),真不需要學(xué)很深。