函式是 JavaScript 中任何應用程式的基本建構區塊。它們是您建立抽象層級的方式,模擬類別、資訊隱藏和模組。在 TypeScript 中,雖然有類別、命名空間和模組,但函式仍然扮演著描述如何執行事情的重要角色。TypeScript 也為標準 JavaScript 函式新增了一些新功能,讓它們更容易使用。
函數
首先,就像在 JavaScript 中一樣,TypeScript 函數可以建立為命名函數或匿名函數。這讓您可以選擇最適合您應用程式的做法,無論您是要在 API 中建立函數清單,還是要建立一次性函數傳遞給其他函數。
快速回顧一下這兩種做法在 JavaScript 中的樣子
tsTry
// Named functionfunctionadd (x ,y ) {returnx +y ;}// Anonymous functionletmyAdd = function (x ,y ) {returnx +y ;};
就像在 JavaScript 中一樣,函數可以參考函數主體之外的變數。當它們這麼做時,它們被說成擷取這些變數。雖然了解這如何運作(以及使用此技術的權衡)超出了本文的範圍,但堅定地了解此機制如何運作是使用 JavaScript 和 TypeScript 的重要部分。
tsTry
letz = 100;functionaddToZ (x ,y ) {returnx +y +z ;}
函數類型
輸入函數
讓我們將類型新增到我們先前簡單的範例
tsTry
functionadd (x : number,y : number): number {returnx +y ;}letmyAdd = function (x : number,y : number): number {returnx +y ;};
我們可以將類型新增到每個參數,然後新增到函數本身以新增傳回類型。TypeScript 可以透過查看傳回陳述來找出傳回類型,因此在許多情況下,我們也可以選擇將其省略。
撰寫函數類型
在我們鍵入函式後,讓我們透過檢視函式類型的每個部分,寫出函式的完整類型。
tsTry
letmyAdd : (x : number,y : number) => number = function (x : number,y : number): number {returnx +y ;};
函式的類型具有相同的兩個部分:參數的類型和回傳類型。在寫出整個函式類型時,需要兩個部分。我們寫出參數類型就像參數清單一樣,為每個參數指定名稱和類型。此名稱只是為了幫助可讀性。我們也可以寫成
tsTry
letmyAdd : (baseValue : number,increment : number) => number = function (x : number,y : number): number {returnx +y ;};
只要參數類型對齊,無論您在函式類型中為參數指定的任何名稱,都視為函式的有效類型。
第二部分是回傳類型。我們使用參數和回傳類型之間的箭頭 (=>
) 來明確回傳類型。如前所述,這是函式類型中必要的部份,因此如果函式不回傳值,您應該使用 void
而不是省略它。
請注意,只有參數和回傳類型組成函式類型。擷取的變數不會反映在類型中。實際上,擷取的變數是任何函式的「隱藏狀態」的一部分,並不構成其 API。
推論類型
在使用範例時,你可能會注意到,即使你只在等號的一邊有型別,TypeScript 編譯器也能找出型別
tsTry
// The parameters 'x' and 'y' have the type numberletmyAdd = function (x : number,y : number): number {returnx +y ;};// myAdd has the full function typeletmyAdd2 : (baseValue : number,increment : number) => number = function (x ,y ) {returnx +y ;};
這稱為「脈絡型別」,是一種型別推論的形式。這有助於減少讓你的程式維持型別所需的努力。
選用和預設參數
在 TypeScript 中,每個參數都假設函式需要。這並不表示不能給予 null
或 undefined
,而是表示在呼叫函式時,編譯器會檢查使用者是否已為每個參數提供值。編譯器也假設這些參數是傳遞給函式的唯一參數。簡而言之,傳遞給函式的引數數量必須與函式預期的參數數量相符。
tsTry
functionbuildName (firstName : string,lastName : string) {returnfirstName + " " +lastName ;}letExpected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.result1 =buildName ("Bob"); // error, too few parametersletExpected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.result2 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult3 =buildName ("Bob", "Adams"); // ah, just right
在 JavaScript 中,每個參數都是選用的,使用者可以視需要略過。略過時,其值為 undefined
。我們可以在 TypeScript 中加入 ?
到我們想要設為選用的參數尾端,以取得此功能。例如,假設我們想要將上述的姓氏參數設為選用
tsTry
functionbuildName (firstName : string,lastName ?: string) {if (lastName ) returnfirstName + " " +lastName ;else returnfirstName ;}letresult1 =buildName ("Bob"); // works correctly nowletExpected 1-2 arguments, but got 3.2554Expected 1-2 arguments, but got 3.result2 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult3 =buildName ("Bob", "Adams"); // ah, just right
任何選用參數都必須在必要參數之後。如果我們想讓名字變成選用,而不是姓氏,我們需要變更函數中參數的順序,將名字放在清單中的最後。
在 TypeScript 中,我們也可以設定一個值,如果使用者沒有提供,或是在其位置傳遞 undefined
,則會指派給參數。這些稱為預設初始化參數。我們來看看前一個範例,並將姓氏預設為 "Smith"
。
tsTry
functionbuildName (firstName : string,lastName = "Smith") {returnfirstName + " " +lastName ;}letresult1 =buildName ("Bob"); // works correctly now, returns "Bob Smith"letresult2 =buildName ("Bob",undefined ); // still works, also returns "Bob Smith"letExpected 1-2 arguments, but got 3.2554Expected 1-2 arguments, but got 3.result3 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult4 =buildName ("Bob", "Adams"); // ah, just right
所有必要參數之後的預設初始化參數會被視為選用,就像選用參數一樣,在呼叫其各自函數時可以省略。這表示選用參數和尾隨預設參數在其類型中會共用,因此兩者
ts
function buildName(firstName: string, lastName?: string) {// ...}
和
ts
function buildName(firstName: string, lastName = "Smith") {// ...}
共用相同的類型 (firstName: string, lastName?: string) => string
。lastName
的預設值會消失在類型中,只留下參數為選用的事實。
與一般選用參數不同,預設初始化參數不需要在必要參數之後。如果預設初始化參數在必要參數之前,使用者需要明確傳遞 undefined
以取得預設初始化值。例如,我們可以用僅在 firstName
上有預設初始化值的方式撰寫最後一個範例
tsTry
functionbuildName (firstName = "Will",lastName : string) {returnfirstName + " " +lastName ;}letExpected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.result1 =buildName ("Bob"); // error, too few parametersletExpected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.result2 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult3 =buildName ("Bob", "Adams"); // okay and returns "Bob Adams"letresult4 =buildName (undefined , "Adams"); // okay and returns "Will Adams"
Rest 參數
必要的、可選的和預設的參數都有個共通點:它們一次只討論一個參數。有時,您想要將多個參數當成一個群組處理,或者您可能不知道一個函式最終會使用多少個參數。在 JavaScript 中,您可以使用每個函式主體中可見的 arguments
變數直接處理參數。
在 TypeScript 中,您可以將這些參數收集到一個變數中
tsTry
functionbuildName (firstName : string, ...restOfName : string[]) {returnfirstName + " " +restOfName .join (" ");}// employeeName will be "Joseph Samuel Lucas MacKinzie"letemployeeName =buildName ("Joseph", "Samuel", "Lucas", "MacKinzie");
Rest 參數被視為無限數量的可選參數。傳遞 rest 參數的參數時,您可以使用任意多個參數;您甚至可以不傳遞任何參數。編譯器會建立一個傳遞參數的陣列,其名稱在省略符號 (...
) 之後給出,讓您可以在函式中使用它。
省略符號也用於具有 rest 參數的函式類型中
tsTry
functionbuildName (firstName : string, ...restOfName : string[]) {returnfirstName + " " +restOfName .join (" ");}letbuildNameFun : (fname : string, ...rest : string[]) => string =buildName ;
this
學習如何在 JavaScript 中使用 this
算是某種入門儀式。由於 TypeScript 是 JavaScript 的超集,因此 TypeScript 開發人員也需要學習如何使用 this
,以及如何找出它未正確使用的時機。幸運的是,TypeScript 允許您使用幾種技術來找出 this
的不正確用法。不過,如果您需要學習 this
在 JavaScript 中如何運作,請先閱讀 Yehuda Katz 的 了解 JavaScript 函式呼叫與「this」。Yehuda 的文章很好地解釋了 this
的內部運作,因此我們在此僅介紹基礎知識。
this
和箭頭函式
在 JavaScript 中,this
是在呼叫函式時設定的變數。這使得它成為一個非常強大且靈活的功能,但代價是必須隨時了解函式執行的內容。這顯然令人困惑,尤其是在傳回函式或將函式傳遞為引數時。
讓我們來看一個範例
tsTry
letdeck = {suits : ["hearts", "spades", "clubs", "diamonds"],cards :Array (52),createCardPicker : function () {return function () {letpickedCard =Math .floor (Math .random () * 52);letpickedSuit =Math .floor (pickedCard / 13);return {suit : this.suits [pickedSuit ],card :pickedCard % 13 };};},};letcardPicker =deck .createCardPicker ();letpickedCard =cardPicker ();alert ("card: " +pickedCard .card + " of " +pickedCard .suit );
請注意,createCardPicker
是一個函式,它本身會傳回一個函式。如果我們嘗試執行此範例,我們會收到一個錯誤,而不是預期的警示方塊。這是因為在 createCardPicker
所建立的函式中所使用的 this
會設定為 window
,而不是我們的 deck
物件。這是因為我們自行呼叫 cardPicker()
。像這樣最上層的非方法語法呼叫會使用 window
作為 this
。(注意:在嚴格模式下,this
會是 undefined
,而不是 window
)。
我們可以透過確保函式在傳回函式以供稍後使用之前,已繫結到正確的 this
來修正此問題。這樣一來,不論稍後如何使用它,它仍能看到原始的 deck
物件。為此,我們變更函式表示式以使用 ECMAScript 6 箭號語法。箭號函式會擷取函式建立的位置,而不是呼叫函式的位置的 this
tsTry
letdeck = {suits : ["hearts", "spades", "clubs", "diamonds"],cards :Array (52),createCardPicker : function () {// NOTE: the line below is now an arrow function, allowing us to capture 'this' right herereturn () => {letpickedCard =Math .floor (Math .random () * 52);letpickedSuit =Math .floor (pickedCard / 13);return {suit : this.suits [pickedSuit ],card :pickedCard % 13 };};},};letcardPicker =deck .createCardPicker ();letpickedCard =cardPicker ();alert ("card: " +pickedCard .card + " of " +pickedCard .suit );
更好的是,如果您將 noImplicitThis
旗標傳遞給編譯器,TypeScript 會在您犯下此錯誤時警告您。它會指出 this.suits[pickedSuit]
中的 this
是 any
類型。
this
參數
不幸的是,this.suits[pickedSuit]
的類型仍然是 any
。這是因為 this
來自物件文字內的函式表達式。若要修正這個問題,你可以提供明確的 this
參數。this
參數是假的參數,出現在函式參數清單的第一個
ts
function f(this: void) {// make sure `this` is unusable in this standalone function}
讓我們在上面的範例中新增幾個介面,Card
和 Deck
,以讓類型更清楚且更容易重複使用
tsTry
interfaceCard {suit : string;card : number;}interfaceDeck {suits : string[];cards : number[];createCardPicker (this :Deck ): () =>Card ;}letdeck :Deck = {suits : ["hearts", "spades", "clubs", "diamonds"],cards :Array (52),// NOTE: The function now explicitly specifies that its callee must be of type DeckcreateCardPicker : function (this :Deck ) {return () => {letpickedCard =Math .floor (Math .random () * 52);letpickedSuit =Math .floor (pickedCard / 13);return {suit : this.suits [pickedSuit ],card :pickedCard % 13 };};},};letcardPicker =deck .createCardPicker ();letpickedCard =cardPicker ();alert ("card: " +pickedCard .card + " of " +pickedCard .suit );
現在 TypeScript 知道 createCardPicker
預期會在 Deck
物件上呼叫。這表示 this
現在是 Deck
類型,而不是 any
,因此 noImplicitThis
不會造成任何錯誤。
this
參數在回呼函式中
當您將函式傳遞給稍後會呼叫函式的函式庫時,您也可能會在回呼中遇到 this
錯誤。由於呼叫回呼的函式庫會像一般函式一樣呼叫它,因此 this
將會是 undefined
。透過一些工作,您也可以使用 this
參數來防止回呼錯誤。首先,函式庫作者需要使用 this
來註解回呼類型
tsTry
interfaceUIElement {addClickListener (onclick : (this : void,e :Event ) => void): void;}
this: void
表示 addClickListener
預期 onclick
是不需要 this
類型的函式。其次,使用 this
註解您的呼叫程式碼
tsTry
classHandler {info : string;onClickBad (this :Handler ,e :Event ) {// oops, used `this` here. using this callback would crash at runtimethis.info =e .message ;}}leth = newHandler ();Argument of type '(this: Handler, e: Event) => void' is not assignable to parameter of type '(this: void, e: Event) => void'. The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'Handler'.2345Argument of type '(this: Handler, e: Event) => void' is not assignable to parameter of type '(this: void, e: Event) => void'. The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'Handler'.uiElement .addClickListener (h .onClickBad ); // error!
使用 this
註解後,您明確表示必須在 Handler
的執行個體上呼叫 onClickBad
。然後 TypeScript 將會偵測到 addClickListener
需要具有 this: void
的函式。若要修正錯誤,請變更 this
的類型
tsTry
classHandler {info : string;onClickGood (this : void,e :Event ) {// can't use `this` here because it's of type void!console .log ("clicked!");}}leth = newHandler ();uiElement .addClickListener (h .onClickGood );
由於 onClickGood
將其 this
類型指定為 void
,因此可以傳遞給 addClickListener
。當然,這也表示它無法使用 this.info
。如果您想要同時使用這兩個,則必須使用箭頭函式
tsTry
classHandler {info : string;onClickGood = (e :Event ) => {this.info =e .message ;};}
這會運作,因為箭頭函式使用外部 this
,因此您隨時可以將它們傳遞給預期 this: void
的函式。缺點是會為 Handler 類型的每個物件建立一個箭頭函式。另一方面,方法只會建立一次並附加到 Handler 的原型。它們會在 Handler 類型的所有物件之間共用。
重載
JavaScript 本質上是一種非常動態的語言。單一 JavaScript 函式根據傳入參數的形狀傳回不同類型的物件,這並非罕見。
tsTry
letsuits = ["hearts", "spades", "clubs", "diamonds"];functionpickCard (x : any): any {// Check to see if we're working with an object/array// if so, they gave us the deck and we'll pick the cardif (typeofx == "object") {letpickedCard =Math .floor (Math .random () *x .length );returnpickedCard ;}// Otherwise just let them pick the cardelse if (typeofx == "number") {letpickedSuit =Math .floor (x / 13);return {suit :suits [pickedSuit ],card :x % 13 };}}letmyDeck = [{suit : "diamonds",card : 2 },{suit : "spades",card : 10 },{suit : "hearts",card : 4 },];letpickedCard1 =myDeck [pickCard (myDeck )];alert ("card: " +pickedCard1 .card + " of " +pickedCard1 .suit );letpickedCard2 =pickCard (15);alert ("card: " +pickedCard2 .card + " of " +pickedCard2 .suit );
在此,pickCard
函式會根據使用者傳入的內容傳回兩種不同的東西。如果使用者傳入代表牌組的物件,函式會挑選卡片。如果使用者挑選卡片,我們會告訴他們挑選了哪張卡片。但我們要如何向類型系統說明這一點?
答案是為同一函式提供多個函式類型作為重載清單。此清單是編譯器用來解析函式呼叫的內容。讓我們建立一個重載清單,說明我們的 pickCard
接受什麼以及傳回什麼。
tsTry
letsuits = ["hearts", "spades", "clubs", "diamonds"];functionpickCard (x : {suit : string;card : number }[]): number;functionpickCard (x : number): {suit : string;card : number };functionpickCard (x : any): any {// Check to see if we're working with an object/array// if so, they gave us the deck and we'll pick the cardif (typeofx == "object") {letpickedCard =Math .floor (Math .random () *x .length );returnpickedCard ;}// Otherwise just let them pick the cardelse if (typeofx == "number") {letpickedSuit =Math .floor (x / 13);return {suit :suits [pickedSuit ],card :x % 13 };}}letmyDeck = [{suit : "diamonds",card : 2 },{suit : "spades",card : 10 },{suit : "hearts",card : 4 },];letpickedCard1 =myDeck [pickCard (myDeck )];alert ("card: " +pickedCard1 .card + " of " +pickedCard1 .suit );letpickedCard2 =pickCard (15);alert ("card: " +pickedCard2 .card + " of " +pickedCard2 .suit );
有了這個變更,重載現在會提供類型檢查的呼叫給 pickCard
函式。
為了讓編譯器挑選正確的類型檢查,它會遵循與底層 JavaScript 類似的程序。它會查看重載清單,並從第一個重載開始,嘗試使用提供的參數呼叫函式。如果找到符合的項目,它會挑選這個重載作為正確的重載。因此,慣例是將重載從最具體的排序到最不具體的。
請注意,function pickCard(x): any
部分並非重載清單的一部分,所以它只有兩個重載:一個接收物件,一個接收數字。使用任何其他參數類型呼叫 pickCard
會導致錯誤。