TypeScript 函數(shù)

2022-04-21 09:20 更新

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

函數(shù)

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

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

// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let 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 = 100;

function addToZ(x, y) {
    return x + y + z;
}

函數(shù)類(lèi)型

為函數(shù)定義類(lèi)型

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

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

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

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

書(shū)寫(xiě)完整函數(shù)類(lèi)型

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

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

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

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

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

第二部分是返回值類(lèi)型。 對(duì)于返回值,我們?cè)诤瘮?shù)和返回值類(lèi)型之前使用( =>)符號(hào),使之清晰明了。 如之前提到的,返回值類(lèi)型是函數(shù)類(lèi)型的必要部分,如果函數(shù)沒(méi)有返回任何值,你也必須指定返回值類(lèi)型為 void而不能留空。

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

推斷類(lèi)型

嘗試這個(gè)例子的時(shí)候,你會(huì)發(fā)現(xiàn)如果你在賦值語(yǔ)句的一邊指定了類(lèi)型但是另一邊沒(méi)有類(lèi)型的話,TypeScript編譯器會(huì)自動(dòng)識(shí)別出類(lèi)型:

// 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; };

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

可選參數(shù)和默認(rèn)參數(shù)

TypeScript里的每個(gè)函數(shù)參數(shù)都是必須的。 這不是指不能傳遞 nullundefined作為參數(shù),而是說(shuō)編譯器檢查用戶(hù)是否為每個(gè)參數(shù)都傳入了值。 編譯器還會(huì)假設(shè)只有這些參數(shù)會(huì)被傳遞進(jìn)函數(shù)。 簡(jiǎn)短地說(shuō),傳遞給一個(gè)函數(shù)的參數(shù)個(gè)數(shù)必須與函數(shù)期望的參數(shù)個(gè)數(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里,每個(gè)參數(shù)都是可選的,可傳可不傳。 沒(méi)傳參的時(shí)候,它的值就是undefined。 在TypeScript里我們可以在參數(shù)名旁使用 ?實(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ù)提供一個(gè)默認(rèn)值當(dāng)用戶(hù)沒(méi)有傳遞這個(gè)參數(shù)或傳遞的值是undefined時(shí)。 它們叫做有默認(rèn)初始化值的參數(shù)。 讓我們修改上例,把last name的默認(rèn)值設(shè)置為"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ù)后面的帶默認(rèn)初始化的參數(shù)都是可選的,與可選參數(shù)一樣,在調(diào)用函數(shù)的時(shí)候可以省略。 也就是說(shuō)可選參數(shù)與末尾的默認(rèn)參數(shù)共享參數(shù)類(lèi)型。

function buildName(firstName: string, lastName?: string) {
    // ...
}

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

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

與普通可選參數(shù)不同的是,帶默認(rèn)值的參數(shù)不需要放在必須參數(shù)的后面。 如果帶默認(rèn)值的參數(shù)出現(xiàn)在必須參數(shù)前面,用戶(hù)必須明確的傳入 undefined值來(lái)獲得默認(rèn)值。 例如,我們重寫(xiě)最后一個(gè)例子,讓 firstName是帶默認(rèn)值的參數(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ù),默認(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");

剩余參數(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ù)類(lèi)型定義上使用到:

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ī)制并且當(dāng)有bug的時(shí)候能夠找出錯(cuò)誤所在。 幸運(yùn)的是,TypeScript能通知你錯(cuò)誤地使用了 this的地方。 如果你想了解JavaScript里的 this是如何工作的,那么首先閱讀Yehuda Katz寫(xiě)的Understanding JavaScript Function Invocation and "this"。 Yehuda的文章詳細(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 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是個(gè)函數(shù),并且它又返回了一個(gè)函數(shù)。 如果我們嘗試運(yùn)行這個(gè)程序,會(huì)發(fā)現(xiàn)它并沒(méi)有彈出對(duì)話框而是報(bào)錯(cuò)了。 因?yàn)?nbsp;createCardPicker返回的函數(shù)里的this被設(shè)置成了window而不是deck對(duì)象。 因?yàn)槲覀冎皇仟?dú)立的調(diào)用了 cardPicker()。 頂級(jí)的非方法式調(diào)用會(huì)將 this視為window。 (注意:在嚴(yán)格模式下, thisundefined而不是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í)的值:

我們把函數(shù)表達(dá)式變?yōu)槭褂胠ambda表達(dá)式( () => {} )。 這樣就會(huì)在函數(shù)創(chuàng)建的時(shí)候就指定了‘this’值,而不是在函數(shù)調(diào)用的時(shí)候。

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會(huì)警告你犯了一個(gè)錯(cuò)誤,如果你給編譯器設(shè)置了--noImplicitThis標(biāo)記。 它會(huì)指出this.suits[pickedSuit]里的this的類(lèi)型為any。

this參數(shù)

不幸的是,this.suits[pickedSuit]的類(lèi)型依舊為any。 這是因?yàn)?nbsp;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,讓類(lèi)型重用能夠變得清晰簡(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),
    // 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期望在某個(gè)Deck對(duì)象上調(diào)用。 也就是說(shuō) thisDeck類(lèi)型的,而非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)被調(diào)用的時(shí)候,它們會(huì)被當(dāng)成一個(gè)普通函數(shù)調(diào)用, this將為undefined。 稍做改動(dòng),你就可以通過(guò) this參數(shù)來(lái)避免錯(cuò)誤。 首先,庫(kù)函數(shù)的作者要指定 this的類(lèi)型:

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

?this: void? 表示 ?addClickListener? 期望 ?onclick ?是一個(gè)不需要 ?this ?類(lèi)型的函數(shù)。 其次,用 ?this ?注釋您的調(diào)用代碼:

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類(lèi)型后,你顯式聲明onClickBad必須在Handler的實(shí)例上調(diào)用。 然后TypeScript會(huì)檢測(cè)到addClickListener要求函數(shù)帶有this: void。 改變 this類(lèi)型來(lái)修復(fù)這個(gè)錯(cuò)誤:

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

因?yàn)?code>onClickGood指定了this類(lèi)型為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è)诓煌?nbsp;Handler對(duì)象間是共享的。

重載

JavaScript本身是個(gè)動(dòng)態(tài)語(yǔ)言。 JavaScript里函數(shù)根據(jù)傳入不同的參數(shù)而返回不同類(lèi)型的數(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.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ù)的不同會(huì)返回兩種不同的類(lèi)型。 如果傳入的是代表紙牌的對(duì)象,函數(shù)作用是從中抓一張牌。 如果用戶(hù)想抓牌,我們告訴他抓到了什么牌。 但是這怎么在類(lèi)型系統(tǒng)里表示呢。

方法是為同一個(gè)函數(shù)提供多個(gè)函數(shù)類(lèi)型定義來(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 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)用的時(shí)候會(huì)進(jìn)行正確的類(lèi)型檢查。

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

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


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)