TypeScript 函式官方文件學習
TypeScript 函式官方文件學習
介紹
函式是JS應用程式的基礎. 它幫助你實現抽象層, 模擬類, 資訊隱藏和模組. 在TypeScript裡,雖然已經支援類,名稱空間和模組,但函式仍然是主要的定義 行為的地方。 TypeScript為JavaScript函式添加了額外的功能,讓我們可以更容易地使用。
函式
和JS一樣, TS可以建立有名字的函式和匿名函式. 你可以隨意選擇適合應用程式的方式, 不論是定義一系列API函式還是隻使用一次的函式。
通過下面的例子可以迅速回想起這兩種JavaScript中的函式:
// 有名字的函式 Named function
function add(x, y) {
return x + y
}
// 匿名函式
let myAdd = function(x, y) {
return x + y
}
(function() {
console.log("Hello world")
})()
通過匿名函式可以實現閉包, 閉包是可以訪問在函式作用域內定義的變數的函式。若要建立一個閉包,往往都需要用到匿名函式。
在JavaScript裡,函式可以使用函式體外部的變數。 當函式這麼做時,我們說它‘捕獲’了這些變數。
至於為什麼可以這樣做以及其中的利弊超出了本文的範圍,但是深刻理解這個機制對學習JavaScript和TypeScript會很有幫助。
let z = 100;
function addToZ(x, y) {
return x + y + z;
}
函式型別
為函式定義型別
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x + y; };
我們可以給每個引數新增型別之後再為函式本身新增返回值型別。 TypeScript能夠根據返回語句自動推斷出返回值型別
書寫完整函式型別
let myAdd: (x: number, y: number) => number = // (x: number, y: number) => number
function(x: number, y: number): number { return x + y; };
函式型別包含兩部分:引數型別和返回值型別。 當寫出完整函式型別的時候,這兩部分都是需要的。
我們以引數列表的形式寫出引數型別,為每個引數指定一個名字和型別。 這個名字只是為了增加可讀性。 我們也可以這麼寫:
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };
引數列表的名字與實際函式引數的名字可以不同.
只要引數型別是匹配的,那麼就認為它是有效的函式型別,而不在乎引數名是否正確。
第二部分是返回值型別。 對於返回值,我們在函式和返回值型別之前使用( =>
)符號,使之清晰明瞭。 如之前提到的,返回值型別是函式型別的必要部分,如果函式沒有返回任何值,你也必須指定返回值型別為 void
而不能留空。
函式的型別只是由引數型別和返回值組成的。 函式中使用的捕獲變數不會體現在型別裡。 實際上,這些變數是函式的隱藏狀態並不是組成API的一部分。
推斷型別
嘗試這個例子的時候, 你會發現如果你在賦值語句的一邊指定了型別但是另外一邊沒有型別的話, TS編譯器會自動識別出型別:
// myAdd 具有完整的函式型別
let myAdd = function(x: number, y: number): number { return x + y; };
// 引數' x '和' y '具有型別number
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };
這叫做“按上下文歸類”,是型別推論的一種。 它幫助我們更好地為程式指定型別。
可選引數和預設引數
TS裡每個函式引數都是必須的. 這不是不能傳遞null
或undefined
作為引數, 而是說編譯器檢查使用者是否為每個引數都傳入了值. 編譯器還會假設只有這些引數會被傳遞進函式.簡短的說, 傳遞給一個函式的引數個數必須與函式期望的引數個數一致.
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName
}
let result1 = buildName("Bob"); // 錯誤, 引數太少
let result2 = buildName("Bob", "Adams", "Sr."); // 錯誤, 引數太多
let result3 = buildName("Bob", "Adams"); // 引數剛剛好
JS裡, 每個引數都是可選的, 可傳可不傳, 沒傳參的時候它的值就是undefined.
我們如果想在TS裡實現如此可選引數的功能, 可以使用?
. 比如, 我們想讓lastName
是可選的:
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // 現在可以正常工作
let result2 = buildName("Bob", "Adams", "Sr."); // 錯誤, 引數太多
let result3 = buildName("Bob", "Adams"); // 引數剛剛好
可選引數必須跟在必須引數後面。 如果上例我們想讓firstName是可選的,那麼就必須調整它們的位置,把firstName放在後面。
在TypeScript裡,我們也可以為引數提供一個預設值當用戶沒有傳遞這個引數或傳遞的值是undefined
時。 它們叫做有預設初始化值的引數。 讓我們修改上例,把lastName的預設值設定為"Smith"
。
function buildName(firstName: string, lastName: string="Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // 現在可以正常工作, 返回 "Bob Smith"
let result2 = buildName("Bob", undefined); // 依然正常工作, 返回 "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // 錯誤, 引數太多
let result4 = buildName("Bob", "Adams"); // 引數剛剛好
在所有必須引數後面的帶預設初始化的引數都是可選的,與可選引數一樣,在呼叫函式的時候可以省略。 也就是說可選引數與末尾的預設引數共享引數型別。
function buildName(firstName: string, lastName?: string) {
// ...
}
function buildName(firstName: string, lastName = "Smith") {
// ...
}
共享同樣的型別(firstName: string, lastName?: string) => string
。 預設引數的預設值消失了,只保留了它是一個可選引數的資訊。
與普通可選引數不同的是,帶預設值的引數不需要放在必須引數的後面。 如果帶預設值的引數出現在必須引數前面,使用者必須明確的傳入 undefined
值來獲得預設值。 例如,我們重寫最後一個例子,讓 firstName
是帶預設值的引數:
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // 錯誤, 引數太少
let result2 = buildName("Bob", "Adams", "Sr."); // 錯誤, 引數太多
let result3 = buildName("Bob", "Adams"); // 正常, 返回 "Bob Adams"
let result4 = buildName(undefined, "Adams"); // 正常, 返回 "Will Adams"
剩餘引數
必要引數,預設引數和可選引數有個共同點:它們表示某一個引數。 有時,你想同時操作多個引數,或者你並不知道會有多少引數傳遞進來。 在JavaScript裡,你可以使用 arguments
來訪問所有傳入的引數。
在TypeScript裡,你可以把所有引數收集到一個變數裡:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
剩餘引數會被當做個數不限的可選引數。 可以一個都沒有,同樣也可以有任意個。 編譯器建立引數陣列,名字是你在省略號( ...
)後面給定的名字,你可以在函式體內使用這個陣列。
這個省略號也會在帶有剩餘引數的函式型別定義上使用到:
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
的地方。
this
和箭頭函式
JavaScript裡,this
的值在函式被呼叫的時候才會指定。 這是個既強大又靈活的特點,但是你需要花點時間弄清楚函式呼叫的上下文是什麼。 但眾所周知,這不是一件很簡單的事,尤其是在返回一個函式或將函式當做引數傳遞的時候。
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
是個函式,並且它又返回了一個函式。 如果我們嘗試執行這個程式,會發現它並沒有彈出對話方塊而是報錯了。 因為 createCardPicker
返回的函式裡的this
被設定成了window
而不是deck
物件。 因為我們只是獨立的呼叫了 cardPicker()
。 頂級的非方法式呼叫會將 this
視為window
。 (注意:在嚴格模式下, this
為undefined
而不是window
)。
為了解決這個問題,我們可以在函式被返回時就綁好正確的this
。 這樣的話,無論之後怎麼使用它,都會引用繫結的‘deck’物件。 我們需要改變函式表示式來使用ECMAScript 6箭頭語法。 箭頭函式能儲存函式建立時的 this
值,而不是呼叫時的值:
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
// 下面一行現在是一個箭頭函式,允許我們在這裡捕獲“this”
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);
this
引數
提供一個顯式的 this
引數。 this
引數是個假的引數,它出現在引數列表的最前面:
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),
// 這個函式現在顯式地指定它的被呼叫者必須是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);
現在TypeScript知道createCardPicker
期望在某個Deck
物件上呼叫。 也就是說 this
是Deck
型別的
this
引數在回撥函式裡
你可以也看到過在回撥函式裡的this
報錯,當你將一個函式傳遞到某個庫函式裡稍後會被呼叫時。 因為當回撥被呼叫的時候,它們會被當成一個普通函式呼叫, this
將為undefined
。
稍做改動,你就可以通過 this
引數來避免錯誤。 首先,庫函式的作者要指定 this
的型別:
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void
意味著addClickListener
期望onclick
是一個不需要this
的函式, 然後用this
註釋你的回撥程式碼.
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// 在這裡使用this會讓這個回撥在執行時崩潰.
this.info = e.message;
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // 報錯
指定了this
型別後,你顯式宣告onClickBad
必須在Handler
的例項上呼叫。 然後TypeScript會檢測到 addClickListener
要求函式帶有this: void
。 改變 this
型別來修復這個錯誤:
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// 這裡不能用this,因為它的型別是void!
console.log('clicked!');
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);
因為onClickGood
指定了this
型別為void
,因此傳遞addClickListener
是合法的。 當然了,這也意味著不能使用 this.info
. 如果你兩者都想要,你不得不使用箭頭函數了:
class Handler {
info: string;
onClickGood = (e: Event) => { this.info = e.message }
}
這是可行的因為箭頭函式不會捕獲this
,所以你總是可以把它們傳給期望this: void
的函式。 缺點是每個 Handler
物件都會建立一個箭頭函式。 另一方面,方法只會被建立一次,新增到 Handler
的原型鏈上。 它們在不同 Handler
物件間是共享的。
過載
JavaScript本身是個動態語言。 JavaScript裡函式根據傳入不同的引數而返回不同型別的資料是很常見的。
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x): any {
// 檢查一下看看是否引數是 object/array
// 如果是這樣的話, 引數給予deck, 我們選取pickedCard
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// 其他情況下只選取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
方法根據傳入引數的不同會返回兩種不同的型別。 如果傳入的是代表紙牌的物件,函式作用是從中抓一張牌。 如果使用者想抓牌,我們告訴他抓到了什麼牌。 但是這怎麼在型別系統裡表示呢。
方法是為同一個函式提供多個函式型別定義來進行函式過載。 編譯器會根據這個列表去處理函式的呼叫。 下面我們來過載 pickCard
函式。
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 {
// 檢查一下看看是否引數是 object/array
// 如果是這樣的話, 引數給予deck, 我們選取pickedCard
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// 其他情況下只選取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
函式在呼叫的時候會進行正確的型別檢查。
為了讓編譯器能夠選擇正確的檢查型別,它與JavaScript裡的處理流程相似。 它查詢過載列表,嘗試使用第一個過載定義。 如果匹配的話就使用這個。 因此,在定義過載的時候,一定要把最精確的定義放在最前面。
注意,function pickCard(x): any
並不是過載列表的一部分,而是具體的實現, 因此這裡只有兩個過載:一個是接收物件另一個接收數字。 以其它引數呼叫 pickCard
會產生錯誤。