携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情
和 JavaScript 一样,TypeScript 函数可以创建有名字的函数和匿名函数,通过下面的例子可以迅速回想起这两种 JavaScript 中的函数:
// 命名函数
function add(x, y) {
return x + y
}
// 匿名函数
let myAdd = function(x, y) {
return x + y;
}
为函数定义类型
函数类型包含两部分:参数类型和返回值类型。当写出完整函数类型的时候,这两部分都是需要的。我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。这个名字只是为了增加可读性。 我们也可以这么写:
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number {
return x + y
}
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用(=>)符号,使之清晰明了。 如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void 而不能留空。
可选参数、默认参数、剩余参数
传递给一个函数的参数个数必须与函数期望的参数个数一致。
function buildName(firstName: string, lastName: string) {
return firstName + ' ' + lastName;
}
let result1 = buildName('Bob') // Error, 参数过少
let result2 = buildName('Bob', 'Adams', 'Sr.'); // Error, 参数过多
let result3 = buildName('Bob', 'Adams'); // OK
JavaScript 里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是 undefined。 在TypeScript 里我们可以在参数名旁使用 ? 实现可选参数的功能。 比如,我们想让 lastName 是可选的:
function buildName(firstName: string, lastName?: string): string {
if (lastName)
return firstName + ' ' + lastName
else
return firstName
}
可选参数必须跟在必须参数后面。 如果上例我们想让 firstName 是可选的,那么就必须调整它们的位置,把 firstName 放在后面。
在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是 undefined 时。 它们叫做有默认初始化值的参数。 让我们修改上例,把lastName 的默认值设置为 "Smith"。
function buildName(firstName: string, lastName = 'Smith'): string {
return firstName + ' ' + lastName
}
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined 值来获得默认值。 例如,我们重写最后一个例子,让 firstName 是带默认值的参数:
function buildName(firstName = 'Will', lastName: string): string {
return firstName + ' ' + lastName
}
let result1 = buildName('Bob') // Error, 参数过少
let result2 = buildName('Bob', 'Adams', "Sr.") // Error, 参数过多
let result3 = buildName('Bob', 'Adams') // OK, 返回 "Bob Adams"
let result4 = buildName(undefined, 'Adams') // OK, 返回 "Will Adams"
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在 JavaScript 里,你可以使用 arguments 来访问所有传入的参数。
在 TypeScript 里,你可以把所有参数收集到一个变量里:
function buildName(firstName: string, ...restOfName: string[]): string {
return firstName + ' ' + restOfName.join(' ')
}
let employeeName = buildName('Joseph', 'Samuel', 'Lucas', 'MacKinzie')
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...)后面给定的名字,你可以在函数体内使用这个数组。
this
学习如何在 JavaScript 里正确使用 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()
console.log('card: ' + pickedCard.card + ' of ' + pickedCard.suit)
可以看到 createCardPicker 是个函数,并且它又返回了一个函数。如果我们尝试运行这个程序,会发现它并没有输出而是报错了。 因为 createCardPicker 返回的函数里的 this 被设置成了 global 而不是 deck 对象。 因为我们只是独立的调用了 cardPicker()。 顶级的非方法式调用会将 this 视为 global。
为了解决这个问题,我们可以在函数被返回时就绑好正确的this。 这样的话,无论之后怎么使用它,都会引用绑定的deck 对象。
// 第一种方式 var that = this
let deck = {
...
createCardPicker: function() {
let that = this
return function() {
let pickedCard = Math.floor(Math.random() * 52)
let pickedSuit = Math.floor(pickedCard / 13)
return {suit: that.suits[pickedSuit], card: pickedCard % 13}
}
}
}
// 第二种方式 使用箭头函数,箭头函数能保存函数创建时的 `this` 值,而不是调用时的值。
let deck = {
...
createCardPicker: function() {
return () => {
let pickedCard = Math.floor(Math.random() * 52)
let pickedSuit = Math.floor(pickedCard / 13)
return {suit: this.suits[pickedSuit], card: pickedCard % 13}
}
}
}
this 参数
在上述的例子中 this.suits[pickedSuit] 的类型为 any,这是因为 this 来自对象字面量里的函数表达式。 修改的方法是,提供一个显式的 this 参数。 this 参数是个假的参数,它出现在参数列表的最前面。
让我们往例子里添加一些接口,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),
// NOTE: 函数现在显式指定其被调用方必须是 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)
现在 TypeScript 知道 createCardPicker 期望在某个 Deck 对象上调用。也就是说 this 是 Deck 类型的,而非 any。
重载
JavaScript 本身是个动态语言。JavaScript 里函数根据传入不同的参数而返回不同类型的数据的场景是很常见的。
let suits = ['hearts', 'spades', 'clubs', 'diamonds']
function pickCard(x): any {
if (Array.isArray(x)) {
let pickedCard = Math.floor(Math.random() * x.length)
return pickedCard
} 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)];
console.log('card: ' + pickedCard1.card + ' of ' + pickedCard1.suit)
let pickedCard2 = pickCard(15)
console.log('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 {
if (Array.isArray(x)) {
let pickedCard = Math.floor(Math.random() * x.length)
return pickedCard
} 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)];
console.log('card: ' + pickedCard1.card + ' of ' + pickedCard1.suit)
let pickedCard2 = pickCard(15)
console.log('card: ' + pickedCard2.card + ' of ' + pickedCard2.suit)
这样改变后,重载的 pickCard 函数在调用的时候会进行正确的类型检查。
为了让编译器能够选择正确的检查类型,它与 JavaScript 里的处理流程相似。它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。
注意,function pickCard(x): any 并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象数组,另一个接收数字。 以其它参数调用 pickCard 会产生错误。