众所周知,函数是构成JS应用的重要基础。他们的作用不仅仅体现在构建抽象层,模拟类、信息的隐藏和模块的实现上。在TS中,当存在类、命名空间(namespace)和模块(module)时,函数也能充分描述他们的构成与作用。TS为了能更好的处理函数这个模块在JS的标准函数中增加了部分新的能力。
怎么在TS中描述一个函数?
TS实际上可以和JS一样,有两种创建函数的方式:命名函数和函数表达式.这意味着你可以随意选择哪种方式创建函数,不管你是准备在一个API中创建一堆函数还是只是想写一个一次性函数专门用来实现特定功能。 回想一下JS中的定义方式: function add(x,y){
return x + y;
}
let myFunc = function(x,y){
return x + y;
}
和JS一样,函数可以读取函数体外已定义的变量。这个行为我们称为变量捕获。当你理解了函数可以读取作用域之外的变量之后,你需要清晰的理解这个function在JS以及TS中究竟是如何运行的。
let z = 100;
function addZ(x,y) {
return add(x,y)+z;
}
函数类型
函数定型
我们在之前的实例上试着增加部分类型定义/声明。function add(a: number, b:number):number {
return x + y;
}
let myAdd = function(a: number, b: number): number {
return x + y;
}
需要注意的是,第二种写法的返回值类型(:number)不能直接写在
myAdd后面,否则就表示开发者实际上想要声明的是一个数字而非一个函数。
我们需要给函数的参数指定类型;同时我们也要指定函数的返回值类型.实际情况中TS可以显式的判断返回值的类型,所以我们一般情况下可以直接省去返回值的类型的声明。
编写函数类型
接着使用上面的函数,我们现在可以试着把函数中的所有的变量以及返回值都指定一个类型。let myAdd:(x:number, y: number) => number = function(
a: number,
b: number,
): number{
return a + b;
}
//实际上函数的定义就是这样: 定义其变量的类型和返回值类型,
let anotherF:(a: number, b: number) => number;
这里可以看出来,虽然定义
myAdd是一个函数类型,但是在定义其实体的时候无需按照其定义的类型的变量名进行操作。另外,在TS中可以单纯的只定义一个函数,允许不给他赋值。
只要函数变量的类型对应定义时的类型,变量就是有效的。
需要注意的点是:参数的类型是要一一对应的,不能更改不同类型的参数的顺序。
类型推断
简而言之,如果你已经严谨的定义了一个函数的类型及返回类型,在赋予其实体函数操作内容的时候,可以不用重复类型定义这一步骤。
let func:(a: number,b:number) => number
func = (a, b) => a+b;
// It's OK!
这也称之为上下文输入,可以帮我们节省繁琐的类型定义的环节。
可选参数&默认值
对于一个普通的定义的函数而言,它的参数的数量需要和实例化的时候的参数相同(调用的时候)。否则会返回一个error.
let add:(a: number, b: number) => number = (a, b) => a+b
add(1, 2); // no problem;
add(1); // nope, the errors come like 'An argument for 'b' was not provided.'
add(1,2,3);// too many~error - 'Expected 2 arguments, but got 3';
在JS中,我们习惯了每个参数实际上都是可选的参数(如果没有显式传值,该值默认为undefined,参与函数的运行逻辑;若超出预期,不会调用)。在TS中,可选参数可以使用?:来定义。
let add(a: number, b?:number) => number
add = function(){} ...
add(1) // ok
需要注意的是,我们一般不会将前置位的变量设置为可选变量(经过测试,实际上不允许可选参数在必选参数之前)。 我们也可以像JS(ES2015)一样给参数设置默认值。
let add:(a:number, b = 1) => number;
add(1); // 2
add(1, undefined); // 2
add(1, null); // 1 之前有提到undefined和null可以定义给任意类型的值,但是在默认值这里两者的表现相反
从语义上而言,有默认值的参数就是可选参数。此外,因为已经赋予默认值,所以类型可以不用断定。(也就是说可以省掉一个参数类型声明)。此外,因为参数的默认值类型决定了参数类型,在赋予默认值的时候需要注意参数类型是否固定唯一,如果不确定可以使用 | 来增加类型。
这里需要注意的是,这种方法允许我们给有默认值的参数多种类型,但是实际使用中,若使用默认值,基本上只会用到第一个定义的默认值。即
b = 1 | '1' | true只会取到数字1这个默认值。
另外,既然有默认值的参数是可选参数,上文已经谈及可选参数和必需参数的顺序关系,所以不建议在必选参数前设置一个有默认值的参数。否则只能像
fun(undefined, 1)这样调用了。(但是在定义的时候不会返回错误)
REST
上一章节我们说完了参数少的情况,现在我们看一下,如果参数传入可能超出预期,TS会如何处理。
不出乎意料的是,TS支持rest parameter的写法,像这样:
function myFunc(a: number, ...restPara: Array<number>){
const arr = [a, ...restPara];
return arr.reduce((a,b) => a+b);
}
// 当然 ...restPara: Array<number> 也可以写成 ...restPara: number[]
在这里,restPara就是一个可选参数的数组集合。
此外,在函数类型定义的时候我们也能使用...rest参数、
let func:(initialValue: number, ...restParameter:Array<number>) => number;
this
this的指向一直是函数在调用的时候会困扰开发者的问题之一。作为JS的超集,TS的开发自然同承一脉。万幸的是,TS提供了一些特殊方法帮助开发者处理this的指向性问题。如果你对this的指向感兴趣,这里有一篇文章详细的描述了this在JS中是如何工作的:understanding-javascript-function-invocation-and-this.(如果大家感兴趣的话,这篇文章我也可以试着讲一下,请在评论区留言)。
this和箭头函数
学习过ES6的开发者们应该对箭头函数颇为熟悉了吧,他的设计灵感最初就是在简写函数的同时,固化this的指向。经典问题:
const obj = {
name: 'inner',
speakName(){
console.log(this.name)
}
};
const name = 'outer';
const changeThis = obj.speakName;
changeThis(); // undefined
关于这个地方的
changeThis()为什么会返回undefined,这里稍微啰嗦几句:在最外层this在node中指向global || globalThis(这两者相同),在browser中指向window。而在node中我们在最外层定义的对象实际上并没有挂载在global上面,所以在这里返回了undefined.
此外,若想将变量挂载在最外层的全局变量上面,开发者可以在声明变量的时候去掉
var,let,const等声明方式。
为了指向函数内部,我们可以使用箭头函数:
const obj = {
name: 'inner',
speakName(){
return () => {
console.log(this.name)
}
}
}
const name = 'outer'
const changeThis = obj.speaker();
changeThis();
--nolmplicitThis标志可以提示开发者this的指向存在部分问题。
This参数
在上文的代码中,this.name类型是any.这是因为this是对象中的函数表达式返回。为了解决这个问题,你可以明确地定义this的类型。实际上,this是函数的第一个参数,同时也是一个假参数。
function func(this: { name: string }){
console.log(this.name)
}
看到这里部分开发者可能觉得有点熟悉: 这不就是接口吗~那我们也可以和接口联动
interface inner{
name: string;
}
interface outer{
name: string;
speakName(this: inner): () => void
}
let func: outer = {
name: '1',
speakName: function(this:outer){
return () => {
console.log(this.name);
}
}
}
部分开发者可能觉得这个地方需要明确
this的类型显得很画蛇添足,其实不然。TS的本质 = JS + type 所以对开发者而言,每个变量都应该明确其类型(即使是this)。
回调函数中的this
很多时候开发者在回调函数中都会在this的指向性问题上吃苦头,尤其是异步函数。如果不好好处理的话,this往往是undefined.稍微做一些了解,可能开发者就可以在回调函数中大展身手了~
interface init{
addAction(onClick: (this: void, e:Event) => void): void
}
this:void意味着接口init接受一个不需要this参数的onClick函数。在此之后,你可以试着调用这个接口:
class MyClass{
name: string;
onAction(this: MyClass, e:event){
this.name = e.message;
}
}
let h = new Handler();
let element:init;
element.addAction(h.onAction); // nope
// 这个地方我们在最后一句中会发现这里实际上将 this:MyClass 赋给了 this:void,因而会返回一个错误。
有了这个注释,开发者就能轻易发现回调函数中的this指向问题了。
这里实际上只需要将
MyClass中的onAction中的参数this的类型改为void即可。或者,我们还是可以不声明this的类型,使用箭头函数来代替。
重载
JavaScript本质上是一种非常动态的语言。单个JavaScript函数根据传入参数的类型返回不同类型的对象并不少见。 但是我们可能有时候也避免不了需要这么做。
function handleWithMultipleType(item: any) {
if (typeof item === 'object') {
return item?.length;
} else if (typeof item === 'number') {
return new Array(Math.abs(item)).fill(item);
}
}
这个函数很好理解,但是我们需要如何将这个函数在TS中描述出来呢? 重载即可。
function handleWithMultipleType(item: {length: number}):number;
function handleWithMultipleType(item: number):Array<number>;
function handleWithMultipleType(item: any):any{
// ....
}
重载可以清晰的告知TS我们在处理什么样的参数类型以及对应的返回类型。 TS在编译的过程中,会试着将函数的参数与实际调用时的参数做类型匹配,优先级是按照匹配程度而定,从最匹配到最不匹配,从而加载函数的使用。
以上。