
介紹
函數(shù)是 JavaScript 應(yīng)用程序的基礎(chǔ),它幫助你實(shí)現(xiàn)抽象層、模擬類、信息隱藏和模塊。在 TypeScript 里,雖然已經(jīng)支持類、命名空間和模塊,但函數(shù)任然是主要的定義行為的地方。 TypeScript 為 JavaScript 函數(shù)添加了額外的功能,讓我們可以更容易的使用。
函數(shù)
和 JavaScript 一樣, TypeScript 函數(shù)可以創(chuàng)建有名字的函數(shù)和匿名函數(shù)。 你可以隨意選擇適合應(yīng)用程序的方式,不論是定義一系列 API 函數(shù)還是只使用一次的函數(shù)。
通過(guò)下面的例子可以迅速回想起這兩種 JavaScript 中的函數(shù):
// Named function
funnction add(x, y) {
return x + y;
}
// Anonymous function
const myAdd = function(x, y) {
return x + y;
}
在 JavaScript 里,函數(shù)可以使用函數(shù)體外部的變量。當(dāng)函數(shù)這么做時(shí),我們說(shuō)它 捕獲 了這些變量。至于為什么可以這樣做以及其中的利弊超出了本文范圍,但是深刻理解這個(gè)機(jī)制對(duì)學(xué)習(xí) JavaScript 和 TypeScript 會(huì)非常有幫助。
let z = 10;
function addToZ(x, y, z) {
return x + y + z;
}
函數(shù)類型
讓我們?yōu)樯厦婺莻€(gè)函數(shù)添加類型:
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number {
return x + y;
};
我們可以給每個(gè)參數(shù)添加類型之后再為函數(shù)本身添加返回值類型。 TypeScript 能夠根據(jù)返回語(yǔ)句自動(dòng)推斷出返回值類型,因此我們通常省略它。
書寫完整函數(shù)類型
現(xiàn)在我們已經(jīng)為函數(shù)指定了類型,下面 ?? 讓我們寫出函數(shù)的完整類型。
let myAdd: (x: number, y: number) => number = function(
x: number,
y: number
): number {
return x + y;
};
函數(shù)類型包含兩部分:
- 1、參數(shù)類型
- 2、返回值類型
當(dāng)寫出完整函數(shù)類型的時(shí)候,這兩部分是需要的。我們以參數(shù)列表的形式寫出參數(shù)類型,為每個(gè)參數(shù)指定一個(gè)名字和類型。這個(gè)名字只是為了增加可讀性。我們也可以這么寫:
let myAdd: (baseValue: number, increment: number) => = function(x: number, y: number): number {
return x + y;
}
只要參數(shù)類型是匹配的,那么就認(rèn)為它是有效的函數(shù)類型,而不在乎參數(shù)名是否正確。
第二部分是返回值類型。對(duì)于返回值,我們?cè)诤瘮?shù)和返回值類型之前使用 => 符號(hào),使之清晰明了。如之前提到的,返回值類型是函數(shù)類型的必要部分,如果函數(shù)沒(méi)有任何返回值,你也必須指定返回值類型為 void 而不能留空。
函數(shù)的類型只是由類型和返回值組成的。函數(shù)中使用的捕獲變量不會(huì)體現(xiàn)在類型里。實(shí)際上,這些變量是函數(shù)的隱藏狀態(tài),并不是組成 API 的一部分。
推斷類型
嘗試這個(gè)例子的時(shí)候,你會(huì)發(fā)現(xiàn)如果你在賦值語(yǔ)句的一邊指定了類型但是另一邊沒(méi)有類型的話,TypeScript 編譯器會(huì)自動(dòng)識(shí)別出類型:
// myAdd has the full function type
let myAdd: (x: number, y: number) => number = function(x: number, y: number): number {
return x + y;
}
// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => = function(x, y): number {
return x + y;
}
這叫做"按上下文歸類",是類型推斷的一種,它幫助我們更好的為程序指定類型。
可選參數(shù)和默認(rèn)參數(shù)
TypeScript 里的每個(gè)函數(shù)參數(shù)都是必須的。這不是指不能傳遞 null 或 undefined 作為參數(shù),而是說(shuō)編譯器檢查用戶是否為每個(gè)參數(shù)都傳入了值。編譯器還會(huì)假設(shè)只有這些參數(shù)會(huì)被傳遞進(jìn)函數(shù)。間短地說(shuō),傳遞給一個(gè)函數(shù)地參數(shù)個(gè)數(shù)必須與函數(shù)期望的參數(shù)個(gè)數(shù)一致。
function buildName(fristName: string, lastName: string) {
return firstName + '' + lastName;
}
let result1 = buildName('Bob'); // error, too few parameters
let result2 = buildName('Bob', 'Admas', 'Sr.'); // error, too many parameters
let result3 = buildName('Bob', 'Admas'); // right
Javascript 里,每個(gè)參數(shù)都是可選的,可傳可不傳。沒(méi)傳參的時(shí)候,它的值就是 undefined。在 TypeScript 里我們可以在參數(shù)名旁使用 ? 實(shí)現(xiàn)可選參數(shù)的功能。比如,我們想讓 lastName 是可選的:
function buildName(firstName: string, lastName?: string) {
if (lastName) return firstName + ' ' + lastName;
else return firstName;
}
let result1 = buildName('Bob'); // works correctly now
let result2 = buildName('Bob', 'Admas', 'Sr.'); // Expected 1-2 arguments, but got 3.
let result3 = buildName('Bob', 'Admas'); // right
可選參數(shù)必須跟在必須參數(shù)后面。如果上例我們想讓 firstName 是可選的,那么就必須調(diào)整它們的位置,把 firstName 放在后面。
在 TypeScript 里,我們也可以為參數(shù)提供一個(gè)默認(rèn)值,當(dāng)用戶沒(méi)有傳遞這個(gè)參數(shù)或傳遞的值是 undefined 時(shí),它們叫做有默認(rèn)初始化值的參數(shù)。讓我們修改上例,把 lastName 的默認(rèn)值設(shè)置為 Smith 。
function buildName(firstName: string, lastName = 'Smith') {
return firstName + ' ' + lastName;
}
let result1 = buildName('Bob'); // Bob Smith
let result2 = buildName('Bob', undefined); // Bob Smith
let result3 = buildName('Bob', 'Admas', 'Sr.'); // Expected 1-2 arguments, but got 3.
let result4 = buildName('Bob', 'Admas'); // Bob Admas
在所有必須參數(shù)后面的帶初始默認(rèn)話的參數(shù)都是可選的,與可選參數(shù)一樣,在調(diào)用函數(shù)的時(shí)候可以省略。也就是說(shuō)可選參數(shù)與末尾的默認(rèn)參數(shù)共享參數(shù)類型。
function buildName(firstName: string, lastName?: string) {
// ...
}
和
function buildName(firstName: string, lastName = 'Smith') {
// ...
}
共享同樣的類型(firstName: string, lastName?: string) => string。默認(rèn)參數(shù)的默認(rèn)值消失了,只保留了它是一個(gè)可選參數(shù)的信息。
與普通可選參數(shù)不同的是,帶默認(rèn)值的參數(shù)不需要放在必須參數(shù)的后面。如果帶默認(rèn)值的參數(shù)出現(xiàn)在必須參數(shù)前面,用戶必須明確的傳入 undefined 值來(lái)獲得默認(rèn)值。例如,我們重寫最后一個(gè)例子,讓 undefined 是帶默認(rèn)值的參數(shù):
function buildName(firstName = 'Will', lastName: string) {
return firstName + ' ' + lastName;
}
let result1 = buildName('Bob'); // error, too few parameters
let result2 = buildName('Bob', 'Admas', 'Sr.'); // error, too many parameters
let result3 = buildName('Bob', 'Admas'); // okay and returns "Bob Admas"
let result4 = buildName(undefined, 'Admas'); // okay and returns "Will Admas"
剩余參數(shù)
必須參數(shù)、默認(rèn)參數(shù)和可選參數(shù)有個(gè)共同點(diǎn):它們表示某一個(gè)參數(shù)。有時(shí),你想同時(shí)操作多個(gè)參數(shù),或者你并不知道會(huì)有多少參數(shù)傳遞進(jìn)來(lái),在 JavaScript 里,你可以使用 arguments 來(lái) 訪問(wèn)所有傳入的參數(shù)。
在 TypeScript 里,你可以把所有參數(shù)收集到一個(gè)變量里:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + ' ' + restOfName.join(' ');
}
let employeeName = buildName('Joseph', 'Samuel', 'Lucas', 'MacKinzie');
// Joseph Samuel Lucas MacKinzie
剩余參數(shù)會(huì)被當(dāng)作個(gè)數(shù)不限的可選參數(shù)??梢砸粋€(gè)都沒(méi)有,同樣也可以有任意個(gè)。編譯器創(chuàng)建參數(shù)數(shù)組,名字是你在省略號(hào) (...) 后面給定的名字,你可以在函數(shù)體內(nèi)使用這個(gè)數(shù)組。
這個(gè)省略號(hào)也會(huì)在帶有剩余參數(shù)的函數(shù)類型定義上使用到:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + ' ' + restOfName.join(' ');
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
This
學(xué)習(xí)如何在 JavaScript 里正確的使用 this 就好比一場(chǎng)成人禮。由于 TypeScript 是 JavaScript 的超集, TypeScript 程序員也需要弄清 this 工作機(jī)制并且有 bug 的時(shí)候能夠找出錯(cuò)誤所在。幸運(yùn)的是, TypeScript 能通知你錯(cuò)誤地使用了 this 的地方。如果你想了解 JavaScript 里的 this 是如何工作的,那么首先閱讀 Yehuda Katz 寫的Understanding JavaScript Function Invocation and "this"。 Yehuda Katz 的文章詳細(xì)的闡述了 this 的內(nèi)部工作原理,因此我們這里只做簡(jiǎn)單介紹。
this 和 箭頭函數(shù)
JavaScript 里, this 的值在函數(shù)被調(diào)用的時(shí)候才會(huì)指定。這是個(gè)既強(qiáng)大又靈活的特點(diǎn),但是你需要花點(diǎn)時(shí)間弄清楚函數(shù)調(diào)用的上下文是什么。但眾所周知,這不是一件很簡(jiǎn)單的事,尤其是返回一個(gè)函數(shù)或?qū)⒑瘮?shù)當(dāng)作參數(shù)傳遞的時(shí)候。
下面看一個(gè)例子:
let deck = {
suits: ['hearts', 'spades', 'clubs', 'diamonds'],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickerCard = Math.floor(Math.random() * 52);
let pickerSuit = Math.floor(pickerCard / 13);
return {
suit: this.suits[pickerSuit],
card: pickerCard % 13
};
};
}
};
let cardPicker = deck.createCardPicker();
let pickerCard = cardPicker();
alert('card: ' + pickedCard.card + ' of ' + pickedCard.suit);
可以看到 createCardPicker 是個(gè)函數(shù),并且它又返回了一個(gè)函數(shù)。如果我們嘗試運(yùn)行這個(gè)程序,會(huì)發(fā)現(xiàn)它并沒(méi)有彈出對(duì)話框而是報(bào)錯(cuò)了。因?yàn)?createCardPicker 返回的函數(shù)里的 this 被設(shè)置成了 window 而不是 deck 對(duì)象。因?yàn)槲覀冎皇仟?dú)立的調(diào)用了 cardPicker 。頂級(jí)的非方法式調(diào)用會(huì)將 this 視為 window 。(注意 ??:在嚴(yán)格模式下, this 為 undefined 而不是 window 。)
為了解決這個(gè)問(wèn)題,我們可以在函數(shù)被返回時(shí)就綁好正確的 this 。這樣的話,無(wú)論之后怎么使用它,都會(huì)引用綁定的 deck 對(duì)象。我們需要改變函數(shù)表達(dá)式來(lái)使用 ECMAScript 6 箭頭語(yǔ)法。箭頭函數(shù)能保存函數(shù)創(chuàng)建時(shí)的 this 值,而不是調(diào)用時(shí)的值。
let deck = {
suits: ['hearts', 'spades', 'clubs', 'diamonds'],
cards: Array(52),
createCardPicker: function() {
return () => {
let pickerCard = Math.floor(Math.random() * 52);
let pickerSuit = Math.floor(pickerCard / 13);
return {
suit: this.suits[pickerSuit],
card: pickerCard % 13
};
};
}
};
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert('card: ' + pickedCard.card + ' of ' + pickedCard.suit);
更好事情是, TypeScript 會(huì)警告你犯了一個(gè)錯(cuò)誤,如果你給編譯器設(shè)置了 --nolmplicitThis 標(biāo)記。它會(huì)指出 this.suits[pickedSuit] 里的 this 的類型為 ay 。
this 參數(shù)
不幸的是,this.suits[pickedSuit] 的類型依舊為 any。這是因?yàn)?this 來(lái)自對(duì)象字面量里的函數(shù)表達(dá)式。修改的方法是,提供一個(gè)顯式的 this 參數(shù)。 this 參數(shù)是個(gè)假的參數(shù),它出現(xiàn)在參數(shù)列表的最前面:
function f(this: void) {
// make sure `this` is unusable in this standalone function
}
讓我們往例子里添加一些接口,Card 和 Deck,讓類型重用能夠變得清晰簡(jiǎn)單些:
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ['hearts', 'spades', 'clubs', 'diamonds'],
cards: Array(52),
// 注意:該函數(shù)現(xiàn)在顯式地指定它的被調(diào)用方必須是Deck類型
createCardPicker: function(this: Deck) {
return () => {
let pickerCard = Math.floor(Math.random() * 52);
let pickerSuit = Math.floor(pickerCard / 13);
return {
suit: this.suits[pickerSuit],
card: pickerCard % 13
};
};
}
};
let cardPicker = deck.createCardPicker();
let pickerCard = cardPicker();
alert('card: ' + pickedCard.card + ' of ' + pickedCard.suit);
現(xiàn)在 TypeScript 知道 createCardPicker 期望在某個(gè) Deck 對(duì)象上調(diào)用。也就是說(shuō) this 是 Deck 類型的,而非 any 類型,因此 --noImplicitThis 不會(huì)報(bào)錯(cuò)了。
this 參數(shù)在回調(diào)函數(shù)里
你也可以看到過(guò)在回調(diào)函數(shù)里的 this 報(bào)錯(cuò):當(dāng)你將一個(gè)函數(shù)傳遞到某個(gè)庫(kù)函數(shù)里稍后會(huì)被調(diào)用時(shí)。因?yàn)楫?dāng)回調(diào)函數(shù)被調(diào)用的時(shí)候,它們會(huì)被當(dāng)成一個(gè)普通函數(shù)調(diào)用, this 將為 undefined 。稍作改動(dòng),你就可以通過(guò) this 參數(shù)來(lái)避免錯(cuò)誤。首先,庫(kù)函數(shù)的作者要指定 this 的類型:
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void 表示 addClickListener 期望 onclick 是不需要此this 類型的函數(shù)。其次,用 this 注釋您的調(diào)用代碼。
class Handler {
info: string;
// oops, used this here. using this callback would crash at runtime
onClickBad(this: Handler, e: Event) {
this.info = e.message;
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!
指定了 this 類型后,你顯式聲明 onClickBad 必須在 Handler 的實(shí)例上調(diào)用。然后 TypeScript 會(huì)檢測(cè) addClickListener 要求函數(shù)帶有 this: void 。改變 this 類型來(lái)修復(fù)這個(gè)錯(cuò)誤:
指定了 this 類型后,你顯式聲明 onClickBad 必須在 Handler 的實(shí)例上調(diào)用。 然后 TypeScript 會(huì)檢測(cè)到 addClickListener 要求函數(shù)帶有 this: void。 改變 this 類型來(lái)修復(fù)這個(gè)錯(cuò)誤:
class Handler {
info: string;
// oops, used this here. using this callback would crash at runtime
onClickGood(this: void, e: Event) {
console.log('clicked');
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);
因?yàn)?onClickGood 指定了 this 類型為 void,因此傳遞 addClickListener 是合法的。 當(dāng)然了,這也意味著不能使用 this.info。 如果你兩者都想要,你不得不使用箭頭函數(shù)了:
class Handler {
info: string;
onClickGood = (e: Event) => {
this.info = e.message;
};
}
這是可行的因?yàn)榧^函數(shù)不會(huì)捕獲 this ,所以你總是可以把它們傳給期望 this: void 的函數(shù)。 缺點(diǎn)是每個(gè) Handler 對(duì)象都會(huì)創(chuàng)建一個(gè)箭頭函數(shù)。 另一方面,方法只會(huì)被創(chuàng)建一次,添加到 Handler 的原型鏈上。 它們?cè)诓煌?Handler 對(duì)象間是共享的。
重載
JavaScript 本身是個(gè)動(dòng)態(tài)語(yǔ)言,JavaScript 里函數(shù)根據(jù)傳入不同的參數(shù)而返回不同類型的數(shù)據(jù)是很常見(jiàn)的。
let suits = ['hearts', 'spades', 'clubs', 'diamonds'];
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == 'object') {
let pickedCard = Math.floor(Math.round() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == 'number') {
let pickedSuit = Math.floor(x / 13);
return {
suit: suits[pickedSuit],
card: x % 13
};
}
}
let myDeck = [
{ suit: 'diamonds', card: 2 },
{ suit: 'spades', card: 10 },
{ suit: 'hearts', card: 4 }
];
let pickedCard1 = myDeck[pickedCard(myDeck)];
alert('card' + pickedCard1.card + 'of' + pickedCard1.suit);
let pickedCard2 = pickedCard(15);
alert('card' + pickedCard2.card + 'of' + pickedCard2.suit);
pickCard 方法根據(jù)傳入?yún)?shù)的不同會(huì)返回兩種不同的類型。如果傳入的是代表紙牌的對(duì)象,函數(shù)作用是從中抓一張牌。如果用戶想抓牌,我們告訴他抓到了什么牌。但是這怎么在類型系統(tǒng)里表示呢。
方法是為同一個(gè)函數(shù)提供多個(gè)函數(shù)類型定義來(lái)進(jìn)行函數(shù)重載,編譯器會(huì)根據(jù)這個(gè)列表去處理函數(shù)的調(diào)用。
下面我們來(lái)重載 pickCard 函數(shù)。
let 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 {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick up the card
if (typeof x = 'object') {
let pickedCard= Math.floor(Math.round() * x.length);
return pickerCard;
}
// Otherwise just let them pick up the card
else if (typeof x == 'number') {
let pickefSuit = Math.floor(x / 13);
return {
suit: suits['pickerSuit'],
card: x % 13
};
}
}
let myDeck = [
{ suit: 'diamonds', card: 2 },
{ suit: 'spades', card: 10 },
{ suit: 'hearts', card: 4 }
];
let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);
alert('card' + pickedCard2.card + 'of' + pickedCard2.suit);
這樣改變后,重載的 pickCard 函數(shù)在調(diào)用的時(shí)候會(huì)進(jìn)行正確的類型檢測(cè)。
為了讓編譯器能夠選擇正確的檢查類型,它與 JavaScript 里的處理流程相似。它查找重載列表,嘗試使用第一個(gè)重載定義。如果匹配的話就使用這個(gè)。因此,在定義重載的時(shí)候,一定要把最精確的定義放在最前面。
注意,function pickCard(x): any 并不是重載列表的一部分,因此這里只要兩個(gè)重載:一個(gè)接收對(duì)象,另一個(gè)接收數(shù)字,以其它參數(shù)調(diào)用 pickCard 會(huì)產(chǎn)生錯(cuò)誤。
本文參考來(lái)源: TypeScript 函數(shù)