TypeScript——函數(shù)

函數(shù)是JavaScript應用程序的基礎。 它幫助你實現(xiàn)抽象層,模擬類,信息隱藏和模塊。 在TypeScript里,雖然已經(jīng)支持類,命名空間和模塊,但函數(shù)仍然是主要的定義 行為的地方。 TypeScript為JavaScript函數(shù)添加了額外的功能,讓我們可以更容易地使用。

和JavaScript一樣,TypeScript函數(shù)可以創(chuàng)建有名字的函數(shù)和匿名函數(shù)。 你可以隨意選擇適合應用程序的方式,不論是定義一系列API函數(shù)還是只使用一次的函數(shù)。

通過下面的例子可以迅速回想起這兩種JavaScript中的函數(shù):

// Named function

function add(x, y) {

? ? return x + y;

}

// Anonymous function

let myAdd = function(x, y) { return x + y; };

在JavaScript里,函數(shù)可以使用函數(shù)體外部的變量。 當函數(shù)這么做時,我們說它‘捕獲’了這些變量。 至于為什么可以這樣做以及其中的利弊超出了本文的范圍,但是深刻理解這個機制對學習JavaScript和TypeScript會很有幫助。

let z = 100;

function addToZ(x, y) {

? ? return x + y + z;

}

函數(shù)類型

為函數(shù)定義類型

讓我們?yōu)樯厦婺莻€函數(shù)添加類型:

function add(x: number, y: number): number {

? ? return x + y;

}

let myAdd = function(x: number, y: number): number { return x + y; };

我們可以給每個參數(shù)添加類型之后再為函數(shù)本身添加返回值類型。 TypeScript能夠根據(jù)返回語句自動推斷出返回值類型,因此我們通常省略它。

書寫完整函數(shù)類型

現(xiàn)在我們已經(jīng)為函數(shù)指定了類型,下面讓我們寫出函數(shù)的完整類型。

let myAdd: (x: number, y: number) => number =

? ? function(x: number, y: number): number { return x + y; };

函數(shù)類型包含兩部分:參數(shù)類型和返回值類型。 當寫出完整函數(shù)類型的時候,這兩部分都是需要的。 我們以參數(shù)列表的形式寫出參數(shù)類型,為每個參數(shù)指定一個名字和類型。 這個名字只是為了增加可讀性。 我們也可以這么寫:

let myAdd: (baseValue: number, increment: number) => number =

? ? function(x: number, y: number): number { return x + y; };

只要參數(shù)類型是匹配的,那么就認為它是有效的函數(shù)類型,而不在乎參數(shù)名是否正確。

第二部分是返回值類型。 對于返回值,我們在函數(shù)和返回值類型之前使用( =>)符號,使之清晰明了。 如之前提到的,返回值類型是函數(shù)類型的必要部分,如果函數(shù)沒有返回任何值,你也必須指定返回值類型為 void而不能留空。

函數(shù)的類型只是由參數(shù)類型和返回值組成的。 函數(shù)中使用的捕獲變量不會體現(xiàn)在類型里。 實際上,這些變量是函數(shù)的隱藏狀態(tài)并不是組成API的一部分。

推斷類型

嘗試這個例子的時候,你會發(fā)現(xiàn)如果你在賦值語句的一邊指定了類型但是另一邊沒有類型的話,TypeScript編譯器會自動識別出類型:

// myAdd has the full function type

let myAdd = function(x: number, y: number): number { return x + y; };

// The parameters `x` and `y` have the type number

let myAdd: (baseValue: number, increment: number) => number =

? ? function(x, y) { return x + y; };

這叫做“按上下文歸類”,是類型推論的一種。 它幫助我們更好地為程序指定類型。

可選參數(shù)和默認參數(shù)

TypeScript里的每個函數(shù)參數(shù)都是必須的。 這不是指不能傳遞 null或undefined作為參數(shù),而是說編譯器檢查用戶是否為每個參數(shù)都傳入了值。 編譯器還會假設只有這些參數(shù)會被傳遞進函數(shù)。 簡短地說,傳遞給一個函數(shù)的參數(shù)個數(shù)必須與函數(shù)期望的參數(shù)個數(shù)一致。

function buildName(firstName: string, lastName: string) {

? ? return firstName + " " + lastName;

}

let result1 = buildName("Bob");? ? ? ? ? ? ? ? ? // error, too few parameters

let result2 = buildName("Bob", "Adams", "Sr.");? // error, too many parameters

let result3 = buildName("Bob", "Adams");? ? ? ? // ah, just right

JavaScript里,每個參數(shù)都是可選的,可傳可不傳。 沒傳參的時候,它的值就是undefined。 在TypeScript里我們可以在參數(shù)名旁使用 ?實現(xiàn)可選參數(shù)的功能。 比如,我們想讓last name是可選的:

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", "Adams", "Sr.");? // error, too many parameters

let result3 = buildName("Bob", "Adams");? // ah, just right

可選參數(shù)必須跟在必須參數(shù)后面。 如果上例我們想讓first name是可選的,那么就必須調(diào)整它們的位置,把first name放在后面。

在TypeScript里,我們也可以為參數(shù)提供一個默認值當用戶沒有傳遞這個參數(shù)或傳遞的值是undefined時。 它們叫做有默認初始化值的參數(shù)。 讓我們修改上例,把last name的默認值設置為"Smith"。

function buildName(firstName: string, lastName = "Smith") {

? ? return firstName + " " + lastName;

}

let result1 = buildName("Bob");? ? ? ? ? ? ? ? ? // works correctly now, returns "Bob Smith"

let result2 = buildName("Bob", undefined);? ? ? // still works, also returns "Bob Smith"

let result3 = buildName("Bob", "Adams", "Sr.");? // error, too many parameters

let result4 = buildName("Bob", "Adams");? ? ? ? // ah, just right

在所有必須參數(shù)后面的帶默認初始化的參數(shù)都是可選的,與可選參數(shù)一樣,在調(diào)用函數(shù)的時候可以省略。 也就是說可選參數(shù)與末尾的默認參數(shù)共享參數(shù)類型。

function buildName(firstName: string, lastName?: string) {

? ? // ...

}

function buildName(firstName: string, lastName = "Smith") {

? ? // ...

}

共享同樣的類型(firstName: string, lastName?: string) => string。 默認參數(shù)的默認值消失了,只保留了它是一個可選參數(shù)的信息。

與普通可選參數(shù)不同的是,帶默認值的參數(shù)不需要放在必須參數(shù)的后面。 如果帶默認值的參數(shù)出現(xiàn)在必須參數(shù)前面,用戶必須明確的傳入 undefined值來獲得默認值。 例如,我們重寫最后一個例子,讓 firstName是帶默認值的參數(shù):

function buildName(firstName = "Will", lastName: string) {

? ? return firstName + " " + lastName;

}

let result1 = buildName("Bob");? ? ? ? ? ? ? ? ? // error, too few parameters

let result2 = buildName("Bob", "Adams", "Sr.");? // error, too many parameters

let result3 = buildName("Bob", "Adams");? ? ? ? // okay and returns "Bob Adams"

let result4 = buildName(undefined, "Adams");? ? // okay and returns "Will Adams"

剩余參數(shù)

必要參數(shù),默認參數(shù)和可選參數(shù)有個共同點:它們表示某一個參數(shù)。 有時,你想同時操作多個參數(shù),或者你并不知道會有多少參數(shù)傳遞進來。 在JavaScript里,你可以使用 arguments來訪問所有傳入的參數(shù)。

在TypeScript里,你可以把所有參數(shù)收集到一個變量里:

function buildName(firstName: string, ...restOfName: string[]) {

? return firstName + " " + restOfName.join(" ");

}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

剩余參數(shù)會被當做個數(shù)不限的可選參數(shù)。 可以一個都沒有,同樣也可以有任意個。 編譯器創(chuàng)建參數(shù)數(shù)組,名字是你在省略號( ...)后面給定的名字,你可以在函數(shù)體內(nèi)使用這個數(shù)組。

這個省略號也會在帶有剩余參數(shù)的函數(shù)類型定義上使用到:

function buildName(firstName: string, ...restOfName: string[]) {

? return firstName + " " + restOfName.join(" ");

}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this

學習如何在JavaScript里正確使用this就好比一場成年禮。 由于TypeScript是JavaScript的超集,TypeScript程序員也需要弄清 this工作機制并且當有bug的時候能夠找出錯誤所在。 幸運的是,TypeScript能通知你錯誤地使用了 this的地方。 如果你想了解JavaScript里的 this是如何工作的,那么首先閱讀Yehuda Katz寫的Understanding JavaScript Function Invocation and "this"。 Yehuda的文章詳細的闡述了 this的內(nèi)部工作原理,因此我們這里只做簡單介紹。

this和箭頭函數(shù)

JavaScript里,this的值在函數(shù)被調(diào)用的時候才會指定。 這是個既強大又靈活的特點,但是你需要花點時間弄清楚函數(shù)調(diào)用的上下文是什么。 但眾所周知,這不是一件很簡單的事,尤其是在返回一個函數(shù)或?qū)⒑瘮?shù)當做參數(shù)傳遞的時候。

下面看一個例子:

let deck = {

? ? suits: ["hearts", "spades", "clubs", "diamonds"],

? ? cards: Array(52),

? ? createCardPicker: function() {

? ? ? ? return function() {

? ? ? ? ? ? let pickedCard = Math.floor(Math.random() * 52);

? ? ? ? ? ? let pickedSuit = Math.floor(pickedCard / 13);

? ? ? ? ? ? return {suit: this.suits[pickedSuit], card: pickedCard % 13};

? ? ? ? }

? ? }

}

let cardPicker = deck.createCardPicker();

let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

可以看到createCardPicker是個函數(shù),并且它又返回了一個函數(shù)。 如果我們嘗試運行這個程序,會發(fā)現(xiàn)它并沒有彈出對話框而是報錯了。 因為 createCardPicker返回的函數(shù)里的this被設置成了window而不是deck對象。 因為我們只是獨立的調(diào)用了 cardPicker()。 頂級的非方法式調(diào)用會將 this視為window。 (注意:在嚴格模式下, this為undefined而不是window)。

為了解決這個問題,我們可以在函數(shù)被返回時就綁好正確的this。 這樣的話,無論之后怎么使用它,都會引用綁定的‘deck’對象。 我們需要改變函數(shù)表達式來使用ECMAScript 6箭頭語法。 箭頭函數(shù)能保存函數(shù)創(chuàng)建時的 this值,而不是調(diào)用時的值:

let deck = {

? ? 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 here

? ? ? ? return () => {

? ? ? ? ? ? let pickedCard = Math.floor(Math.random() * 52);

? ? ? ? ? ? let pickedSuit = Math.floor(pickedCard / 13);

? ? ? ? ? ? return {suit: this.suits[pickedSuit], card: pickedCard % 13};

? ? ? ? }

? ? }

}

let cardPicker = deck.createCardPicker();

let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

更好事情是,TypeScript會警告你犯了一個錯誤,如果你給編譯器設置了--noImplicitThis標記。 它會指出 this.suits[pickedSuit]里的this的類型為any。

this參數(shù)

不幸的是,this.suits[pickedSuit]的類型依舊為any。 這是因為 this來自對象字面量里的函數(shù)表達式。 修改的方法是,提供一個顯式的 this參數(shù)。 this參數(shù)是個假的參數(shù),它出現(xiàn)在參數(shù)列表的最前面:

function f(this: void) {

? ? // make sure `this` is unusable in this standalone function

}

讓我們往例子里添加一些接口,Card 和 Deck,讓類型重用能夠變得清晰簡單些:

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),

? ? // NOTE: The function now explicitly specifies that its callee must be of type Deck

? ? createCardPicker: function(this: Deck) {

? ? ? ? return () => {

? ? ? ? ? ? let pickedCard = Math.floor(Math.random() * 52);

? ? ? ? ? ? let pickedSuit = Math.floor(pickedCard / 13);

? ? ? ? ? ? return {suit: this.suits[pickedSuit], card: pickedCard % 13};

? ? ? ? }

? ? }

}

let cardPicker = deck.createCardPicker();

let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);

現(xiàn)在TypeScript知道createCardPicker期望在某個Deck對象上調(diào)用。 也就是說 this是Deck類型的,而非any,因此--noImplicitThis不會報錯了。

this參數(shù)在回調(diào)函數(shù)里

你可以也看到過在回調(diào)函數(shù)里的this報錯,當你將一個函數(shù)傳遞到某個庫函數(shù)里稍后會被調(diào)用時。 因為當回調(diào)被調(diào)用的時候,它們會被當成一個普通函數(shù)調(diào)用, this將為undefined。 稍做改動,你就可以通過 this參數(shù)來避免錯誤。 首先,庫函數(shù)的作者要指定 this的類型:

interface UIElement {

? ? addClickListener(onclick: (this: void, e: Event) => void): void;

}

this: void means that addClickListener expects onclick to be a function that does not require a this type. Second, annotate your calling code with this:

class Handler {

? ? info: string;

? ? onClickBad(this: Handler, e: Event) {

? ? ? ? // oops, used this here. using this callback would crash at runtime

? ? ? ? this.info = e.message;

? ? }

}

let h = new Handler();

uiElement.addClickListener(h.onClickBad); // error!

指定了this類型后,你顯式聲明onClickBad必須在Handler的實例上調(diào)用。 然后TypeScript會檢測到 addClickListener要求函數(shù)帶有this: void。 改變 this類型來修復這個錯誤:

class Handler {

? ? info: string;

? ? onClickGood(this: void, e: Event) {

? ? ? ? // can't use this here because it's of type void!

? ? ? ? console.log('clicked!');

? ? }

}

let h = new Handler();

uiElement.addClickListener(h.onClickGood);

因為onClickGood指定了this類型為void,因此傳遞addClickListener是合法的。 當然了,這也意味著不能使用 this.info. 如果你兩者都想要,你不得不使用箭頭函數(shù)了:

class Handler {

? ? info: string;

? ? onClickGood = (e: Event) => { this.info = e.message }

}

這是可行的因為箭頭函數(shù)不會捕獲this,所以你總是可以把它們傳給期望this: void的函數(shù)。 缺點是每個 Handler對象都會創(chuàng)建一個箭頭函數(shù)。 另一方面,方法只會被創(chuàng)建一次,添加到 Handler的原型鏈上。 它們在不同 Handler對象間是共享的。

重載

JavaScript本身是個動態(tài)語言。 JavaScript里函數(shù)根據(jù)傳入不同的參數(shù)而返回不同類型的數(shù)據(jù)是很常見的。

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.random() * 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[pickCard(myDeck)];

alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);

alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

pickCard方法根據(jù)傳入?yún)?shù)的不同會返回兩種不同的類型。 如果傳入的是代表紙牌的對象,函數(shù)作用是從中抓一張牌。 如果用戶想抓牌,我們告訴他抓到了什么牌。 但是這怎么在類型系統(tǒng)里表示呢。

方法是為同一個函數(shù)提供多個函數(shù)類型定義來進行函數(shù)重載。 編譯器會根據(jù)這個列表去處理函數(shù)的調(diào)用。 下面我們來重載 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 the card

? ? if (typeof x == "object") {

? ? ? ? let pickedCard = Math.floor(Math.random() * 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[pickCard(myDeck)];

alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);

alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

這樣改變后,重載的pickCard函數(shù)在調(diào)用的時候會進行正確的類型檢查。

為了讓編譯器能夠選擇正確的檢查類型,它與JavaScript里的處理流程相似。 它查找重載列表,嘗試使用第一個重載定義。 如果匹配的話就使用這個。 因此,在定義重載的時候,一定要把最精確的定義放在最前面。

注意,function pickCard(x): any并不是重載列表的一部分,因此這里只有兩個重載:一個是接收對象另一個接收數(shù)字。 以其它參數(shù)調(diào)用 pickCard會產(chǎn)生錯誤。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容