函式是 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 可以透過檢查 return 語句來推斷返回型別,因此在許多情況下,我們可以選擇性地省略它。
編寫函式型別
現在我們已經為函式添加了型別,讓我們看看函式型別的各個組成部分,寫出完整的函式型別。
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 =("Bob"); // error, too few parameters buildName letExpected 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
任何可選引數都必須位於必需引數之後。如果我們想讓名字(first name)變為可選,而不是姓氏(last name),我們需要更改函式的引數順序,將名字放在引數列表的最後。
在 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 =("Bob"); // error, too few parameters buildName letExpected 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"
剩餘引數
必需引數、可選引數和預設引數有一個共同點:它們一次處理一個引數。有時,你希望將多個引數作為一個組來處理,或者你可能不知道函式最終會接收多少個引數。在 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 parameters)被視為數量無限的可選引數。當傳入剩餘引數的實參時,你可以傳入任意數量;甚至可以一個都不傳。編譯器會將傳入的引數構建成一個數組,其名稱取自省略號 (...) 之後指定的名稱,從而允許你在函式中使用它。
省略號也用於帶有剩餘引數的函式型別中:
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,而不是呼叫時的 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 註釋,你就可以顯式宣告 onClickBad 必須在 Handler 的例項上被呼叫。然後 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。如果你既想使用 this 又想傳遞該函式,那麼你將不得不使用箭頭函式:
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 都會導致錯誤。