TypeScript 函數(shù)(Function)
本節(jié)介紹 TypeScript 的函數(shù),函數(shù)是任何應(yīng)用程序的基本構(gòu)建部分,通過(guò)函數(shù)返回一個(gè)計(jì)算后的值。
TypeScript 的函數(shù)聲明中函數(shù)類型是極為重要的,函數(shù)的參數(shù)都需要標(biāo)注參數(shù)類型,這可以幫助編譯器進(jìn)行正確的類型推導(dǎo)。本節(jié)還會(huì)著重講解 this
的使用,可以通過(guò)編譯選項(xiàng)和 this 參數(shù)兩種方法,正確理解 this 的指向。
1. 慕課解釋
在 JavaScript 中,函數(shù)是頭等(first-class)對(duì)象,因?yàn)樗鼈兛梢韵袢魏纹渌麑?duì)象一樣具有屬性和方法。在 JavaScript 中,每個(gè)函數(shù)都是一個(gè) Function
對(duì)象。
TypeScript 又為 JavaScript 函數(shù)添加了一些額外的功能,讓我們可以更容易地使用:
- 函數(shù)類型
- 可選參數(shù)
- 默認(rèn)參數(shù)
- 剩余參數(shù)
- 函數(shù)重載
2. 函數(shù)類型
在 TypeScript 中編寫函數(shù),需要給形參和返回值指定類型:
const add = function(x: number, y: number): string {
return (x + y).toString()
}
代碼解釋:
參數(shù) x 和 y 都是 number 類型,兩個(gè)參數(shù)相加后將其類型轉(zhuǎn)換為 string, 所以整個(gè)函數(shù)的返回值為 string 類型。
上面的代碼只是對(duì) =
等號(hào)右側(cè)的匿名函數(shù)進(jìn)行了類型定義,等號(hào)左側(cè)的 add
同樣可以添加類型:
const add: (x: number, y: number) => string = function(x: number, y: number): string {
return (x + y).toString()
}
可以看到,等號(hào)左側(cè)的類型定義由兩部分組成:參數(shù)類型和返回值類型,通過(guò) =>
符號(hào)來(lái)連接。
這里要注意:函數(shù)類型的 =>
和 箭頭函數(shù)的 =>
是不同的含義。
通過(guò)箭頭函數(shù)改寫一下剛才寫的函數(shù):
const add = (x: number, y: number): string => (x + y).toString()
等號(hào)左右兩側(cè)書寫完整:
// 只要參數(shù)位置及類型不變,變量名稱可以自己定義,比如把兩個(gè)參數(shù)定位為 a b
const add: (a: number, b: number) => string = (x: number, y: number): string => (x + y).toString()
3. 函數(shù)的參數(shù)
3.1 參數(shù)個(gè)數(shù)保持一致
TypeScript 中每個(gè)函數(shù)參數(shù)都是必須的。 這不是指不能傳遞 null 或 undefined 作為參數(shù),而是說(shuō)編譯器會(huì)檢查用戶是否為每個(gè)參數(shù)都傳入了值。簡(jiǎn)短地說(shuō),傳遞給一個(gè)函數(shù)的參數(shù)個(gè)數(shù)必須與函數(shù)期望的參數(shù)個(gè)數(shù)一致。
const fullName = (firstName: string, lastName: string): string => `${firstName}${lastName}`
let result1 = fullName('Sherlock', 'Holmes')
let result2 = fullName('Sherlock', 'Holmes', 'character') // Error, Expected 2 arguments, but got 3
let result3 = fullName('Sherlock') // Error, Expected 2 arguments, but got 1
代碼解釋:
第 1 行,一個(gè)需要傳入 2 個(gè)字符串類型參數(shù)的函數(shù)類型定義。
第 4 行,result2
傳入了 3 個(gè)參數(shù),與聲明的 2 個(gè)參數(shù)不符。
第 5 行,result3
只傳入了 1 個(gè)參數(shù),同樣與聲明的 2 個(gè)參數(shù)不符。
3.2 可選參數(shù)
在 JavaScript 中每個(gè)參數(shù)都是可選的,可傳可不傳。沒(méi)傳參的時(shí)候,它的值就是 undefined。 而在 TypeScript 里我們可以在參數(shù)名旁使用 ?
實(shí)現(xiàn)可選參數(shù)的功能,可選參數(shù)必須跟在必須參數(shù)后面。
const fullName = (firstName: string, lastName?: string): string => `${firstName}${lastName}`
let result1 = fullName('Sherlock', 'Holmes')
let result2 = fullName('Sherlock', 'Holmes', 'character') // Error, Expected 1-2 arguments, but got 3
let result3 = fullName('Sherlock') // OK
代碼解釋:
第 1 行,firstName 是必須參數(shù),lastName 是可選參數(shù)。
第 4 行,傳入了 3 個(gè)參數(shù),與聲明的 2 個(gè)參數(shù)不符。
第 5 行,lastName 是可選參數(shù),可以省略。
3.3 默認(rèn)參數(shù)
參數(shù)可以取默認(rèn)值,上面介紹的可選參數(shù)必須跟在必須參數(shù)后面,而帶默認(rèn)值的參數(shù)不需要放在必須參數(shù)的后面,可隨意調(diào)整位置:
const token = (expired = 60*60, secret: string): void => {}
// 或
const token1 = (secret: string, expired = 60*60 ): void => {}
代碼解釋:
第 1 行,帶默認(rèn)值的參數(shù) expired 在參數(shù)列表首位。
第 3 行,帶默認(rèn)值的參數(shù) expired 在參數(shù)列表末位。
3.4 剩余參數(shù)
有的時(shí)候,函數(shù)的參數(shù)個(gè)數(shù)是不確定的,可能傳入未知個(gè)數(shù),這時(shí)沒(méi)有關(guān)系,有一種方法可以解決這個(gè)問(wèn)題。
通過(guò) rest 參數(shù)
(形式為 ...變量名
)來(lái)獲取函數(shù)的剩余參數(shù),這樣就不需要使用 arguments
對(duì)象了。
function assert(ok: boolean, ...args: string[]): void {
if (!ok) {
throw new Error(args.join(' '));
}
}
assert(false, '上傳文件過(guò)大', '只能上傳jpg格式')
代碼解釋:
第 1 行,第二個(gè)參數(shù)傳入剩余參數(shù),且均為字符串類型。
第 7 行,調(diào)用函數(shù) assert()
時(shí),除了第一個(gè)函數(shù)傳入一個(gè)布爾類型,接下來(lái)可以無(wú)限傳入多個(gè)字符串類型的參數(shù)。
TIP:注意
rest 參數(shù)
只能是最后一個(gè)參數(shù)。
3.5 this 參數(shù)
JavaScript 里,this 的值在函數(shù)被調(diào)用的時(shí)候才會(huì)被指定,但是這個(gè) this 到底指的是什么還是需要花點(diǎn)時(shí)間弄清楚。
默認(rèn)情況下,tsconfig.json
中,編譯選項(xiàng) compilerOptions
的屬性 noImplicitThis
為 false
,我們?cè)谝粋€(gè)對(duì)象中使用的 this 時(shí),它的類型是 any 類型。
let triangle = {
a: 10,
b: 15,
c: 20,
area: function () {
return () => {
// this 為 any 類型
const p = (this.a + this.b + this.c) / 2
return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
}
}
}
const myArea = triangle.area()
console.log(myArea())
代碼解釋:
在實(shí)際工作中 any 類型是非常危險(xiǎn)的,我們可以添加任意屬性到 any 類型的參數(shù)上,比如將 const p = (this.a + this.b + this.c) / 2
這句改為 const p = (this.d + this.d + this.d) / 2
也不會(huì)報(bào)錯(cuò),這很容易造成不必要的問(wèn)題。
所以我們應(yīng)該明確 this 的指向,下面介紹兩種方法:
第一種,在 tsconfig.json
中,將編譯選項(xiàng) compilerOptions
的屬性 noImplicitThis
設(shè)置為 true
,TypeScript 編譯器就會(huì)幫你進(jìn)行正確的類型推斷:
let triangle = {
a: 10,
b: 15,
c: 20,
area: function () {
return () => {
const p = (this.a + this.b + this.c) / 2
return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
}
}
}
const myArea = triangle.area()
console.log(myArea())
代碼解釋:
將 noImplicitThis
設(shè)置為 true
以后,把鼠標(biāo)放在第 7 行的 this
上,可以看到:
this: {
a: number;
b: number;
c: number;
area: () => () => number;
}
這時(shí),TypeScript 編譯器就能準(zhǔn)確的知道了 this 的類型,如果取不存在于 this 屬性中的 d
,將會(huì)報(bào)錯(cuò) Property 'd' does not exist on type '{ a: number; b: number; c: number; area: () => () => any; }'
除了這種方法,我們還可以通過(guò) this 參數(shù)
這種形式來(lái)解決 this 為 any 類型這一問(wèn)題。提供一個(gè)顯式的 this
參數(shù),它出現(xiàn)在參數(shù)列表的最前面:
// 語(yǔ)法
function f(this: void) {
}
改造剛才的例子:
interface Triangle {
a: number;
b: number;
c: number;
area(this: Triangle): () => number;
}
let triangle: Triangle = {
a: 10,
b: 15,
c: 20,
area: function (this: Triangle) {
return () => {
const p = (this.a + this.b + this.c) / 2
return Math.sqrt(p * (p - this.a) * (p - this.b) *(p - this.c))
}
}
}
const myArea = triangle.area()
console.log(myArea())
代碼解釋:
我們聲明了一個(gè)接口 Triangle
,其中的函數(shù)類型顯式的傳入了 this
參數(shù),這個(gè)參數(shù)的類型為 Triangle
類型(第 5 行):
area(this: Triangle): () => number;
此時(shí),在第 14 行,this
指向 Triangle
,就可以進(jìn)行正確的類型判斷,如果取未定義參數(shù),編譯器將直接報(bào)錯(cuò)。
4. 函數(shù)重載
函數(shù)重載是指函數(shù)根據(jù)傳入不同的參數(shù),返回不同類型的數(shù)據(jù)。
它的意義在于讓你清晰的知道傳入不同的參數(shù)得到不同的結(jié)果,如果傳入的參數(shù)不同,但是得到相同類型的數(shù)據(jù),那就不需要使用函數(shù)重載。
比如面試中??嫉淖址崔D(zhuǎn)問(wèn)題,這里就不考慮負(fù)數(shù)情況了,只是為了演示函數(shù)重載:
function reverse(target: string | number) {
if (typeof target === 'string') {
return target.split('').reverse().join('')
}
if (typeof target === 'number') {
return +[...target.toString()].reverse().join('')
}
}
console.log(reverse('imooc')) // coomi
console.log(reverse(23874800)) // 847832
編譯器并不知道入?yún)⑹鞘裁搭愋偷?,返回值類型也不能確定。這時(shí)可以為同一個(gè)函數(shù)提供多個(gè)函數(shù)類型定義來(lái)進(jìn)行函數(shù)重載。
(通過(guò) --downlevelIteration
編譯選項(xiàng)增加對(duì)生成器和迭代器協(xié)議的支持)
function reverse(x: string): string
function reverse(x: number): number
function reverse(target: string | number) {
if (typeof target === 'string') {
return target.split('').reverse().join('')
}
if (typeof target === 'number') {
return +[...target.toString()].reverse().join('')
}
}
console.log(reverse('imooc')) // coomi
console.log(reverse(23874800)) // 847832
代碼解釋:
因?yàn)檫@個(gè)反轉(zhuǎn)函數(shù)在傳入字符串類型的時(shí)候返回字符串類型,傳入數(shù)字類型的時(shí)候返回?cái)?shù)字類型,所以在前兩行進(jìn)行了兩次函數(shù)類型定義。在函數(shù)執(zhí)行時(shí),根據(jù)傳入的參數(shù)類型不同,進(jìn)行不同的計(jì)算。
為了讓編譯器能夠選擇正確的檢查類型,它會(huì)從重載列表的第一個(gè)開始匹配。因此,在定義重載時(shí),一定要把最精確的定義放在最前面。
5. 使用函數(shù)時(shí)的注意事項(xiàng)
- 如果一個(gè)函數(shù)沒(méi)有使用
return
語(yǔ)句,則它默認(rèn)返回undefined
。 - 調(diào)用函數(shù)時(shí),傳遞給函數(shù)的值被稱為函數(shù)的
實(shí)參
(值傳遞),對(duì)應(yīng)位置的函數(shù)參數(shù)被稱為形參
。 - 在函數(shù)執(zhí)行時(shí),
this
關(guān)鍵字并不會(huì)指向正在運(yùn)行的函數(shù)本身,而是指向調(diào)用函數(shù)的對(duì)象
。 arguments
對(duì)象是所有(非箭頭)函數(shù)中都可用的局部變量
。你可以使用 arguments 對(duì)象在函數(shù)中引用函數(shù)的參數(shù)。
6. 小結(jié)
本節(jié)介紹了 TypeScript 中函數(shù)的一些新增功能,編寫 TypeScript 代碼一定要將類型的概念了解透徹,無(wú)論是變量還是函數(shù),都要記得進(jìn)行類型定義。