TypeScript 之函數(shù)

picture

介紹

函數(shù)是 JavaScript 應(yīng)用程序的基礎(chǔ),它幫助你實(shí)現(xiàn)抽象層、模擬類、信息隱藏和模塊。在 TypeScript 里,雖然已經(jīng)支持類、命名空間和模塊,但函數(shù)任然是主要的定義行為的地方。 TypeScriptJavaScript 函數(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í) JavaScriptTypeScript 會(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ù)都是必須的。這不是指不能傳遞 nullundefined 作為參數(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)成人禮。由于 TypeScriptJavaScript 的超集, 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)格模式下, 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í)的值。

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
}

讓我們往例子里添加一些接口,CardDeck,讓類型重用能夠變得清晰簡(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ō) thisDeck 類型的,而非 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ù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

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