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