在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
函数是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; }; 只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。 第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用( 函数的类型只是由参数类型和返回值组成的。 函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成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里的每个函数参数都必须有值。当然,如果允许的话,我们也可以传递 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里,我们也可以为参数提供一个默认值。它们叫做有默认初始化值的参数。当用户没有传递这个参数或传递的值是 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 = "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里,你可以使用 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等等。
|
请发表评论