TypeScript 函数
函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,隐藏信息和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。
和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数。 你可以随意选择适合应用程序的方式,不论是定义一系列API函数还是只使用一次的函数。
// 具有名称的函数 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; }
定义函数
让我们为前面声明的函数添加类型,让它变成一个TypeScript的函数:
// 具有名称的函数 function add(x: number, y: number): number { return x + y; } // 匿名函数 let myAdd = function(x: number, y: number): number { return x + y; }; let myAdd2:(x: number, y: number) => number
我们可以给每个参数添加类型之后,再为函数添加返回值类型。 不过TypeScript能够根据返回值自动推断出返回值类型,除非必要的时候,否则我们通常省略它。
// 具有名称的函数 function add(x: number, y: number) { return x + y; } // 匿名函数 let myAdd = function(x: number, y: number) { return x + y; };
函数类型包含了两部分:参数类型和返回值类型
所以我们也可以给一个变量赋值一个函数类型:
let myAdd2:(x: number, y: number) => number
书写完整函数类型
现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。
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
而不能留空。
函数的类型只是由参数类型和返回值组成的。 函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。
推断类型
尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,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里参数多一个或者少一个都是不能通过编译时检查的,如果我们希望该参数是可选的,可以在参数名旁使用 ?
实现可选参数的功能。 如下所示:
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是可选的,那么就必须调整它们的位置,把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
默认参数与可选参数一样,是可以省略的。不同的是,默认参数省略后会有一个默认值提供给函数使用。可选参数与末尾的默认参数共享参数类型。 默认参数的默认值消失了,只保留了它是一个可选参数的信息。
还有一点不同,待默认值的参数不需要放在必需参数的最后面,它可以在任何位置。 如果带默认值的参数出现在必需参数前面,用户在调用时必须明确的传入 undefined或null
值来对该参数的位置进行占位处理获得默认值。我们重写最后一个例子,让 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(" "); } 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;
回调函数和promise
为了能充分体会到promise的好处,先让我们回到过去,看看在ES5时代如何处理下面这个看似简单的例子,这个例子将会为我们证明一件事,那就是使用回调函数来创建异步代码会使代码的可读性变得非常糟糕。
这个例子的原理是: 先同步读取文件然后将内容转换成JSON字符串
const fs = require('fs'); function loadJSONSync(filePath: string){ return JSON.parse(fs.readFileSync(filePath)); } console.log(loadJSONSync('test.json'));
为了便于理解,我们忽略了一些可能发生的异常行为,比如test.json里面是一个无效的JSON文件或者test.json本身就不存在。
现在让我们将上面的代码改造为异步的版本:
const fs = require('fs'); function loadJSON( filePath: string, callback: (error:Error,data?: any) => void ){ fs.readFile(filePath,function(error,data){ if(error){ callback(error); }else{ callback(null,JSON.parse(data)); } }); }
虽然我们处理了文件读写的异常,但在并没有处理JSON.parse的异常,这可能会导致抛出异常。
在使用基于回调方式的异步函数时需要记住如下准则:
-
- 一定不要调用两次回调
- 一定不要抛出错误
然而,上面示例中的函数违背了第二条准则。
一个最简单天真的解决方案是使用try catch将JSON.parse包裹起来,就像下面这样:
const fs = require('fs'); function loadJSON( filePath: string, callback: (error:Error,data?: any) => void ){ fs.readFile(filePath,function(error,data){ if(error){ callback(error); }else{ try{ callback(null,JSON.parse(data)); }catch(error){ callback(error); } } }); }
然而在这份代码中有一个非常奇妙的bug,很难用肉眼发现,如果回调函数有错误,经过try-catch捕获后,将会再执行一次,所以同步代码需要放在try-catch中,回调函数则要放在另外的位置。
遵循上面的准则,我们可以实现一个具有高完成度版本的异步执行函数loadJSON,如下所示:
const fs = require('fs'); function loadJSON( filePath: string, callback: (error:Error,data?: any) => void ){ fs.readFile(filePath,function(error,data){ if(error){ callback(error); }else{ let result; try{ result = JSON.parse(data); }catch(error){ callback(error); } callback(null,result); } }); }
是不是已经开始感觉有点复杂了,虽然我们只是写了一个读取文件再转换成JSON的小段代码,但回调方式使得问题变得复杂了许多。
接下来让我们看看如何使用promise来更好地处理这个问题。
1.创建promise
promise是拥有过程状态的,这很像我们前面学习的迭代器。但不同的是它的状态更为简单,只有pending、resolved、rejected三种情况。
promise通过Promise构造器创建,resolve和reject是两个参数,分别处理成功或失败的情况。让我们先来构造一个promise:
const promise = new Promise((resolve,reject) => { });
2.订阅promise
promise可以使用then或者catch来订阅:
const promise = new Promise((resolve,reject) => { resolve(2333); }); promise.then(res => { console.log(res); //2333 }); promise.catch(err => { //没有reject不会被调用 }); const promise1 = new Promise((resolve,reject) => { reject(new Error('抛出异常')); }); promise1.then(res => { //没有resolve不会被调用 }); promise1.catch(err => { console.log(err); //抛出异常 });
3.promise的链式性
promise的链式性是promise的核心优点。一旦你得到了一个Promise对象,从那一个promise起,使用Promise.then会不断地得到新的promise,如下所示:
Promise.resolve(2333) .then(res => { console.log(res) //2333 return 2333333333; }) .then(res => { console.log(res); //2333333333 return Promise.resolve(233333333333); }) .then(res => { console.log(res); //233333333333 return Promise.resolve(2333333333333333333333333333); });
你可以将之前任何promise点上的异常都放在最后的Promise.catch中去处理,像下面这样:
Promise.reject(new Error('发生异常')) .then(res => { console.log(res); //不会被调用 return 2333; }) .then(res => { console.log(res); //不会被调用 return Promise.resolve(233333333); }) .then(res => { console.log(res); return Promise.resolve(23333333333333333); }) .catch(err => { console.log(err.message); //发生异常 });
Promise.catch实际上仍然会返回一个新的Promise对象。
4.TypeScript和promise
TypeScript的强大之处在于它可以通过promise链推测出传递值的类型。
Promise.resolve(2333) .then(res => { // (parameter) res: number return true; }) .then(res => { // (parameter) res: boolean })
这样的类型推导难不倒TypeScript。
现在开始把一开始的回调风格函数重构为一个promise。过程非常简单,只需将函数调用放到promise中,把错误移动到Promise.reject中,把没有错误的回调放到Promise.resolve里就行了。
const fs = require('fs'); function readFileAsync(filePath: string):Promise<any>{ return new Promise((resolve,reject) = > { fs.readFile(filePath,(error,result) => { if(error){ reject(error); }else{ resolve(result); } }); }); }
重构完读取文件的函数之后,我们可以再来重构下loadJSONAsync。当我们调用readFileAsync时,它已经是一个promise了,我们直接将它作为返回进行操作就可以了,就像下面的例子一样:
function loadJSONAsync(filePath: string):Promise<any>{ return readFileAsync(filePath).then(result => { return JSON.parse(result); }); }
这个时候我们再来使用它就变得非常优雅了,就像是在使用同步调用一样:
loadJSONAsync('test.json') .then(result => { console.log(result); }) .catch(error => { console.log(error); });
5.并行控制流
promise为异步操作带来了便利性,这只是其优势的冰山一角。
如果你想执行一系列的异步任务并在所有任务完成后执行操作,该怎么办呢?
这是一个非常常见的场景,例如,现在有三个API,分别是获取用户信息、获取购物信息及获取商品信息,三者信息同时拉取完成后才能进行业务的渲染。
在这里,我们可以使用promise提供的Promise.all函数来完成上面的需求
通过promise提供的静态Promise.all函数,我们可以运行一系列的异步任务,然后得到所有结果。可以使用它来等待n个promise完成,你提供给Promise.all一个包含了n个promise的数组,而Promise.all返回给你一个包括了n个resolved值的数组。
function fetchUserInfo(userId: string):Promise<{}>{ return new Promise(resolve => { setTimeout(() => { resolve({id:userId,'name':'fanqi'}); }, 1000); }); } function fetchCartInfo(userId: string):Promise<{}>{ return new Promise(resolve => { setTimeout(() => { resolve({id:1,userId:userId,cardNum:'112233'}); }, 1200); }); } function fetctGoodInfo(goodId: string):Promise<{}>{ return new Promise(resolve => { setTimeout(() => { resolve({id:goodId,goodName:'好吃的'}); }, 1500); }); } Promise.all([fetchUserInfo("1"),fetchCartInfo("2"),fetctGoodInfo("3")]) .then(res => { console.log(res); }); //(3)[{...},{...},{...}] //0:{id:userId,'name':'fanqi'} //1:{id:1,userId:userId,cardNum:'112233'} //2:{id:goodId,goodName:'好吃的'}
async和await
除了promise,ES8(你没有听错,的确是ES8)还提出新的关键字:async和await。它们用一种声明的方式告诉JavaScript运行时在await关键字处暂停执行代码,等待结果返回,并在结果返回处继续执行代码。
下面是一段服务器上运行的代码,用于查找用户数据。
async function fetchUserInfo(id: string){ try{ return await findUser(id); }catch(err){ console.log(err); } }
async/await关键字让代码的执行方式更贴近同步调用,因为它会暂停函数的执行能力,等待结果的返回。这一点和迭代器非常相似,而本质上,async/await也是基于迭代器实现的。
回顾一下迭代器的能力:
-
- 暂停函数执行
- 把值放入到函数中
- 把错误抛到函数中
而async/await又不同于迭代器,因为我们并不用去手动操作next,操作throw等等。
this
学习如何在JavaScript里正确使用this
就好比一场成年礼。 由于TypeScript是JavaScript的超集,TypeScript程序员也需要弄清 this
工作机制并且当有bug的时候能够找出错误所在。 幸运的是,TypeScript能通知你错误地使用了 this
的地方。 如果你想了解JavaScript里的 this
是如何工作的,那么首先阅读Yehuda Katz写的Understanding JavaScript Function Invocation and "this"。 Yehuda的文章详细的阐述了 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
视为window
。 (注意:在严格模式下, this
为undefined
而不是window
)。
为了解决这个问题,我们可以在函数被返回时就绑好正确的this
。 这样的话,无论之后怎么使用它,都会引用绑定的‘deck’对象。 我们需要改变函数表达式来使用ECMAScript 6箭头语法。 箭头函数能保存函数创建时的 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);
更好事情是,TypeScript会警告你犯了一个错误,如果你给编译器设置了--noImplicitThis
标记。 它会指出 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
,因此--noImplicitThis
不会报错了。
this
参数在回调函数里
你可以也看到过在回调函数里的this
报错,当你将一个函数传递到某个库函数里稍后会被调用时。 因为当回调被调用的时候,它们会被当成一个普通函数调用, this
将为undefined
。 稍做改动,你就可以通过 this
参数来避免错误。 首先,库函数的作者要指定 this
的类型:
interface UIElement { addClickListener(onclick: (this: void, e: Event) => void): void; }
this: void
means that addClickListener
expects onclick
to be a function that does not require a this
type. Second, annotate your calling code with 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
类型后,你显式声明onClickBad
必须在Handler
的实例上调用。 然后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
方法根据传入的不同参数类型处理不同的逻辑,返回两种不同的类型。 如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。 如果用户想抓牌,我们告诉他抓到了什么牌。 但是这怎么在类型系统里表示呢。
方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。 下面我们来重载 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
会产生错误。
TypeScript中的重载并不支持我们实现多个不同参数的pickCard函数,它的语法有一点点怪异,是声明多个不同的pickCard函数,最后在一个类型最宽泛的版本中去实现了它,这都是为了能在编译时和代码提示时获得最佳的体验。简洁度并不如联合声明,所以TypeScript中的重载使用率很低,大家更倾向于使用其他方式达到类似的效果,而不是直接使用它。