TypeScript - 函数
# 1. 函数介绍
函数是 JavaScript 应用程序的基础。它们帮助开发者实现抽象层、模拟类、信息隐藏和模块化。在 TypeScript 中,虽然已经支持类、命名空间和模块,但函数仍然是主要用于定义行为的工具。TypeScript 为 JavaScript 函数添加了额外的功能,使其使用更加便捷和强大。
# 2. 基本示例
与 JavaScript 一样,TypeScript 中可以创建命名函数和匿名函数。你可以根据应用程序的需求,选择适合的函数定义方式,既可以定义一系列 API 函数,也可以定义只会使用一次的函数。
下面的示例展示了这两种 JavaScript 中的函数定义方式:
// 命名函数
function add(x, y) {
return x + y;
}
// 匿名函数
let myAdd = function(x, y) {
return x + y;
}
2
3
4
5
6
7
8
9
在 JavaScript 中,函数可以使用函数体外部的变量。当函数这么做时,我们说它「捕获」了这些变量。这个特性可以让函数访问并使用其所在作用域中的变量。对于深入理解 JavaScript 和 TypeScript,这个机制的理解是非常重要的。
let z = 100;
// 函数 addToZ 捕获了外部变量 z
function addToZ(x, y) {
return x + y + z;
}
2
3
4
5
6
# 3. 函数类型
# 为函数定义类型
在 TypeScript 中,我们可以为函数的参数和返回值添加类型注解,从而提高代码的可读性和安全性。下面是为函数添加类型注解的示例:
// 为函数 add 添加参数类型和返回值类型
function add(x: number, y: number): number {
return x + y;
}
// 为匿名函数 myAdd 添加参数类型和返回值类型
let myAdd = function(x: number, y: number): number {
return x + y;
}
2
3
4
5
6
7
8
9
我们可以为每个参数添加类型注解,并为函数本身指定返回值类型。TypeScript 能够根据返回语句自动推断出返回值类型,但显式地指定返回值类型可以提高代码的可读性和安全性。
# 书写完整函数类型
现在我们已经为函数指定了类型,接下来让我们写出函数的完整类型。
// 为 myAdd 变量指定完整的函数类型
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number {
return x + y;
}
2
3
4
5
函数类型包含两个部分:参数类型和返回值类型。当写出完整函数类型时,这两部分都是必需的。我们可以以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。参数名字仅用于增加可读性。以下写法也是有效的:
// 为 myAdd 变量指定完整的函数类型
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number {
return x + y;
}
2
3
4
5
只要参数类型是匹配的,那么它就是有效的函数类型,不需要关心参数名是否一致。
第二部分是返回值类型。对于返回值,我们在函数和返回值类型之间使用 =>
符号,使之清晰明了。如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void
而不是留空。
函数的类型只由参数类型和返回值类型组成。函数中使用的捕获变量不会体现在类型里。实际上,这些变量是函数的隐藏状态,并不是组成 API 的一部分。
# 推断类型
在 TypeScript 中,如果你在赋值语句的一边指定了类型,而另一边没有指定类型,TypeScript 编译器会自动识别出类型。这种机制被称为「按上下文归类」,它是类型推论的一种,帮助我们更好地为程序指定类型。
// 通过类型推断,myAdd 变量会被推断为一个函数类型
let myAdd = function(x: number, y: number): number {
return x + y;
}
// 通过类型推断,函数的参数类型会被自动推断
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) {
return x + y;
}
2
3
4
5
6
7
8
9
10
通过类型推断,TypeScript 编译器能够在不显式指定类型的情况下推断出函数参数和返回值的类型,这使得代码更简洁,但依然安全。
# 4. 可选参数和默认参数
在 TypeScript 中,默认情况下,函数的每个参数都是必须的。这意味着在调用函数时,必须为每个参数提供值,否则编译器会报错。参数个数必须与函数定义的参数个数一致。
# 必需参数
TypeScript 中的函数参数默认都是必需的,传递给函数的参数个数必须与定义时一致。
示例:
// 定义一个函数 buildName,接受两个必需参数
function buildName(firstName: string, lastName: string): string {
return firstName + ' ' + lastName;
}
// 函数调用示例
let result1 = buildName('Bob'); // 错误, 参数过少
let result2 = buildName('Bob', 'Adams', 'Sr.'); // 错误, 参数过多
let result3 = buildName('Bob', 'Adams'); // 正确,返回 "Bob Adams"
2
3
4
5
6
7
8
9
# 可选参数
在 TypeScript 中,可以通过在参数名后面添加 ?
来表示该参数是可选的。可选参数可以在函数调用时省略。
示例:
// 定义一个函数 buildName,其中 lastName 是可选参数
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
// 函数调用示例
let result1 = buildName('Bob'); // 正确,返回 "Bob"
let result2 = buildName('Bob', 'Adams', 'Sr.'); // 错误, 参数过多
let result3 = buildName('Bob', 'Adams'); // 正确,返回 "Bob Adams"
2
3
4
5
6
7
8
9
10
11
12
13
可选参数必须放在必需参数的后面。
# 默认参数
在 TypeScript 中,可以为参数指定默认值,当调用函数时,如果不提供该参数或传递 undefined
,将使用默认值。
示例:
// 定义一个函数 buildName,其中 lastName 具有默认值 "Smith"
function buildName(firstName: string, lastName = 'Smith'): string {
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'); // 正确,返回 "Bob Adams"
2
3
4
5
6
7
8
9
10
带有默认值的参数不需要放在必需参数的后面。如果出现在前面,调用函数时必须传递 undefined
以获得默认值。
示例:
// 定义一个函数 buildName,其中 firstName 具有默认值 "Will"
function buildName(firstName = 'Will', lastName: string): 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"
2
3
4
5
6
7
8
9
10
# 剩余参数
有时候,我们可能需要同时处理多个参数,或者在函数定义时不知道会有多少参数传递进来。在 JavaScript 中,可以使用 arguments
对象来访问所有传入的参数。在 TypeScript 中,可以使用剩余参数(rest parameters)来处理这种情况。
示例:
// 定义一个函数 buildName,使用剩余参数 restOfName 接收多个参数
function buildName(firstName: string, ...restOfName: string[]): string {
return firstName + ' ' + restOfName.join(' '); // 使用 join 方法拼接剩余参数
}
// 函数调用示例
let employeeName = buildName('Joseph', 'Samuel', 'Lucas', 'MacKinzie'); // 返回 "Joseph Samuel Lucas MacKinzie"
2
3
4
5
6
7
剩余参数会被当做个数不限的可选参数。可以一个都没有,也可以有任意多个。编译器会将它们打包成一个数组,供函数体内使用。
在函数类型定义中,也可以使用省略号(...
)表示带有剩余参数的函数:
示例:
// 定义一个函数类型 buildNameFun,接收一个必需参数 fname 和多个剩余参数
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName; // 将 buildName 函数赋值给 buildNameFun
2
使用 TypeScript 的可选参数、默认参数和剩余参数特性,可以更加灵活地定义函数,并处理不确定数量的参数。
# 5. this
在 JavaScript 中正确地使用 this
是一项重要的技能,因为它的行为可能会让人感到困惑。由于 TypeScript 是 JavaScript 的超集,TypeScript 程序员也需要理解 this
的工作机制,并在代码中正确使用它。幸运的是,TypeScript 可以帮助我们在编译时检测一些 this
使用上的错误。如果你想深入了解 JavaScript 中的 this
,可以阅读 Yehuda Katz 写的 Understanding JavaScript Function Invocation and "this" (opens new window),这篇文章详细解释了 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);
// 这里的 this 指向全局对象,而不是 deck 对象
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
console.log('card: ' + pickedCard.card + ' of ' + pickedCard.suit);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在这个例子中,createCardPicker
是一个函数,它返回一个新的函数。当我们调用 cardPicker()
时,this
被设置为全局对象而不是 deck
对象,因为我们是以顶级的非方法调用方式来调用 cardPicker()
。要解决这个问题,我们可以在函数被创建时绑定正确的 this
值。这可以通过使用 ECMAScript 6 的箭头函数来实现,箭头函数会捕获函数创建时的 this
值,而不是调用时的值。
let deck = {
suits: ['hearts', 'spades', 'clubs', 'diamonds'],
cards: Array(52),
createCardPicker: function() {
// 使用箭头函数
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
// 这里的 this 正确地指向 deck 对象
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
console.log('card: ' + pickedCard.card + ' of ' + pickedCard.suit);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# this 参数
在上述例子中,this.suits[pickedSuit]
的类型是 any
,因为 this
来自对象字面量里的函数表达式。我们可以通过显式的 this
参数来解决这个问题。this
参数是一个假的参数,它出现在参数列表的最前面。
function f(this: void) {
// 确保 this 在此独立函数中不可用
}
2
3
接下来,让我们在例子中添加一些接口,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();
console.log('card: ' + pickedCard.card + ' of ' + pickedCard.suit);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
现在,TypeScript 知道 createCardPicker
函数期望在某个 Deck
对象上调用,也就是说 this
是 Deck
类型的,而非 any
。
# this 参数在回调函数里
你可能也遇到过在回调函数中 this
的问题,当你将一个函数传递给某个库函数,稍后会被调用时,因为回调函数被当作普通函数调用,this
将是 undefined
。通过使用 this
参数,我们可以避免这些错误。首先,库函数的作者要指定 this
的类型。
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
2
3
this: void
意味着 addClickListener
期望传入的 onclick
方法不需要 this
。
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
class Handler {
type: string;
onClickBad(this: Handler, e: Event) {
this.type = e.type;
}
}
let h = new Handler();
let uiElement: UIElement = {
addClickListener() {}
}
// 错误,因为 onClickBad 需要 this: Handler
uiElement.addClickListener(h.onClickBad);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
通过指定 this
类型,TypeScript 可以检测到 addClickListener
要求 onclick
方法的 this
是 void
,从而产生错误。
class Handler {
type: string;
onClickGood(this: void, e: Event) {
console.log('clicked!');
}
}
let h = new Handler();
let uiElement: UIElement = {
addClickListener() {}
}
// 正确,因为 onClickGood 的 this 是 void
uiElement.addClickListener(h.onClickGood);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
因为 onClickGood
指定了 this
类型为 void
,因此传递给 addClickListener
是合法的。当然了,这也意味着不能使用 this
。如果你需要访问 this
,可以使用箭头函数。
class Handler {
type: string;
// 使用箭头函数来捕获 this
onClickGood = (e: Event) => {
this.type = e.type;
}
}
2
3
4
5
6
7
8
这是可行的,因为箭头函数不会捕获 this
,所以你总是可以将它们传递给期望 this: void
的函数。
# 6. 函数重载
函数重载:函数名称相同,但参数的个数或类型不同。
JavaScript 是一种动态语言,函数可以根据传入参数的不同而返回不同类型的数据,这种特性在 JavaScript 中很常见。
示例:
// 定义一个函数 getMessage,接受 number 或 string 类型参数,并返回 number 或 string 类型
function getMessage(type: number | string): number | string {
if (typeof type === "number") {
return 27;
} else {
return "kele";
}
}
// 报错,因为无法确定返回类型是 number 还是 string
let msg: number = getMessage(1);
2
3
4
5
6
7
8
9
10
11
在上面的例子中,函数 getMessage
的返回类型始终是 number | string
,但实际上,如果参数是 number
,返回值一定是 number
;如果参数是 string
,返回值一定是 string
。出现这个问题的原因是编译器无法在编译时确定入参类型,从而无法确定返回值类型。
可以通过函数重载来解决这个问题,为函数提供多个重载签名。
示例:
// 重载签名
function getMessage(type: number): number;
// 重载签名
function getMessage(type: string): string;
// 实现签名
function getMessage(type: number | string): number | string {
if (typeof type === "number") {
return 27;
} else {
return "kele";
}
}
// 编译器会根据参数类型调用相应的重载签名
let msg = getMessage(1);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在这个例子中,前两个 getMessage
函数是重载签名,最后一个是实现签名。TypeScript 会根据调用时的参数类型选择合适的重载签名进行调用。
# 函数重载规则
至少有一个实现签名:实现签名提供了函数的具体实现。
可以有一个或多个重载签名:重载签名定义了不同参数类型或个数的函数签名。
外部调用时只能调用重载签名:在外部调用函数时,只能使用重载签名,不能直接调用实现签名。
调用时根据参数类型选择合适的重载签名:编译器会根据传递的参数类型选择相应的重载签名。
只有一个函数体:实现签名提供了唯一的函数体,所有的重载签名共享该函数体的实现。
函数重载可以让函数根据不同的参数类型返回不同的类型,这使得函数更具灵活性和可扩展性。在 TypeScript 中,函数重载通过重载签名和实现签名的组合实现。