在JavaScript中,函數是構成任何應用程序的基礎塊。通過函數,你得以實現建立抽象層、模仿類、信息隱藏和模塊化。在TypeScript中,雖然已經存在類和模塊化,但是函數依舊在如何去"處理"事件的問題上起關鍵作用。TypeScript在JavaScript的標准基礎上給函數添加了一些新的功能使使用者可以更好的用函數處理工作。
函數
首先,和JavaScript一樣,TypeScript中的函數可以創建命名函數和匿名函數。這樣你就可以為應用程序選擇最合適的方式,無論是定義一系列函數API還是一次性使用的函數。
快速的回顧下JavaScript中的這兩種方法是怎么樣的:
// 命名函數 function add(x, y) { return x+y; } // 匿名函數 var myAdd = function(x, y) { return x+y; };
在JavaScript中,函數可以使用函數外部的變量。當這么做的時候,我們稱之為"捕獲"這些變量。要理解它是怎么工作和衡量何時使用這項技術,這已經超出本文的內容范圍了,透徹的理解這個機制在JavaScript和TypeScript中是多么重要的一部分是需要的。
var z = 100; function addToZ(x, y) { return x+y+z; }
函數類型
給函數添加類型
為之前的簡單案例添加類型:
function add(x: number, y: number): number { return x+y; } var myAdd = function(x: number, y: number): number { return x+y; };
我們可以給每個參數指定類型,並且為函數本身return的值指定類型。TypeScript能夠根據return語句推算出返回值的類型,所以很多情況下可以忽略它。
編寫函數類型
現在我們已經為函數添加了類型,接下來為函數寫出所有的類型:
var myAdd: (x:number, y:number)=>number = function(x: number, y: number): number { return x+y; };
函數類型包括兩個部分:arguments(參數)的類型和return(返回)值的類型。當需要寫所有的函數類型,這兩部分是必需的。我們寫參數類型就像寫一個參數列表一樣,每個參數給定一個名稱和類型。這個名稱只是為了增加可讀性,我們可以這樣寫:
var myAdd: (baseValue:number, increment:number)=>number = function(x: number, y: number): number { return x+y; };
只要參數類型正確,它就被認為是有效的函數類型,而不用去在乎參數名稱是否正確。
第二部分是返回值類型。我們在參數和返回值類型之間用肥肥的箭頭(=>)來明確這個類型。正如前面所說,這只是函數類型的一部分,所以當不存在返回值時,應該使用"void",而不是任由它空着。
注:只有參數和返回值的類型組成了函數類型。捕獲到的變量不會在類型中體現。實際上,捕獲的變量屬於函數"隱藏狀態"部分的,並且也不是API的組成部分。
類型推斷
在例子中,你可能已經注意到,當你在賦值語句的任意一邊指定類型,TypeScript編譯器都能夠在另一邊自動識別類型:
// myAdd 函數中所有的類型 var myAdd = function(x: number, y: number): number { return x+y; }; // 參數'x'和'y'是number類型 var myAdd: (baseValue:number, increment:number)=>number = function(x, y) { return x+y; };
這稱為"上下文(語境)歸類",一種類型推斷的形式。這有助於減少工作量並且保持程序的類型。
可選參數和默認參數
和JavaScript不同,TypeScript函數中的每個參數都是必需的。這並不意味着不可以傳入"null"值,當一個函數被調用的時候,編譯器會檢查用戶是否為每一個參數提供值。編譯器也會假設這些參數就是需要被傳入函數的參數。簡而言之,函數的傳入參數的個數必須和函數所期望被傳入參數的個數相等。
function buildName(firstName: string, lastName: string) { return firstName + " " + lastName; } var result1 = buildName("Bob"); // 錯誤,傳的參數太少 var result2 = buildName("Bob", "Adams", "Sr."); // 錯誤,傳的參數太多 var result3 = buildName("Bob", "Adams"); // 額,這是正確的
在JavaScript中,每一個參數都是可選的,用戶可以在恰當的時候不用傳某個參數。這樣做就相當於傳入"undefined"代替這個參數。在TypeScript中,我們可以在參數后面加上"?"符號,讓這個參數變成可選參數。例如,我們想要"lastName"是可選的:
function buildName(firstName: string, lastName?: string) { if (lastName) return firstName + " " + lastName; else return firstName; } var result1 = buildName("Bob"); // 正常運行 var result2 = buildName("Bob", "Adams", "Sr."); // 錯誤,傳的參數太多 var result3 = buildName("Bob", "Adams"); // 額,這是正確的
可選參數必須放在必需參數后面(存在必需參數的情況下)。假如我們要"firstName"變成可選參數而不是"lastName",我們需要改變函數參數的排序,將"firstName"放到后面。
在TypeScript中也可以為某個參數設置值,當用戶未提供該參數時,將使用這個值。這被稱為"默認值"。將上個例子中的"lastName"的默認值設置為"Smith":
function buildName(firstName: string, lastName = "Smith") { return firstName + " " + lastName; } var result1 = buildName("Bob"); // 正常運行 var result2 = buildName("Bob", "Adams", "Sr."); // 錯誤,傳的參數太多 var result3 = buildName("Bob", "Adams"); // 額,這是正確的
和可選參數一樣,默認參數必須放在必需參數后面(存在必需參數的情況下)。
可選參數和默認參數共享類型。如
function buildName(firstName: string, lastName?: string) {
和
function buildName(firstName: string, lastName = "Smith") {
共享同一個類型 "(firstName: string, lastName?: string)=>string"。
其他參數
必需參數,可選參數和默認參數都有以個共同點:它們只表示一個參數。有些時候可能想要多個參數,或者不知道具體有多少個參數最終需要被傳入。在JavaScript中,你可以使用arguments來訪問函數傳入的所有參數。
在TypeScript中,你可以將所有的參數聚集到一個變量中:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" "); } var employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
其他參數被視為數量無限的可選參數。你可以一個都不傳,也可以傳任意你想傳的個數。編譯器將會生成一個你傳入函數的參數數組,以省略號("...")之后的名稱命名,你可以在函數中使用這個數組。
省略號也用在帶有其他參數的函數類型:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" "); } var buildNameFun: (fname: string, ...rest: string[])=>string = buildName;
Lambdas和使用"this"
對於JavaScript程序員來說,關於"this"工作機制的話題算是老生常談了。確實,學會如何使用"this"是JavaScript程序員成長中必須經歷的。因為TypeScript是JavaScript的一個超集,TypeScript程序員也需要學會如何讓使用"this"並且如何處理錯誤使用"this"引發的問題。假如要用來描述如何使用JavaScript中的"this",一整篇文章都可以寫,並且該類文章也有很多。在這里只介紹基礎知識。
在JavaScript中,"this"在函數被調用的時候被指定。這使得它強大而又靈活,只是你需要為理解函數執行的上下文付出代價。眾所周知,這是很麻煩的,比如,當一個函數當作回調函數使用的時候。
來看一個例子:
var deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return function() { var pickedCard = Math.floor(Math.random() * 52); var pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } } } var cardPicker = deck.createCardPicker(); var pickedCard = cardPicker(); alert("card: " + pickedCard.card + " of " + pickedCard.suit);
試着運行這個例子,我們會得到一個錯誤,而不是預期的彈出警告框。因為'createCardPicker'返回的函數里"this"指向的是Window對象而不是"deck"對象。所以在這里調用"cardPicker()",將Window對象綁定到了"this"上。(注意:嚴格模式下,this是undefined而不是Window)
我們可以在返回函數的時候就綁定正確的"this",這樣,無論后面怎么調用這個函數,"this"依舊會指向"deck"對象。
為了解決這問題,我們可以使用lambda語法代替JavaScript函數表達式。它將會在函數創建的時候自動捕獲"this"變量,而不是在被調用的時候處理"this"。
var deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { // 注意: 下面這行現在是使用lambda語法, 更早的捕獲'this' return () => { var pickedCard = Math.floor(Math.random() * 52); var pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } } } var cardPicker = deck.createCardPicker(); var pickedCard = cardPicker(); alert("card: " + pickedCard.card + " of " + pickedCard.suit);
想要了解關於"this"的更多信息,可以閱讀Yahuda Katz的 理解JavaScript函數調用和"this"。
重載
JavaScript本身就是動態語言。根據傳入參數的不同而返回不同類型的值在JavaScript函數中用的並不少。
var suits = ["hearts", "spades", "clubs", "diamonds"]; function pickCard(x) { // 檢查我們處理的是什么類型的參數 // 如果是數組對象, 則給定deck並且選擇card if (typeof x == "object") { var pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // 否則只選擇card else if (typeof x == "number") { var pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; } } var myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }]; var pickedCard1 = myDeck[pickCard(myDeck)]; alert("card: " + pickedCard1.card + " of " + pickedCard1.suit); var pickedCard2 = pickCard(15); alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
這里的"pickCard"將會根據用戶傳入的參數不同而返回不同的數據。如用戶傳入的是代表"deck"的對象,則函數將會選擇一個"card"並且返回。如果用戶已經選擇了"card",我們則會返回他選擇的是哪個"card"。但是怎么在類型系統里描述這些呢。
解決方案是基於同一個函數提供多個函數類型,就如函數重載列表。這個列表將被編譯器用來解決函數的調用。現在來創建一個重載列表,描述"pickCard"接收的是什么,返回的是什么。
var suits = ["hearts", "spades", "clubs", "diamonds"]; function pickCard(x: {suit: string; card: number; }[]): number; function pickCard(x: number): {suit: string; card: number; }; function pickCard(x): any { // 檢查我們處理的是什么類型的參數 // 如果是數組對象, 則給定deck並且選擇card if (typeof x == "object") { var pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // 否則只選擇card else if (typeof x == "number") { var pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; } } var myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }]; var pickedCard1 = myDeck[pickCard(myDeck)]; alert("card: " + pickedCard1.card + " of " + pickedCard1.suit); var pickedCard2 = pickCard(15); alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
有了這些改變,重載后的"pickCard"函數會在被調用的時候進行合適的類型檢查。
為了編譯器選擇正確的類型檢查,它和JavaScript底層運行是相似的。它會查找重載列表,使用第一次定義的函數進行檢查,如果能匹配,則調用當前這個函數。正是因為如此,一般都會將最明確的定義放在前面。
注意,"function pickCard(x: any): any"不是重載列表的一部分。所以它只有2次重載:一個針對的是object,一次針對的是number。調用"pickCard"並且傳入其他類型的參數將會導致錯誤。