函式是 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 的重要部分。
tsTryletz = 100;functionaddToZ (x ,y ) {returnx +y +z ;}
函數類型
輸入函數
讓我們將類型新增到我們先前簡單的範例
tsTryfunctionadd (x : number,y : number): number {returnx +y ;}letmyAdd = function (x : number,y : number): number {returnx +y ;};
我們可以將類型新增到每個參數,然後新增到函數本身以新增傳回類型。TypeScript 可以透過查看傳回陳述來找出傳回類型,因此在許多情況下,我們也可以選擇將其省略。
撰寫函數類型
在我們鍵入函式後,讓我們透過檢視函式類型的每個部分,寫出函式的完整類型。
tsTryletmyAdd : (x : number,y : number) => number = function (x : number,y : number): number {returnx +y ;};
函式的類型具有相同的兩個部分:參數的類型和回傳類型。在寫出整個函式類型時,需要兩個部分。我們寫出參數類型就像參數清單一樣,為每個參數指定名稱和類型。此名稱只是為了幫助可讀性。我們也可以寫成
tsTryletmyAdd : (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,而是表示在呼叫函式時,編譯器會檢查使用者是否已為每個參數提供值。編譯器也假設這些參數是傳遞給函式的唯一參數。簡而言之,傳遞給函式的引數數量必須與函式預期的參數數量相符。
tsTryfunctionbuildName (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 中加入 ? 到我們想要設為選用的參數尾端,以取得此功能。例如,假設我們想要將上述的姓氏參數設為選用
tsTryfunctionbuildName (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"。
tsTryfunctionbuildName (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
所有必要參數之後的預設初始化參數會被視為選用,就像選用參數一樣,在呼叫其各自函數時可以省略。這表示選用參數和尾隨預設參數在其類型中會共用,因此兩者
tsfunction buildName(firstName: string, lastName?: string) {// ...}
和
tsfunction buildName(firstName: string, lastName = "Smith") {// ...}
共用相同的類型 (firstName: string, lastName?: string) => string。lastName 的預設值會消失在類型中,只留下參數為選用的事實。
與一般選用參數不同,預設初始化參數不需要在必要參數之後。如果預設初始化參數在必要參數之前,使用者需要明確傳遞 undefined 以取得預設初始化值。例如,我們可以用僅在 firstName 上有預設初始化值的方式撰寫最後一個範例
tsTryfunctionbuildName (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 中,您可以將這些參數收集到一個變數中
tsTryfunctionbuildName (firstName : string, ...restOfName : string[]) {returnfirstName + " " +restOfName .join (" ");}// employeeName will be "Joseph Samuel Lucas MacKinzie"letemployeeName =buildName ("Joseph", "Samuel", "Lucas", "MacKinzie");
Rest 參數被視為無限數量的可選參數。傳遞 rest 參數的參數時,您可以使用任意多個參數;您甚至可以不傳遞任何參數。編譯器會建立一個傳遞參數的陣列,其名稱在省略符號 (...) 之後給出,讓您可以在函式中使用它。
省略符號也用於具有 rest 參數的函式類型中
tsTryfunctionbuildName (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 是在呼叫函式時設定的變數。這使得它成為一個非常強大且靈活的功能,但代價是必須隨時了解函式執行的內容。這顯然令人困惑,尤其是在傳回函式或將函式傳遞為引數時。
讓我們來看一個範例
tsTryletdeck = {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
tsTryletdeck = {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 參數是假的參數,出現在函式參數清單的第一個
tsfunction f(this: void) {// make sure `this` is unusable in this standalone function}
讓我們在上面的範例中新增幾個介面,Card 和 Deck,以讓類型更清楚且更容易重複使用
tsTryinterfaceCard {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 來註解回呼類型
tsTryinterfaceUIElement {addClickListener (onclick : (this : void,e :Event ) => void): void;}
this: void 表示 addClickListener 預期 onclick 是不需要 this 類型的函式。其次,使用 this 註解您的呼叫程式碼
tsTryclassHandler {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 的類型
tsTryclassHandler {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。如果您想要同時使用這兩個,則必須使用箭頭函式
tsTryclassHandler {info : string;onClickGood = (e :Event ) => {this.info =e .message ;};}
這會運作,因為箭頭函式使用外部 this,因此您隨時可以將它們傳遞給預期 this: void 的函式。缺點是會為 Handler 類型的每個物件建立一個箭頭函式。另一方面,方法只會建立一次並附加到 Handler 的原型。它們會在 Handler 類型的所有物件之間共用。
重載
JavaScript 本質上是一種非常動態的語言。單一 JavaScript 函式根據傳入參數的形狀傳回不同類型的物件,這並非罕見。
tsTryletsuits = ["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 接受什麼以及傳回什麼。
tsTryletsuits = ["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 會導致錯誤。