在JavaScript中,函数是构成任何应用程序的基础块。通过函数,你得以实现建立抽象层、模仿类、信息隐藏和模块化。在TypeScript中,虽然已经存在类和模块化,但是函数依旧在如何去"处理"事件的问题上起关键作用。TypeScript在JavaScript的标准基础上给函数添加了一些新的功能使使用者可以更好的用函数处理工作。
函数
首先,和JavaScript一样,TypeScript中的函数可以创建命名函数和匿名函数。这样你就可以为应用程序选择最合适的方式,无论是定义一系列函数API还是一次性使用的函数。
快速的回顾下JavaScript中的这两种方法是怎么样的:
// 命名函数 function add(x, y) { return x+y; } // 匿名函数 var myAdd = function(x, y) { return x+y; };
在JavaScript中,函数可以使用函数外部的变量。当这么做的时候,我们称之为"捕获"这些变量。要理解它是怎么工作和衡量何时使用这项技术,这已经超出本文的内容范围了,透彻的理解这个机制在JavaScript和TypeScript中是多么重要的一部分是需要的。
var z = 100; function addToZ(x, y) { return x+y+z; }
函数类型
给函数添加类型
为之前的简单案例添加类型:
function add(x: number, y: number): number { return x+y; } var myAdd = function(x: number, y: number): number { return x+y; };
我们可以给每个参数指定类型,并且为函数本身return的值指定类型。TypeScript能够根据return语句推算出返回值的类型,所以很多情况下可以忽略它。
编写函数类型
现在我们已经为函数添加了类型,接下来为函数写出所有的类型:
var myAdd: (x:number, y:number)=>number = function(x: number, y: number): number { return x+y; };
函数类型包括两个部分:arguments(参数)的类型和return(返回)值的类型。当需要写所有的函数类型,这两部分是必需的。我们写参数类型就像写一个参数列表一样,每个参数给定一个名称和类型。这个名称只是为了增加可读性,我们可以这样写:
var myAdd: (baseValue:number, increment:number)=>number = function(x: number, y: number): number { return x+y; };
只要参数类型正确,它就被认为是有效的函数类型,而不用去在乎参数名称是否正确。
第二部分是返回值类型。我们在参数和返回值类型之间用肥肥的箭头(=>)来明确这个类型。正如前面所说,这只是函数类型的一部分,所以当不存在返回值时,应该使用"void",而不是任由它空着。
注:只有参数和返回值的类型组成了函数类型。捕获到的变量不会在类型中体现。实际上,捕获的变量属于函数"隐藏状态"部分的,并且也不是API的组成部分。
类型推断
在例子中,你可能已经注意到,当你在赋值语句的任意一边指定类型,TypeScript编译器都能够在另一边自动识别类型:
// myAdd 函数中所有的类型 var myAdd = function(x: number, y: number): number { return x+y; }; // 参数'x'和'y'是number类型 var myAdd: (baseValue:number, increment:number)=>number = function(x, y) { return x+y; };
这称为"上下文(语境)归类",一种类型推断的形式。这有助于减少工作量并且保持程序的类型。
可选参数和默认参数
和JavaScript不同,TypeScript函数中的每个参数都是必需的。这并不意味着不可以传入"null"值,当一个函数被调用的时候,编译器会检查用户是否为每一个参数提供值。编译器也会假设这些参数就是需要被传入函数的参数。简而言之,函数的传入参数的个数必须和函数所期望被传入参数的个数相等。
function buildName(firstName: string, lastName: string) { return firstName + " " + lastName; } var result1 = buildName("Bob"); // 错误,传的参数太少 var result2 = buildName("Bob", "Adams", "Sr."); // 错误,传的参数太多 var result3 = buildName("Bob", "Adams"); // 额,这是正确的
在JavaScript中,每一个参数都是可选的,用户可以在恰当的时候不用传某个参数。这样做就相当于传入"undefined"代替这个参数。在TypeScript中,我们可以在参数后面加上"?"符号,让这个参数变成可选参数。例如,我们想要"lastName"是可选的:
function buildName(firstName: string, lastName?: string) { if (lastName) return firstName + " " + lastName; else return firstName; } var result1 = buildName("Bob"); // 正常运行 var result2 = buildName("Bob", "Adams", "Sr."); // 错误,传的参数太多 var result3 = buildName("Bob", "Adams"); // 额,这是正确的
可选参数必须放在必需参数后面(存在必需参数的情况下)。假如我们要"firstName"变成可选参数而不是"lastName",我们需要改变函数参数的排序,将"firstName"放到后面。
在TypeScript中也可以为某个参数设置值,当用户未提供该参数时,将使用这个值。这被称为"默认值"。将上个例子中的"lastName"的默认值设置为"Smith":
function buildName(firstName: string, lastName = "Smith") { return firstName + " " + lastName; } var result1 = buildName("Bob"); // 正常运行 var result2 = buildName("Bob", "Adams", "Sr."); // 错误,传的参数太多 var result3 = buildName("Bob", "Adams"); // 额,这是正确的
和可选参数一样,默认参数必须放在必需参数后面(存在必需参数的情况下)。
可选参数和默认参数共享类型。如
function buildName(firstName: string, lastName?: string) {
和
function buildName(firstName: string, lastName = "Smith") {
共享同一个类型 "(firstName: string, lastName?: string)=>string"。
其他参数
必需参数,可选参数和默认参数都有以个共同点:它们只表示一个参数。有些时候可能想要多个参数,或者不知道具体有多少个参数最终需要被传入。在JavaScript中,你可以使用arguments来访问函数传入的所有参数。
在TypeScript中,你可以将所有的参数聚集到一个变量中:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" "); } var employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
其他参数被视为数量无限的可选参数。你可以一个都不传,也可以传任意你想传的个数。编译器将会生成一个你传入函数的参数数组,以省略号("...")之后的名称命名,你可以在函数中使用这个数组。
省略号也用在带有其他参数的函数类型:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" "); } var buildNameFun: (fname: string, ...rest: string[])=>string = buildName;
Lambdas和使用"this"
对于JavaScript程序员来说,关于"this"工作机制的话题算是老生常谈了。确实,学会如何使用"this"是JavaScript程序员成长中必须经历的。因为TypeScript是JavaScript的一个超集,TypeScript程序员也需要学会如何让使用"this"并且如何处理错误使用"this"引发的问题。假如要用来描述如何使用JavaScript中的"this",一整篇文章都可以写,并且该类文章也有很多。在这里只介绍基础知识。
在JavaScript中,"this"在函数被调用的时候被指定。这使得它强大而又灵活,只是你需要为理解函数执行的上下文付出代价。众所周知,这是很麻烦的,比如,当一个函数当作回调函数使用的时候。
来看一个例子:
var deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return function() { var pickedCard = Math.floor(Math.random() * 52); var pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } } } var cardPicker = deck.createCardPicker(); var pickedCard = cardPicker(); alert("card: " + pickedCard.card + " of " + pickedCard.suit);
试着运行这个例子,我们会得到一个错误,而不是预期的弹出警告框。因为'createCardPicker'返回的函数里"this"指向的是Window对象而不是"deck"对象。所以在这里调用"cardPicker()",将Window对象绑定到了"this"上。(注意:严格模式下,this是undefined而不是Window)
我们可以在返回函数的时候就绑定正确的"this",这样,无论后面怎么调用这个函数,"this"依旧会指向"deck"对象。
为了解决这问题,我们可以使用lambda语法代替JavaScript函数表达式。它将会在函数创建的时候自动捕获"this"变量,而不是在被调用的时候处理"this"。
var deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { // 注意: 下面这行现在是使用lambda语法, 更早的捕获'this' return () => { var pickedCard = Math.floor(Math.random() * 52); var pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } } } var cardPicker = deck.createCardPicker(); var pickedCard = cardPicker(); alert("card: " + pickedCard.card + " of " + pickedCard.suit);
想要了解关于"this"的更多信息,可以阅读Yahuda Katz的 理解JavaScript函数调用和"this"。
重载
JavaScript本身就是动态语言。根据传入参数的不同而返回不同类型的值在JavaScript函数中用的并不少。
var suits = ["hearts", "spades", "clubs", "diamonds"]; function pickCard(x) { // 检查我们处理的是什么类型的参数 // 如果是数组对象, 则给定deck并且选择card if (typeof x == "object") { var pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // 否则只选择card else if (typeof x == "number") { var pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; } } var myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }]; var pickedCard1 = myDeck[pickCard(myDeck)]; alert("card: " + pickedCard1.card + " of " + pickedCard1.suit); var pickedCard2 = pickCard(15); alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
这里的"pickCard"将会根据用户传入的参数不同而返回不同的数据。如用户传入的是代表"deck"的对象,则函数将会选择一个"card"并且返回。如果用户已经选择了"card",我们则会返回他选择的是哪个"card"。但是怎么在类型系统里描述这些呢。
解决方案是基于同一个函数提供多个函数类型,就如函数重载列表。这个列表将被编译器用来解决函数的调用。现在来创建一个重载列表,描述"pickCard"接收的是什么,返回的是什么。
var 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 { // 检查我们处理的是什么类型的参数 // 如果是数组对象, 则给定deck并且选择card if (typeof x == "object") { var pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // 否则只选择card else if (typeof x == "number") { var pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; } } var myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }]; var pickedCard1 = myDeck[pickCard(myDeck)]; alert("card: " + pickedCard1.card + " of " + pickedCard1.suit); var pickedCard2 = pickCard(15); alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
有了这些改变,重载后的"pickCard"函数会在被调用的时候进行合适的类型检查。
为了编译器选择正确的类型检查,它和JavaScript底层运行是相似的。它会查找重载列表,使用第一次定义的函数进行检查,如果能匹配,则调用当前这个函数。正是因为如此,一般都会将最明确的定义放在前面。
注意,"function pickCard(x: any): any"不是重载列表的一部分。所以它只有2次重载:一个针对的是object,一次针对的是number。调用"pickCard"并且传入其他类型的参数将会导致错误。