当前位置: 首页 > 科技观察

TypeScript函数声明和重载详解

时间:2023-03-19 21:15:51 科技观察

在JavaScript中,函数是构建应用程序的基石。我们可以使用函数来提取可重用的逻辑、抽象模型和封装过程。函数仍然是TypeScript中最基本和最重要的概念之一。让我们看一下TypeScript中的函数类型是如何定义和使用的。一、函数类型定义1、直接定义函数类型定义包括参数和返回值的类型定义:functionadd(arg1:number,arg2:number):number{returnx+y;}constadd=(arg1:number,arg2:数字):数字=>{returnx+y;};这里,add函数定义了两种形式:函数字面量和箭头函数。函数参数arg1和arg2都是数值类型,它们相加得到的结果也是数值类型。如果此处省略了参数的类型,TypeScript将假定该参数是任何类型;如果省略了返回值的类型,如果函数没有返回值,那么TypeScript会将函数的返回值默认为void类型;如果函数有返回值,那么TypeScript会根据定义的逻辑推断返回类型。需要注意的是,在TypeScript中,如果一个函数没有返回值,而我们显式定义这个函数的返回值类型为undefined,会报错:Afunctionwhosedeclaredtypeisneither'void'nor'any'must返回一个值。正确的方法是将函数的返回值类型声明为void:functionfn(x:number):void{console.log(x)}函数的定义包括函数名、参数、逻辑和返回值。在为函数定义类型时,完整的定义应该包括参数类型和返回类型。以上都是定义函数的指定参数类型和返回值类型。我们来定义一个完整的函数类型,通过这个函数类型来指定函数定义时参数和返回值需要符合的类型。letadd:(x:number,y:number)=>number;add=(arg1:number,arg2:number):number=>arg1+arg2;add=(arg1:string,arg2:string):string=>arg1+arg2;//error这里定义了一个变量add,并为其赋值了一个函数类型,即(x:number,y:number)=>number。该函数类型包括参数类型和返回值类型。然后分配一个实际的功能来添加。该函数的参数类型和返回类型与函数类型中定义的一致,因此可以赋值。后来给它赋值了一个新的函数,这个函数的参数类型和返回值类型都是字符串类型。这时会报如下错误:thetype"(arg1:string,arg2:string)=>string"Assignedtotype"(x:number,y:number)=>number"。参数类型“arg1”和“x”不兼容。类型“数字”不可分配给类型“字符串”。注意:如果在函数中使用了在函数体之外定义的变量,则该变量的类型不会反映在函数类型定义中。2.接口定义接口可以用来明确定义函数类型。让我们使用接口为add函数定义函数类型:interfaceAdd{(x:number,y:number):number;}letadd:Add=(arg1:string,arg2:string):string=>arg1+arg2;//错误类型“(arg1:string,arg2:string)=>string”不能分配给类型“Add”。函数类型以接口的形式定义。这个接口Add定义了这个结构体是一个函数,两个参数类型都是数字类型,返回值也是number类型。当指定变量add类型为Add时,必须是函数赋值add,参数类型和返回值类型必须满足接口Add。显然这个函数不满足条件,所以报错。3、类型别名定义类型别名可以用来定义函数类型,更直观和可读:typeAdd=(x:number,y:number)=>number;letadd:Add=(arg1:string,arg2:string):string=>arg1+arg2;//errorcannotassigntype"(arg1:string,arg2:string)=>string"totype"Add"使用type关键字给任何定义的类型一个别名。上面定义了Add的别名后,Add就变成了符合(x:number,y:number)=>number的类型定义。上面定义了Add类型,指定add类型为Add,但是赋给add的值不符合Add类型的要求,所以报错。注意这里的=>和ES6中箭头函数的=>不同。TypeScript函数类型中的=>用来表示函数的定义,左边是函数的参数类型,右边是函数的返回值类型;而ES6中的=>是函数的实现。二、函数参数的定义1、可选参数TypeScript在编写代码时调用函数时会检查参数中的一些错误:typeAdd=(x:number,y:number)=>number;letadd:Add=(arg1,arg2)=>arg1+arg2;add(1,2);//成功add(1,2,3);//错误应该有2个参数,但是得到了3个add(1);//错误应该有2个参数,但是get1在JavaScript中,上面代码中的最后两个函数调用不会报错,但是add(1,2,3)可以返回正确的结果3,add(1)会返回NaN。在TypeScript中,我们设置了指定的参数,所以在使用这个类型时,传入的参数必须与定义的参数类型和数量保持一致。但是有时候,函数的一些参数不是必须的,我们可以将函数的参数设置为可选参数。可选参数只需要后跟一个?在参数名称之后:typeAdd=(x:number,y:number,z?:number)=>number;letadd:Add=(arg1,arg2,arg3)=>arg1+arg2+arg3;add(1,2);//success3add(1,2,3);//success6上面代码中z为可选参数,则其类型为number|undefined,也就是说参数z是可以defaulted的,是不是说defaultable类型就等同于undefined?看下面的例子:functionlog(x?:number){console.log(x);}functionlog1(x:number|undefined){console.log(x);}log();log(undefined);log1();//Expected1arguments,butgot0log1(undefined);可以看出第三次函数调用报错,其中?:表示调用函数时,不能显式传入参数。但是,如果参数类型声明为number|undefined,表示函数参数不能默认,类型必须是number或undefined。需要注意的是,可选参数必须放在必选参数之后,这与JS中定义函数是一致的。看个例子:typeAdd=(x?:number,y:number)=>number;//error必选参数不能放在可选参数后面。在TypeScript中,可选参数必须放在最后,可选参数x放在必选参数y的前面,所以会报错。JavaScript中没有可选参数的概念,但是在写逻辑的时候,可能会判断一个参数是否是undefined,如果是,说明调用函数时没有传递这个参数,需要做兼容性处理;and如果在几个参数中,第一个参数是可选的,而后一个参数需要传递,则需要在可选参数的位置传入一个未定义的占位符。2、默认参数在ES6标准出来之前,默认参数的实现很繁琐:varcount=0;functioncounter(step){step=step||1;count+=step;}上面定义了一个计数器自增函数,它有一个参数step,即每次增加的步长,如果没有传入参数,则step接收undefined,undefined转boolean值为false,所以step||1这里取1,从而达到默认不传参的效果步骤===1.ES6中定义函数时,给参数设置默认值,直接用等号连接默认值在参数之后:constcount=0;constcounter=(step=1)=>{count+=step;};当为参数指定使用默认参数时,TypeScript会识别默认参数的类型;调用函数时,如果给这个参数传了一个默认值的其他类型的参数,会报错:constadd=(x:number,y=2)=>{returnx+y;};add(1,"ts");//错误类型"string"的参数不能赋值给"number"类型的参数当然也可以显式设置默认参数y的类型:constadd=(x:number,y:number=2)=>{returnx+y;};注意:函数的默认参数类型必须是参数类型的子类型,如下:constadd=(x:number,y:number|string=2)=>{returnx+y;};这里,add函数参数y的类型是可选的关节类型号|string,但因为默认参数numbertype是jointtypenumber|的子类型字符串,TypeScript也会通过检查。3.剩余参数在JavaScript中,如果你定义了一个可以输入任意多个参数的函数,那么在定义参数列表的时候就不能一个一个定义了。在ES6发布之前,需要arguments来获取参数列表。arguments是一个类似数组的对象,它包含了调用函数时传递给函数的所有实际参数,它还包含一个length属性,表示参数的个数。让我们模拟实现函数的重载:functionhandleData(){if(arguments.length===1)returnarguments[0]*2;elseif(arguments.length===2)returnarguments[0]*arguments[1];elsereturnArray.prototype.slice.apply(arguments).join("_");}handleData(2);//4handleData(2,3);//6handleData(1,2,3,4,5);//如果代码'1_2_3_4_5'在TypeScript环境下执行,调用3次handleData会报错,因为handleData函数在定义的时候是没有参数的。在ES6中,加入了...扩展运算符,可以反汇编一个函数或对象。它还支持在函数的参数列表中使用来处理任意数量的参数:constandleData=(arg1,...args)=>{console.log(args);};handleData(1,2,3,4,5);//[2,3,4,5]在TypeScript中,可以为其余参数指定类型:constandleData=(arg1:number,...args:number[])=>{};handleData(1,"a");//error不能将"string"类型的参数赋值给"number"类型的参数。3.函数重载在大多数函数中,它只能接受一组固定的参数。但是有些函数可以接受可变数量的参数、不同类型的参数,甚至根据调用函数的方式返回不同类型的参数。为了使用这些函数,TypeScript提供了函数重载。让我们看看函数重载是如何工作的。1、函数签名先来看一个简单的例子:functiongreet(person:string):string{return`Hello,${person}!`;}greet('World');//'Hello,World!'这里的greet方法接收一个参数名称,类型为字符串。那么如果你想让greet方法接收一组名字呢?然后greet函数将接收一个字符串或一个字符串数组作为参数,并返回一个字符串或一个字符串数组。那么如何修改这个函数呢?主要有两种方式:通过判断参数类型修改函数签名functiongreet(person:string|string[]):string|string[]{if(typeofperson==='string'){return`Hello,${person}!`;}elseif(Array.isArray(person)){returnperson.map(name=>`Hello,${name}!`);}thrownewError('error');}greet('World');//'Hello,World!'greet(['TS','JS']);//['Hello,TS!','Hello,JS!']这是最简单直接的方式,但在某些情况下,我们要单独定义调用函数的方式,那么可以使用函数重载。2.函数重载当修改函数签名复杂或者涉及多种数据类型时,建议使用函数重载来完成。在函数重载中,我们需要定义重载签名和实现签名。重载函数签名只定义函数的参数和返回类型,不定义函数的字面量。对于一个函数的不同调用方法,可以有多个重载签名。让我们实现greet()函数的重载://重载签名函数greet(person:string):string;functiongreet(persons:string[]):string[];//实现签名函数greet(person:unknown):unknown{if(typeofperson==='string'){return`Hello,${person}!`;}elseif(Array.isArray(person)){returnperson.map(name=>`Hello,${name}!`);}thrownewError('error');}这里的greet()函数有两个重载签名和一个实现签名。每个重载签名都描述了一种调用函数的方法。我们可以使用字符串参数或字符串参数数组调用greet()函数。您现在可以使用字符串或字符串数??组调用greet():greet('World');//'Hello,World!'greet(['TS','JS']);//['Hello,TS!','Hello,JS!']定义函数重载时需要注意以下两点:(1)可以调用函数签名。上面虽然我们定义了重载的签名和签名方法,但是签名的实现是不能直接调用的,只能调用重载的签名:constsomeValue:unknown='Unknown';greet(someValue);如果这样调用,会报错:Nooverloadmatchesthiscall.Overload1of2,'(person:string):string',gavethefollowingerror.Argumentoftype'unknown'isnotassignabletoparameteroftype'string'.Overload2of2,'(persons:string[]):string[]',gavethefollowingerror.Argumentoftype'unknown'isnotassignabletoparameteroftype'string[]'即,即使签名实现接受未知类型的参数,但我们不能直接将未知类型的参数传递给greet()方法。参数只能是函数重载签名中定义的参数类型。(2)实现签名必须是泛型的在实现签名时,定义的数据类型需要是泛型的,以包含重载的签名。如果我们将greet()方法的返回值类型定义为字符串,那么就会出现问题:functiongreet(person:string):string;functiongreet(persons:string[]):string[];functiongreet(person:unknown):string{//...thrownewError('error');}这时候string[]类型会和string不兼容。因此,实现签名的返回类型和参数类型必须包含所有重载签名中的参数类型和返回值类型,保证通用。3、方法重载除了常规的函数,类中的方法也可以被重载。例如,使用重载方法greet()来实现一个类:classGreeter{message:string;constructor(message:string){this.message=message;}greet(person:string):string;greet(persons:string[]):string[];greet(person:unknown):unknown{if(typeofperson==='string'){return`${this.message},${person}!`;}elseif(Array.isArray(person)){returnperson.map(name=>`${this.message},${name}!`);}thrownewError('error');}}Greeter类包含greet()重载方法:有两个重载签名描述了如何调用该方法及其实现签名。这样我们就可以通过两种方式调用hi.greet():consthi=newGreeter('Hi');hi.greet('World');//'你好,世界!'hi.greet(['TS','JS']);//['你好,TS!','你好,JS!']