函数
简介
函数是 JavaScript 中任何应用程序的基本组成部分。它们是建立抽象、封装和模块的方式。在 TypeScript 中,尽管有类、命名空间和模块,但是函数仍然起着关键作用。TypeScript 在标准的 JavaScript 函数之上增加了一些新的功能让函数用起来更方便。
函数
和 JavaScript 一样,TypeScript 的函数可以是普通函数也可以是匿名函数,你可以选择最合适的方法来构建应用程序,无论是构建 API 相关函数还是从一个功能切换到另外一个功能。
我们来复习一下 JavaScript 中是如何做的。
// 普通函数
function add(x, y) {
return x + y;
}
// 匿名函数
let myAdd = function(x, y) { return x + y; };
在 JavaScript 中,函数可以引用函数体外部的变量。当我们这样做时,这种操作称为 变量捕获,尽管了解其工作原理(以及使用此技术时的取舍)超出了本文的范围,但是了解这种运行机制对使用 JavaScript 和 TypeScript 来说非常重要。
let z = 100;
function addToZ(x, y) {
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 可以通过 return 语句推断出返回类型,所以很多情况下我们无需声明函数的返回类型。
给函数加类型
我们已经写好了函数,现在让我们写出完整的函数的类型。
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number { return x + y; };
函数的类型有相同的两部分:参数的类型和返回值的类型,当我们写出完整的函数的类型时,两部分都是必须要有的。我们像参数列表一样列出参数类型列表,并为每个参数设置名字和类型,名字仅仅是为了提高可读性,我们也可以这样写。
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };
无论在函数类型中为参数指定什么名称,只要将参数类型设置好,就将其视为有效的函数类型。
第二部分是返回值,我们通过在参数和返回类型之间使用箭头(=>)来声明返回类型,就像前面提到的,这是函数类型必须要有的部分,所以如果一个函数没有返回值,你也必须要使用 void 关键字进行声明。
需要注意的是,只有参数和返回类型构成函数类型,捕获的变量不影响函数的类型。
类型推断
在学习上面的例子的时候,你可能已经注意到即使仅仅在赋值表达式的一侧有类型而另一侧没有的情况下,TypeScript 编译器也会找出其类型。
// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters 'x' and 'y' have the type number
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };
这叫 上下文类型,它是一种类型推断的方式,这有助于减少程序代码的输入。
可选和默认参数
在 TypeScript 中,函数认为每个参数都是必不可少的。这并不意味着不能给参数设置值为 null 或者 undefined。当函数被调用时,编译器将检查每个参数的值,编译器假定这些参数是传递给函数的唯一参数。简单点说,函数调用时传递给函数的参数数量必须与函数声明时期望的参数数量一致。
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right
在 JavaScript 中,每个参数都是可选的,使用时可以有这个参数也可以没有这个参数。当不给这个参数传值时,函数内部默认认为这个参数的值为 undefined。在 TypeScript 中,为了实现相同的功能,我们可以通过在函数参数的名字后面加上 ? 类实现。例如,我们现在让上面例子中的 last name 是可选的。
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
let result3 = buildName("Bob", "Adams"); // ah, just right
任何可选参数都必须定义在必选参数的后面,如果我们想定义 first name 为可选参数,而不是 last name,那么我们需要改变函数中两个参数的先后顺序,把 first name 放在参数列表的后面。
在 TypeScript 中,如果调用者没有为参数设置值或者值为 undefined,那么我们可以为参数设置一个默认值。它们称为默认初始化参数,我们还是用之前的例子,并设置 last name 的默认值为 Smith。
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
let result4 = buildName("Bob", "Adams"); // ah, just right
$跟在必选参数之后的默认初始化参数被认为是可选参数。像可选参数一样,它们在函数调用时可以不用传值。这意味着可选参数和跟在必选参数后面的默认初始化参数将共享类型通用性。
function buildName(firstName: string, lastName?: string) {
// ...
}
function buildName(firstName: string, lastName = "Smith") {
// ...
}
上面两段代码拥有同样的类型 (firstName: string, lastName?: string) => string。lastName 的默认值在类型中消失了,仅声明参数是可选的。
与可选参数不同,默认初始化参数不用必须出现在必选参数之后。如果默认初始化参数出现在必选参数之前,调用者需要显式地传递 undefined 以获取默认的初始值。例如,我们可以修改之前的例子,为 firstName 设置默认的初始值。
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"
剩余参数
必选、可选和默认参数都有一个共同点:它们一次只处理一个参数。有时候,你想要一起处理多个参数,或者你不清楚这个函数有多少个参数。在 JavaScript 中,你可以使用内置的 arguments 变量。
在 TypeScript 中,你可以把这些变量合并到一个变量中。
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
// employeeName will be "Joseph Samuel Lucas MacKinzie"
let employeeName = buildName("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。幸运的是,TypeScript 中有很多技巧让你避免错误地使用 this。如果你想学习 JavaScript 中 this 是如何工作的,首先请看这篇文章,这篇文章解释了 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();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
注意 createCardPicker 是一个函数,它本身也返回一个函数。如果我们运行这个例子,会返回错误而不是期待的告警弹框,这是因为在 createCardPicker 函数中使用的 this 将被设置为全局对象(window)而不是 deck 对象,因为我们单独调用 cardPicker(),这种顶级的非方法语法将使用全局对象填充 this。(注意,在严格模式下,this 有可能是 undefined 而非全局对象)
在函数返回之前,我们可以通过保证函数使用正确的值来填充 this 变量来解决这个问题。不管以后这个函数如何被调用,它将始终指向原来的 deck 对象。为了做到这一点,我们使用 ES6 的箭头函数代替函数表达式。箭头函数在函数创建时而不是调用时捕获 this 的值。
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);
如果将 --noImplicitThis 标志传递给编译器,当有这些问题时 TypeScript 将进行告警。编译器将指出 this.suits[pickedSuit] 中的 this 的类型为 any。
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),
// 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);
现在 TypeScript 知道了 createCardPicker 函数需要在 Deck 类型的对象上调用。这意味着 this 现在是 Deck 类型,不是 any 类型,因此 --noImplicit 不会触发任何错误。
回调函数中的 this 参数
当你向第三方框架传递一个函数并让框架之后回调该函数时,你也有可能在回调函数中错误地使用 this。因为框架会像调用普通函数一样调用你的函数,this 的值将是 undefined。通过一些方法可以避免出现这种错误。首先,框架代码中需要对回调中的 this 进行标注。
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void 表示 addClickListener 期望 onclick 是一个不需要 this 的函数。其次,在代码中标注 this。
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!
使用 this 注解,你明确指出必须在 Handler 的实例上调用 onClickBad。TypeScript 检测到 addClickListener 需要具有 this:void 的函数。要解决该错误,只需要更改 this 的类型。
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);
因为 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.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);
这里,pickCard 函数将根据调用者传入的参数值返回不同的对象。如果传入的是代表 deck 的对象,函数将返回 pickedCard。如果传入的是 number,则告诉调用者拿的是哪个 card 。那我们如何用 TypeScript 来描述上面的逻辑呢?
答案是使用重载,该列表是编译器将用来解析函数调用的列表。让我们创建一个重载列表,以描述 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 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);
更改代码后,重载为我们提供了对 pickCard 函数的类型检查能力。
为了使编译器选择正确的类型检查方式,它遵循与基础 JavaScript 相似的过程。它查看重载列表,并在第一次重载之前尝试使用提供的参数调用该函数。如果找到匹配项,它将选择此重载作为正确的重载。因此,习惯上按从最具体到最不具体的顺序对重载进行排序。
注意,function pickCard(x): any 代码段不是重载的成员,所以这里只有两个重载:一个的参数是对象,另一个的参数是数字。使用任何其他参数调用 pickCard 会导致错误。