此頁面已過時

此手冊頁面已被替換,請前往新頁面

函式

函式是 JavaScript 中任何應用程式的基本構建塊。透過函式,你可以構建抽象層、模擬類、隱藏資訊以及實現模組化。在 TypeScript 中,儘管存在類、名稱空間和模組,但函式在描述如何“執行”任務方面仍然起著關鍵作用。此外,TypeScript 還為標準 JavaScript 函式添加了一些新功能,使它們更易於使用。

函式

首先,與 JavaScript 一樣,TypeScript 函式既可以作為命名函式建立,也可以作為匿名函式建立。這使你可以根據應用程式的需求選擇最合適的方法,無論是構建 API 中的函式列表,還是建立傳遞給其他函式的一次性函式。

快速回顧一下這兩種方法在 JavaScript 中的樣子

ts
// Named function
function add(x, y) {
return x + y;
}
 
// Anonymous function
let myAdd = function (x, y) {
return x + y;
};
Try

和 JavaScript 一樣,函式可以引用函式體之外的變數。當它們這樣做時,被稱為“捕獲”了這些變數。雖然理解其工作原理(以及使用此技術的權衡)超出了本文的範圍,但紮實地掌握這種機制是如何運作的,是使用 JavaScript 和 TypeScript 的重要部分。

ts
let z = 100;
 
function addToZ(x, y) {
return x + y + z;
}
Try

函式型別

為函式新增型別

讓我們為前面提到的簡單示例新增型別

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

我們可以為每個引數新增型別,然後為函式本身新增返回型別。TypeScript 可以透過檢查 return 語句來推斷返回型別,因此在許多情況下,我們可以選擇性地省略它。

編寫函式型別

現在我們已經為函式添加了型別,讓我們看看函式型別的各個組成部分,寫出完整的函式型別。

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

函式型別包含兩個部分:引數型別和返回型別。編寫完整的函式型別時,這兩部分都是必需的。我們像引數列表一樣寫出引數型別,為每個引數指定名稱和型別。這裡的名稱僅用於提高可讀性。我們也可以這樣寫:

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

只要引數型別匹配,它就被認為是該函式的有效型別,無論你在函式型別中為引數指定了什麼名稱。

第二部分是返回型別。我們使用箭頭 (=>) 來分隔引數和返回型別,從而明確其返回型別。如前所述,這是函式型別的必需部分,因此如果函式不返回任何值,你應該使用 void,而不是省略它。

值得注意的是,只有引數和返回型別構成了函式型別。被捕獲的變數不會體現在型別中。實際上,被捕獲的變數是任何函式“隱藏狀態”的一部分,並不構成其 API。

推斷型別

在嘗試示例時,你可能注意到即使你只在等式的一側添加了型別,TypeScript 編譯器也能算出型別:

ts
// The parameters 'x' and 'y' have the type number
let myAdd = function (x: number, y: number): number {
return x + y;
};
 
// myAdd has the full function type
let myAdd2: (baseValue: number, increment: number) => number = function (x, y) {
return x + y;
};
Try

這被稱為“上下文型別化”,是型別推斷的一種形式。這有助於減少為程式新增型別的工作量。

可選引數和預設引數

在 TypeScript 中,預設假設函式的每個引數都是必需的。這並不意味著不能傳入 nullundefined,而是指在呼叫函式時,編譯器會檢查使用者是否為每個引數提供了值。編譯器還會假設這些引數是函式僅有的引數。簡而言之,傳遞給函式的引數數量必須與函式預期的引數數量匹配。

ts
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
 
let result1 = buildName("Bob"); // error, too few parameters
Expected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
Expected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.
let result3 = buildName("Bob", "Adams"); // ah, just right
Try

在 JavaScript 中,每個引數都是可選的,使用者可以隨意省略它們。當用戶省略時,它們的值為 undefined。我們可以在 TypeScript 中透過在引數名後新增 ? 來實現此功能。例如,假設我們想讓上面的姓氏引數變為可選的:

ts
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
Expected 1-2 arguments, but got 3.2554Expected 1-2 arguments, but got 3.
let result3 = buildName("Bob", "Adams"); // ah, just right
Try

任何可選引數都必須位於必需引數之後。如果我們想讓名字(first name)變為可選,而不是姓氏(last name),我們需要更改函式的引數順序,將名字放在引數列表的最後。

在 TypeScript 中,我們還可以為引數設定預設值,以便在使用者未提供值或傳入 undefined 時使用。這些被稱為預設初始化引數。讓我們拿上面的例子,並將姓氏的預設值設為 "Smith"

ts
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
Expected 1-2 arguments, but got 3.2554Expected 1-2 arguments, but got 3.
let result4 = buildName("Bob", "Adams"); // ah, just right
Try

位於所有必需引數之後的預設初始化引數會被視為可選引數。就像可選引數一樣,在呼叫對應的函式時可以省略它們。這意味著可選引數和尾隨的預設引數在型別上具有共通性,因此兩者:

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

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

共享相同的型別 (firstName: string, lastName?: string) => stringlastName 的預設值不會體現在型別中,只會體現出該引數是可選的。

與普通的可選引數不同,預設初始化引數不需要出現在必需引數之後。如果預設初始化引數位於必需引數之前,使用者需要顯式傳入 undefined 才能獲取預設值。例如,我們可以重寫上一個例子,僅為 firstName 設定預設值:

ts
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
 
let result1 = buildName("Bob"); // error, too few parameters
Expected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
Expected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.
let result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"
Try

剩餘引數

必需引數、可選引數和預設引數有一個共同點:它們一次處理一個引數。有時,你希望將多個引數作為一個組來處理,或者你可能不知道函式最終會接收多少個引數。在 JavaScript 中,你可以使用每個函式體內部可見的 arguments 變數直接處理引數。

在 TypeScript 中,你可以將這些引數收集到一個變數中:

ts
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
 
// employeeName will be "Joseph Samuel Lucas MacKinzie"
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
Try

剩餘引數(Rest parameters)被視為數量無限的可選引數。當傳入剩餘引數的實參時,你可以傳入任意數量;甚至可以一個都不傳。編譯器會將傳入的引數構建成一個數組,其名稱取自省略號 (...) 之後指定的名稱,從而允許你在函式中使用它。

省略號也用於帶有剩餘引數的函式型別中:

ts
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
 
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
Try

this

學習如何在 JavaScript 中使用 this 是一種必經之路。由於 TypeScript 是 JavaScript 的超集,TypeScript 開發人員也需要學習如何使用 this,以及如何識別它何時被錯誤使用。幸運的是,TypeScript 讓你能夠通過幾種技術手段捕捉 this 的錯誤使用。如果你需要學習 this 在 JavaScript 中是如何工作的,請先閱讀 Yehuda Katz 的 理解 JavaScript 函式呼叫與 “this”。Yehuda 的文章很好地解釋了 this 的內部工作原理,因此我們在這裡只介紹基礎知識。

this 與箭頭函式

在 JavaScript 中,this 是一個在函式被呼叫時設定的變數。這使它成為一個非常強大且靈活的功能,但代價是必須時刻了解函式執行時的上下文。這非常容易混淆,尤其是在返回函式或將函式作為引數傳遞時。

讓我們看一個例子:

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

請注意,createCardPicker 本身就是一個返回函式的函式。如果我們嘗試執行這個示例,會得到一個錯誤,而不是預期的警告框。這是因為在 createCardPicker 建立的函式中使用的 this 將被設定為 window,而不是我們的 deck 物件。這是因為我們獨立呼叫了 cardPicker()。像這樣的頂層非方法語法呼叫會使用 window 作為 this。(注意:在嚴格模式下,this 將是 undefined 而不是 window)。

我們可以透過確保函式在返回以供後續使用前繫結到正確的 this 來解決這個問題。這樣,無論它隨後如何使用,它仍然能夠看到原始的 deck 物件。為了做到這一點,我們將函式表示式更改為使用 ECMAScript 6 的箭頭語法。箭頭函式會捕獲建立函式時的 this,而不是呼叫時的 this

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

更好的是,如果你向編譯器傳遞了 noImplicitThis 標誌,TypeScript 將在你犯這個錯誤時發出警告。它會指出 this.suits[pickedSuit] 中的 this 型別為 any

this 引數

遺憾的是,this.suits[pickedSuit] 的型別仍然是 any。這是因為 this 來自物件字面量內部的函式表示式。要修復此問題,你可以提供一個顯式的 this 引數。this 引數是出現在函式引數列表最前面的虛假引數:

ts
function f(this: void) {
// make sure `this` is unusable in this standalone function
}

讓我們在上面的示例中新增幾個介面 CardDeck,使型別更清晰且更易於重用:

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

現在 TypeScript 知道 createCardPicker 期望在 Deck 物件上被呼叫。這意味著 this 的型別現在是 Deck 而不是 any,因此 noImplicitThis 不會產生任何錯誤。

回撥函式中的 this 引數

當你將函式傳遞給庫,而庫隨後會呼叫它們時,你在回撥函式中也可能會遇到 this 相關錯誤。由於呼叫你的回撥函式的庫會像呼叫普通函式一樣呼叫它,因此 this 將是 undefined。透過一些工作,你也可以使用 this 引數來防止回撥函式的錯誤。首先,庫作者需要使用 this 註釋回撥型別:

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

this: void 表示 addClickListener 期望 onclick 是一個不需要 this 型別的函式。其次,用 this 註釋你的呼叫程式碼:

ts
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!
Argument of type '(this: Handler, e: Event) => void' is not assignable to parameter of type '(this: void, e: Event) => void'. The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'Handler'.2345Argument of type '(this: Handler, e: Event) => void' is not assignable to parameter of type '(this: void, e: Event) => void'. The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'Handler'.
Try

有了 this 註釋,你就可以顯式宣告 onClickBad 必須在 Handler 的例項上被呼叫。然後 TypeScript 會檢測到 addClickListener 需要一個帶有 this: void 的函式。要修復錯誤,請更改 this 的型別:

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

因為 onClickGood 將其 this 型別指定為 void,所以傳遞給 addClickListener 是合法的。當然,這也意味著它不能使用 this.info。如果你既想使用 this 又想傳遞該函式,那麼你將不得不使用箭頭函式:

ts
class Handler {
info: string;
onClickGood = (e: Event) => {
this.info = e.message;
};
}
Try

這之所以有效,是因為箭頭函式使用外部的 this,所以你可以始終將它們傳遞給期望 this: void 的地方。缺點是每個 Handler 型別的物件都會建立一個箭頭函式。另一方面,方法只建立一次並掛載到 Handler 的原型上。它們在所有 Handler 型別的物件之間共享。

過載

JavaScript 本質上是一種非常動態的語言。單個 JavaScript 函式根據傳入引數的形態返回不同型別的物件並不罕見。

ts
let suits = ["hearts", "spades", "clubs", "diamonds"];
 
function pickCard(x: any): 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);
Try

在這裡,pickCard 函式會根據使用者傳入的內容返回兩種不同的東西。如果使用者傳入代表牌堆的物件,函式將選擇一張牌;如果使用者選擇了一張牌,我們會告訴他們選擇了哪張牌。但是我們該如何向型別系統描述這一點呢?

答案是為同一個函式提供多個函式型別,作為過載列表。編譯器將使用此列表來解析函式呼叫。讓我們建立一個過載列表來描述 pickCard 接收什麼以及返回什麼:

ts
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): 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);
Try

透過此更改,過載現在可以對 pickCard 函式的呼叫進行型別檢查。

為了讓編譯器選擇正確的型別檢查,它遵循與底層 JavaScript 類似的過程。它檢視過載列表,從第一個過載開始,嘗試使用提供的引數呼叫函式。如果找到匹配項,它就會選擇該過載作為正確的過載。因此,通常的做法是將過載按從最具體到最不具體的順序排列。

請注意,function pickCard(x): any 部分不屬於過載列表,因此它只有兩個過載:一個接收物件,一個接收數字。用任何其他引數型別呼叫 pickCard 都會導致錯誤。

TypeScript 文件是一個開源專案。歡迎 傳送 Pull Request 幫助我們改進這些頁面 ❤

此頁面的貢獻者
RCRyan Cavanaugh (55)
DRDaniel Rosenwasser (23)
OTOrta Therox (18)
NSNathan Shively-Sanders (4)
MFMartin Fischer (1)
24+

最後更新:2026 年 3 月 27 日