函数
函数是JavaScript中任意应用程序的基本构件块。可以在函数基础上建立抽象层,模拟类,信息隐藏,模块。在TypeScript中,虽然已经有类和模块,但函数函数仍然扮演着如何做事的关键角色。TypeScript还在标准JavaScript 函数基础上增加了一些新的能力,来使得函数更容易使用。
函数
TypeScript与JavaScript一样,都可以创建命名函数,也可以创建匿名函数。这样允许开发人员根据应用程序选择最合适的方式,不论是在一个API中建立一列函数还是建立一个one-off函数来转向另一个函数。
先快速看下JavaScript中这两种方法是什么样的:
// Named function-命名函数 function add(x, y) { return x+y; } //Anonymous function-匿名函数 var myAdd = function(x, y) { return x+y; };
就像JavaScript中一样,函数可以返回变量。当返回变量时就称‘捕获’这些变量。理解这是如何工作,以及使用该技术时需要注意哪些事项,虽然超出了本片文章范围,但要掌握JavaScript和TypeScript语言,就需要对该机制能够有透彻理解。
var z = 100; function addToZ(x, y) { return x+y+z; }
函数类型
Typing the function
我们对前面的例子添加类型:
function add(x: number, y: number): number { return x+y; } var myAdd = function(x: number, y: number): number { return x+y; };
我们对每个参数添加类型,然后对函数返回值添加类型。TypeScript可以通过查看返回语句算出返回类型,所以在许多情况下也可以选择不添加返回类型。
Writing the function type
现在我们已经对函数添加了类型,现在我们写出函数的完整类型,来看看函数类型的每一部分。
var myAdd: (x:number, y:number)=>number = function(x: number, y: number): number { return x+y; };
一个函数的类型有相同的两部分:参数类型和返回类型。当写下所有函数类型时,这两部分都需要写出。参数类型就像一个参数列表一样,每个参数有一个名称和一个类型。名称只是有助于可读性。也可以写为:
var myAdd: (baseValue:number, increment:number)=>number = function(x: number, y: number): number { return x+y; };
只要函数的参数类型一一对应,就认为是有效类型,而不用考虑参数名称是否相同。
第二部分是返回类型。我们在参数和返回类型之间使用fat arrow (=>)使返回类型更为清晰。就像前面提到的,这是函数类型所需的一部分,所以函数如果没有返回值,应当用‘void‘而不是什么也不填写。
注意,只有参数类型和返回类型组成了函数类型。捕获的变量并不反映在类型中。实际上,捕获变量是函数‘隐藏状态’的一部分,不是API的一部分。
Inferring the types
下面例子中,你会注意到如果在等式的一边有类型而另一边没有类型时,TypeScript编译器可以算出类型:
// myAdd has the full function type // myAdd有完整的函数类型 var myAdd = function(x: number, y: number): number { return x+y; }; // The parameters ‘x‘ and ‘y‘ have the type number // 参数‘x‘ 与 ‘y‘的类型是number var myAdd: (baseValue:number, increment:number)=>number = function(x, y) { return x+y; };
这被称为‘contextual typing‘(上下文类型推断),一种类型推断形式。这有助于减少程序中需要键入的类型。
可选参数与缺省参数
与JavaScript不同,在TypeScript中认为函数的每个参数都是必须的。这并不表示参数取值不是为‘null‘,而是当编译器调用函数时将检查每个参数的值。编译器还假定只有这些参数传递给函数。简言之,输入的函数参数数量必须与函数期待的参数数量相同。
function buildName(firstName: string, lastName: string) { return firstName + " " + lastName; } var result1 = buildName("Bob"); //error, too few parameters var result2 = buildName("Bob", "Adams", "Sr."); //error, too many parameters var 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; } var result1 = buildName("Bob"); //works correctly now var result2 = buildName("Bob", "Adams", "Sr."); //error, too many parameters var result3 = buildName("Bob", "Adams"); //ah, just right
可选参数必须跟在必选参数后面。假定想要使first name而不是last name为可选参数,就需要改变函数参数顺序,将first name参数放在后面。
在TypeScript中,当用户没有对可选参数提供值时可以事先设置一个值,这些参数也被称为缺省参数。以前面例子举例,设置last name的缺省值为"Smith"。
function buildName(firstName: string, lastName = "Smith") { return firstName + " " + lastName; } var result1 = buildName("Bob"); //works correctly now, also var result2 = buildName("Bob", "Adams", "Sr."); //error, too many parameters var result3 = buildName("Bob", "Adams"); //ah, just right
就像可选参数一样,在参数列表中缺省参数也必须放在必选参数后面。
可选参数和缺省参数都拥有相同的类型:
function buildName(firstName: string, lastName?: string) {
与
function buildName(firstName: string, lastName = "Smith") {
有相同的类型 "(firstName: string, lastName?: string)=>string",缺省参数对应的缺省值消失了,只剩下可选参数。
Rest参数
必选参数,可选参数,缺省参数都有一样相同:一次只涉及一个参数。有时候希望将多个参数归为一组,或不清楚函数最终会传入多少个参数。在JavaScript中,可以用函数体内可见的可变参数来表示。
在TypeScript中,可以将这些参数组合成一个变量:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" "); } var employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
Rest参数被看做无穷数量个可选参数。用户可以不输入参数,或根据实际情况输入N个参数。编译器将函数中在省略号...后面的参数名称用于构建一个参数数组,这样可以在函数中使用。
省略号...也用在函数rest参数的类型中:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" "); } var buildNameFun: (fname: string, ...rest: string[])=>string = buildName;
Lambdas及使用‘this‘
在JavaScript函数中‘this‘如何工作是学习JavaScript编程人员常见的问题。事实上,学习如何使用它就像是开发人员对JavaScript越来越得心应手的一种成长仪式。由于TypeScript是JavaScript的一个超集,TypeScript开发人员也需要学习如何使用‘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);
如果运行这个例子,会得到一个错误而不是预期的alert box。这是因为函数中用到的‘this‘是由‘createCardPicker‘创建的,它被设置为‘window‘而不是‘deck‘对象。当调用‘cardPicker()‘时就会发生,这里‘this‘除了Window以外没有动态绑定。(备注:在严格模式下,this将等于undefined而不是window)。
可以在函数返回前将函数绑定到正确的‘this‘变量来修复该问题。这样不用考虑函数在后面如何使用,就能够看到最初的‘deck‘ 对象。
为了修复问题,我们用lambda语法( ()=>{} )而非JavaScript函数表达式来表示函数。这样当函数创建时就自动捕获‘this‘而不是在函数被调用时捕获:
var deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { // Notice: the line below is now a lambda, allowing us to capture ‘this‘ earlier 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的Understanding JavaScript Function Invocation and “this”。
译者注:这篇参考文章的核心思想:
fn(...args)等同于fn.call(window [ES5-strict: undefined], ...args)
(function() {})()等同于(function() {}).call(window [ES5-strict: undefined)
重载(Overloads)
JavaScript本质上就是一种动态语言。经常可以看到一个JavaScript函数可以基于传递参数的形(shape)返回不同类型的对象。
var 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") { var pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the 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的对象(一副牌),函数就pick the card(从中挑选一张牌);如果用户选择一个数字,就告诉用户选择的是什么牌。但类型系统中如何来描述呢?
答案是对同一个函数提供多个函数类型来重载(overloads)。编译器用这个列表来解析函数调用。下面创建一组重载函数,来描述‘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 { // 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") { var pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the 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‘代码片段不是重载列表,这里只有两个重载函数:一个函数接受对象,一个函数接受一个数字。调用‘pickCard‘时传入其他类型参数会导致错误。
参考资料
[1] http://www.typescriptlang.org/Handbook#functions
[2] TypeScript系列1-简介及版本新特性, http://my.oschina.net/1pei/blog/493012
[3] TypeScript系列2-手册-基础类型, http://my.oschina.net/1pei/blog/493181
[4] TypeScript系列3-手册-接口, http://my.oschina.net/1pei/blog/493388
[5] TypeScript系列4-手册-类, http://my.oschina.net/1pei/blog/493539
[6] TypeScript系列5-手册-模块, http://my.oschina.net/1pei/blog/495948