此页面已弃用

此手册页面已被替换,转到新页面

函数

函数是 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

任何可选参数都必须位于必需参数之后。如果我们想让名字可选,而不是姓氏,我们需要更改函数中参数的顺序,将名字放在列表的最后。

在 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

剩余参数被视为无限数量的可选参数。在为剩余参数传递参数时,您可以使用任意数量的参数;您甚至可以不传递任何参数。编译器将使用省略号 (...) 后面的名称构建一个传递参数的数组,允许您在函数中使用它。

省略号也用于具有剩余参数的函数的类型中。

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。如果你想要两者,那么你必须使用箭头函数。

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+

上次更新时间:2024 年 3 月 21 日