TypeScript 之函数
介绍
函数是 JavaScript
应用程序的基础,它帮助你实现抽象层、模拟类、信息隐藏和模块。在 TypeScript
里,虽然已经支持类、命名空间和模块,但函数任然是主要的定义行为的地方。 TypeScript
为 JavaScript
函数添加了额外的功能,让我们可以更容易的使用。
函数
和 JavaScript
一样, TypeScript
函数可以创建有名字的函数和匿名函数。 你可以随意选择适合应用程序的方式,不论是定义一系列 API
函数还是只使用一次的函数。
通过下面的例子可以迅速回想起这两种 JavaScript
中的函数:
// Named function
funnction add(x, y) {
return x + y;
}
// Anonymous function
const myAdd = function(x, y) {
return x + y;
}
在 JavaScript
里,函数可以使用函数体外部的变量。当函数这么做时,我们说它 捕获
了这些变量。至于为什么可以这样做以及其中的利弊超出了本文范围,但是深刻理解这个机制对学习 JavaScript
和 TypeScript
会非常有帮助。
let z = 10;
function addToZ(x, y, z) {
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 = function(
x: number,
y: number
): number {
return x + y;
};
函数类型包含两部分:
- 1、参数类型
- 2、返回值类型
当写出完整函数类型的时候,这两部分是需要的。我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。这个名字只是为了增加可读性。我们也可以这么写:
let myAdd: (baseValue: number, increment: number) => = function(x: number, y: number): number {
return x + y;
}
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
第二部分是返回值类型。对于返回值,我们在函数和返回值类型之前使用 =>
符号,使之清晰明了。如之前提到的,返回值类型是函数类型的必要部分,如果函数没有任何返回值,你也必须指定返回值类型为 void
而不能留空。
函数的类型只是由类型和返回值组成的。函数中使用的捕获变量不会体现在类型里。实际上,这些变量是函数的隐藏状态,并不是组成 API
的一部分。
推断类型
尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript
编译器会自动识别出类型:
// 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;
}
这叫做"按上下文归类",是类型推断的一种,它帮助我们更好的为程序指定类型。
可选参数和默认参数
TypeScript
里的每个函数参数都是必须的。这不是指不能传递 null
或 undefined
作为参数,而是说编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。间短地说,传递给一个函数地参数个数必须与函数期望的参数个数一致。
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
里,每个参数都是可选的,可传可不传。没传参的时候,它的值就是 undefined
。在 TypeScript
里我们可以在参数名旁使用 ?
实现可选参数的功能。比如,我们想让 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
可选参数必须跟在必须参数后面。如果上例我们想让 firstName
是可选的,那么就必须调整它们的位置,把 firstName
放在后面。
在 TypeScript
里,我们也可以为参数提供一个默认值,当用户没有传递这个参数或传递的值是 undefined
时,它们叫做有默认初始化值的参数。让我们修改上例,把 lastName
的默认值设置为 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
在所有必须参数后面的带初始默认话的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。也就是说可选参数与末尾的默认参数共享参数类型。
function buildName(firstName: string, lastName?: string) {
// ...
}
和
function buildName(firstName: string, lastName = 'Smith') {
// ...
}
共享同样的类型(firstName: string, lastName?: string
) => string
。默认参数的默认值消失了,只保留了它是一个可选参数的信息。
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined
值来获得默认值。例如,我们重写最后一个例子,让 undefined
是带默认值的参数:
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"
剩余参数
必须参数、默认参数和可选参数有个共同点:它们表示某一个参数。有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来,在 JavaScript
里,你可以使用 arguments
来 访问所有传入的参数。
在 TypeScript
里,你可以把所有参数收集到一个变量里:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + ' ' + restOfName.join(' ');
}
let employeeName = buildName('Joseph', 'Samuel', 'Lucas', 'MacKinzie');
// 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
的地方。如果你想了解 JavaScript
里的 this
是如何工作的,那么首先阅读 Yehuda Katz
写的Understanding JavaScript Function Invocation and "this"。 Yehuda Katz
的文章详细的阐述了 this
的内部工作原理,因此我们这里只做简单介绍。
this
和 箭头函数
JavaScript
里, this
的值在函数被调用的时候才会指定。这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。但众所周知,这不是一件很简单的事,尤其是返回一个函数或将函数当作参数传递的时候。
下面看一个例子:
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
是个函数,并且它又返回了一个函数。如果我们尝试运行这个程序,会发现它并没有弹出对话框而是报错了。因为 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() {
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
会警告你犯了一个错误,如果你给编译器设置了 --nolmplicitThis
标记。它会指出 this.suits[pickedSuit]
里的 this
的类型为 ay
。
this
参数
不幸的是,this.suits[pickedSuit]
的类型依旧为 any
。这是因为 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 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);
现在 TypeScript
知道 createCardPicker
期望在某个 Deck
对象上调用。也就是说 this
是 Deck
类型的,而非 any
类型,因此 --noImplicitThis
不会报错了。
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;
// 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
的实例上调用。然后 TypeScript
会检测 addClickListener
要求函数带有 this: void
。改变 this
类型来修复这个错误:
指定了 this 类型后,你显式声明 onClickBad 必须在 Handler 的实例上调用。 然后 TypeScript 会检测到 addClickListener 要求函数带有 this: void。 改变 this 类型来修复这个错误:
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);
因为 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 {
// 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
方法根据传入参数的不同会返回两种不同的类型。如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。如果用户想抓牌,我们告诉他抓到了什么牌。但是这怎么在类型系统里表示呢。
方法是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。
下面我们来重载 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 {
// 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
函数在调用的时候会进行正确的类型检测。
为了让编译器能够选择正确的检查类型,它与 JavaScript
里的处理流程相似。它查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。
注意,function pickCard(x): any
并不是重载列表的一部分,因此这里只要两个重载:一个接收对象,另一个接收数字,以其它参数调用 pickCard
会产生错误。
本文参考来源: TypeScript 函数