TS 中的函数

类型定义

同 js 一样, ts 中函数分为两种形式,只不过我们可以给参数和函数的返回值定义上类型:

  • 函数声明(命名函数)
typescript 复制代码
function fn(day: string): string {
  return '今天是' + day
}

console.log(fn('周天')) // 今天是周天

如此,传入函数的参数的类型和个数就被限制了。

  • 函数表达式(匿名函数)
typescript 复制代码
const fn = function(day: string): string {
  return '今天是' + day
}

console.log(fn('周天')) // 今天是周天

注意:

  • 函数的返回值,其实也可以不写,因为 ts 会根据 return 的值进行类型推导,当然明确写出来可以增加代码的可读性;
  • 对于某些回调函数的定义,参数如果可以根据上下文推导出类型(contextual typing),则可以不明确写出。比如下面的 item, 根据 list 可以知道必为 string, 就可以不写类型:
typescript 复制代码
const list = ['Jay', 'Eson']
list.forEach(item => console.log(item))

函数的完整写法

typescript 复制代码
const fn: (day: string) => string = function(day: string): string {
  return '今天是' + day
}

console.log(fn('周五')) // 今天是周五

(day: string) => string 被称为函数类型表达式(function type expression),是函数 fn 的类型,或许你会觉得这里的 day 反正是个形参,可以省略不写,但 ts 是要求必须写的。如果直接写 (string) => string ,ts 会认为这个函数是接收一个类型为 any 的参数,参数的名字叫 string

调用签名(Call Signature)

通过接口的方式作为函数的类型来使用,可以描述带有属性的函数。

typescript 复制代码
// 定义函数接口
interface IAddFn {
  name: string,
  (a: number, b: number): number
}
// 定义函数
const myAddFn: IAddFn = function (a, b) {
  return a + b
}

console.log(myAddFn(1, 2)) // 3
console.log(myAddFn.name) // myAddFn
  • (a: number, b: number): number 就是调用签名, 定义了参数和返回值的类型。注意调用签名是使用 : 而不是 =>
typescript 复制代码
interface IPerson {
  run: () => void
}

run: () => void表示的是 IPerson 类型有个属性 run 为函数,而调用签名则表示该类型的变量是可以调用的。

  • 使用函数表达式 来定义类型是上述接口的函数,函数的参数和返回值的类型可以省略不写,ts 的类型系统会根据 IAddFn 接口推断出参数类型。

一般对于函数的类型,还是使用 type 定义阅读性更好:

typescript 复制代码
type AddFnType = (a: number, b: number) => number

构造签名(Construct Signature)

如果想表示一个函数是构造函数(类),必须要通过 new 调用,则可以在函数的调用签名前加上 new 关键字:

typescript 复制代码
interface IAdd {
  new(a: number, b: number): number
}

const fn1 = function (foo: IAdd) {
  new foo(1, 2) // 如果直接调用 foo(1, 2) 则会报错
}

// 也可以直接在函数类型表达式前加上 new ,表示 foo 需要为构造函数
const fn2 = function (foo: new () => void) {
  new foo()
}

默认参数和可选参数

默认参数与 ES6 中的 js 里写法一样,在参数后用 = 号定义默认值,可选参数则是在参数后加个 ?

默认参数

当参数有默认值时,可以不显示地定义参数的类型,而依靠类型推导:

typescript 复制代码
// 默认参数
function fn(day = '周五'): string {
  return '今天是' + day
}

const result = fn()
console.log(result) // 今天是周五

有多个参数时,默认参数不像可选参数那样一定要放在最后,在下例中就是放在了第一位,但是在调用参数时,必须传递第二个参数,只不过第一个传 undefined 代表使用默认值:

typescript 复制代码
const add = (num1 = 1, num2: number) => {
  console.log(num1 + num2)
}
add(undefined, 2) // 3

所以一般有多个参数时,其顺序最好是:普通参数 -> 有默认值的参数 -> 可选参数

可选参数

typescript 复制代码
// 可选参数
function fn(day?: string): string {
  return '今天是' + day
}

const result = fn()
console.log(result) // 今天是undefined

可选参数的类型其实是和 undefined 的联合类型。在 js 中,所有参数都是可选参数,即不赋值时, 它的值就是 undefined

注意,如果有多个参数,那么可选参数需要放最后一个:

typescript 复制代码
const add = (num1: number, num2?: number) => {
  return num2 ? num1 + num2 : num1
}

如果使用了可选参数,有时会遇到问题,比如下面这样写就会报错,因为 day 可能为 undefined

解决的办法不止一种:

非空类型断言

如果你十分确定在调用 fn() 时都传了参数,也就是 day 一定有值,那么比较快捷的办法是使用非空类型断言 !,让 ts 在编译阶段跳过对它的检测:

typescript 复制代码
function fn(day?: string) {
  console.log(day!.length)
}

可选链

如果不确定 day 到底存不存在,也可以使用 js 就有的语法 ------ 可选链 ?.

typescript 复制代码
function fn(day?: string) {
  console.log(day?.length)
}

如果是赋值操作,就不能用可选链了,但是依旧可以使用非空断言,或者是类型缩小:

typescript 复制代码
function fn(day?: string) {
  day!.length = 3
  if (day) {
    day.length = 3
  }
}

剩余参数

与 ES6 中的剩余参数基本一致,只是可以添加个类型

typescript 复制代码
function eat(food1: string, ...foods: string[]) {
  console.log(food1) // 麦辣鸡腿堡
  console.log(foods) //  ["麦辣鸡翅", "薯条", "可乐"]
}
eat('麦辣鸡腿堡', '麦辣鸡翅', '薯条', '可乐') 

函数作为函数的参数

当参数个数少于定义的时候不校验。

当一个函数 fn 作为参数传给另一个函数 foo 时,如果 fn 的参数个数少于 foo 定义时声明的 fn 的参数的个数,ts 是不会报错的。比如下例中 foo 的第 3 个参数 fn 应该有两个参数 ab,但是实际上在第 4 行传给 foo 的函数一个参数也没写,也不会报错:

typescript 复制代码
function foo(m: number, n: number, fn: (a: number, b: number) => void) {
  fn(m, n)
}
foo(1, 2, () => {
  console.log('hello')
})

请注意:

  • 在第 2 行执行 fn() 时,也就是 fn 被调用时,还是必须得有 2 个参数的;
  • 如果在第 4 行传给 foo 的函数的参数的个数多于 2 个,则依旧会报错;或者 mn 传的不是数字 1 和 2 而是字符串或别的类型,也会报错;
  • 虽然在第 4 行作为参数传给 foo 时可能没有 ab 参数,但因为我们在第 2 行调用 fn 时,其一定是有 2 个参数的,所以无需为了 fn 作为 foo 的参数传入时可能没有参数而在第 1 行定义类型时将 ab 变为可选参数。官方文档说明
  • ts 有这种规则的好处在于,像传给 forEach 的这个函数,照理也是有 3 个参数的 ------ value: number, index: number, array: number[] ------ 但我们可以像下面这样只传一个:
typescript 复制代码
const arr = [1, 2, 3]
arr.forEach(item => console.log(item))

函数重载

函数名相同, 而形参不同(可以是个数不同,也可以是类型不同)的多个函数。也就是可以声明同一个函数的几种不同的情况,比如参数不同,返回值不同,方便错误检查。

比如我们需要个 add 函数,只希望存在两种情况:

  • 两个参数都是数字,返回相加的结果
  • 两个参数都是字符串,返回拼接的结果
typescript 复制代码
function add(x: number | string, y: number | string): number | string {
  if (typeof x === 'number' && typeof y === 'number') {
    return x + y // number 类型的
  } else if(typeof x === 'string' && typeof y === 'string'){ 
    return x + y // string 类型的
  } else {
    return '错误'
  }
}

console.log(add(1, 2)) // 3
console.log(add('1', '2')) // 12
console.log(add(1, '2')) // 错误

上述使用联合类型的写法,当传入的参数一个为数字一个为字符串时,vs code 并不会有标红的波浪线提示错误。而用函数重载可以解决这个问题,也就是在函数声明前进行重载签名(overload signature):

typescript 复制代码
function add(a: number, b: number): number
function add(a: string, b: string): string
function add(a, b) {
  return a + b
}
add(1, 2)
add('hellow', 'world')

现在,当传入 add 的参数均为 number 时,add 的类型如下:

当传入的都是 string 时:

注意:

  • 第 1~ 2 行的重载签名是没有执行体的,具体的执行是写在第 3 ~ 5 行的实现函数,在实现函数中我们对 ab 的类型就可以写得宽泛些,比如不写就是默认 any,当然也可以是联合类型 "string | number"。
  • 实现函数是不能被直接调用的,当我们调用 add 时,ts 会从上到下的去匹配重载签名,如果匹配不到就会报错,所以即使第 3 行这看起来 ab 可以是任意类型,但如果传给 add 的是一个数字和一个字符串,就会有标红提示了:

条件类型

还可以使用条件类型(Conditional types)将上例中的 2 行重载前面改为只需 1 行:

typescript 复制代码
function add<T extends number | string>(a: T, b: T): T extends number ? number : string
function add(a: any, b: any) {
  return a + b
}

条件类型的写法形同三元运算符,通过判断 T 是否 extends number 来决定函数的返回值到底是 number 还是 string

P.S. 如果不写成函数重载的形式而是直接在函数上应用条件类型来判断,会有如下报错:

this

像下面这样写:

typescript 复制代码
const singer = {
  name: 'Jay',
  sing() {
    console.log(`是谁在唱歌? 是${this.name}`)
  }
}
singer.sing()

在第 7 行调用 sing 方法时,在有使用 tsc --init 生成默认配置的 tsconfig.json 的情况下,ts 是会根据上下文推导出第 4 行的 this 指向的是 singer 的:

但是如果改成下面这样,单独声明一个函数 sing,然后赋值给 singersing 属性:

typescript 复制代码
function sing() {
  console.log(`是谁在唱歌? 是${this.name}`)
}
const singer = {
  name: 'Jay',
  sing
}

singer.sing()

此时严格模式下(tsconfig.json 配置了 "strict": true),ts 无法判断 this 的指向,编译就会报错:

解决的办法有 2 个:

  • 直接在 tsconfig.json 中配置:"noImplicitThis": false,即允许 this 隐式具有 any 类型;
  • sing 内传入 this 作为第 1 个 参数(如果有其它参数就放在 this 后面就好),此处 this 的类型为一个有name 属性的对象:
typescript 复制代码
function sing(this: { name: string }) {
  console.log(`是谁在唱歌? 是${this.name}`)
}

现在执行 singer.sing() 就不会报错了,但是现在如果直接执行 sing() 会报错:

可以通过 callthis 指定为一个含有 name 属性的对象来调用:sing.call({ name: 'Jay' })

相关推荐
一生为追梦43 分钟前
Linux 内存管理机制概述
前端·chrome
喝旺仔la1 小时前
使用vue创建项目
前端·javascript·vue.js
心.c1 小时前
植物大战僵尸【源代码分享+核心思路讲解】
前端·javascript·css·数据结构·游戏·html
喝旺仔la1 小时前
Element Plus中button按钮相关大全
前端·javascript·vue.js
柒@宝儿姐2 小时前
Git的下载与安装
前端·javascript·vue.js·git·elementui·visual studio
Hiweir ·2 小时前
机器翻译之数据处理
前端·人工智能·python·rnn·自然语言处理·nlp·机器翻译
曈欣2 小时前
vue 中属性值上变量和字符串怎么拼接
前端·javascript·vue.js
QGC二次开发3 小时前
Vue3:v-model实现组件通信
前端·javascript·vue.js·前端框架·vue·html
努力的小雨4 小时前
从设计到代码:探索高效的前端开发工具与实践
前端
小鼠米奇4 小时前
详解Ajax与axios的区别
前端·javascript·ajax