TypeScript中的函数(中)

894 阅读5分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

注解this的类型

JavaScript中的每个函数都有this变量,而不局限于类的方法;以不同的调用函数,this的值也不同,这极易导致代码脆弱、难以理解

let x = {
  a(){
    return this
  }
}
​
x.a() // this的值为x对象

this的值取决于调用函数的方式,而不受声明方式的干扰;如果在函数使用this请在函数的第一个参数中声明this的类型,这样每次调用函数时,TypeScript将确保this的确是你预期的类型;this不常规的参数,而是保留字,是函数签名的一部分。

生成器函数

生成器函数是生成一系列值的便利方式;生成器的使用可以精确生成生成值;生成器是惰性的,只在使用方要求时才计算下一个值。

function* createFibonacciGenerator() {
    let a = 0;
    let b = 1;
    while (true) {
        yield a;
        [a, b] = [b, a+b]
    }
}
​
let fibonacciGenerator = createFibonacciGenerator()
console.log(fibonacciGenerator.next()) // { value: 0, done: false }
console.log(fibonacciGenerator.next()) // { value: 1, done: false }
console.log(fibonacciGenerator.next()) // { value: 1, done: false }
console.log(fibonacciGenerator.next()) // { value: 2, done: false }
console.log(fibonacciGenerator.next()) // { value: 3, done: false }
console.log(fibonacciGenerator.next()) // { value: 5, done: false }
console.log(fibonacciGenerator.next()) // { value: 8, done: false }
  • 函数名称前面的星号(*)表明这是一个生成器函数。调用生成器返回一个可迭代的迭代器

  • 这个生成器可一直生成值

  • 生成器使用yield关键字产出值。使用方让生成器提供下一个值,yield把结果发给使用方,然后停止执行,直到使用方要求提供下一个值为止;这里的while(true)循环不会一直运行下去,程序不会崩溃

  • 为了计算下一个斐波那契数列,然后把b赋值给a、把a + b赋值给b

    调用createFibonacciGenerator得到的是一个IterableIterator。每次调用next(),迭代器计算下一个斐波那契数,然后通过yield产出

迭代器

迭代器是生成器的向对面:生成器是生成一系列值的方式,而迭代器是使用这些值的方式

可迭代对象:有Symbol.iterrator属性的对象,而且该属性的值为一个函数,返回一个迭代器

迭代器:定义有next方法的对象,该方法返回一个具有valuedone属性的对象

自定义迭代器或者可迭代对象

let numbers = {
    *[Symbol.iterator]() {
        for (let i = 0; i < 10; i++) {
            yield i
        }
    }
}

numbers是一个迭代器,调用生成器函数numbers[Symbol.iterator]()返回一个可迭代的迭代器;除了我们自己定义迭代器外,还可以使用JavaScript内置的常用集合类型(Array、Map、Set、String)

调用签名

多数时候Function类型并不是我们想要的最终结果;Object能描述所有对象,类似地,Functoin也可以表示所有函数,但是并不是能体现函数的具体类型

function sum(a: number, b: number): number {
  return a + b
}

(a: number, b: number) => number是TypeScript表示函数类型的句法,也称调用签名。如果把函数作为参数传给另一个函数,或者作为函数的返回值,就是使用这样的句法注解

ab这两个参数名称只是一种表意手段,不影响类型函数的可赋值性

函数的调用签名只包含类型层面的代码,即只有类型,没有值。因此函数的调用签名可以表示参数的类型、this的类型、返回值的类型、剩余参数的类型和可选参数的类型,但是无法推导默认值(因为默认值是值,不是类型)。调用签名没有函数的定义体,无法推导出返回类型,所以必须显示注解

type User = (nickname: string, email: string) => void;
​
let user: User = (nickname, email) => {
    console.log(`${nickname}的邮箱:${email}`)
}
​
user('Forest', '767425412@qq.com') // Forest的邮箱:767425412@qq.com

首先声明了一个函数表达式user,其类型为User;因为定义User时,已经注解了成员变量的类型,所以user中的形参就不用再次注解类型了,TypeScript能从User中推导出来

上下文类型推导

在上面的函数表达式user中把类型声明为User,所以TypeScript能从上下文中推导出nicknameemail的类型,这个行为就被称为上下文类型推导,这也是TypeScript类型推导的一大特性

function times(f: (index: number) => void, n: number) {
    for (let i = 0; i < n; i++) {
        f(i)
    }
}
​
times(n => console.log(n), 6)

以上代码会依次打印0、1、2、3、4、5;TypeScript能从上下文中推导出n是个数字,因为在times的签名中,我们声明f的参数index是一个数字

函数重载

函数重载就是有多个调用签名的函数

在大多数编程语言中,声明函数时一旦指定了特定的参数和返回类型,就只能使用相应的参数调用函数,而且返回值的类型是中如一。但是JavaScript的情况并非如此;因为JavaScript是一门动态语言,是比需要以多种方式调用一个函数的方法;不仅如此,而且又是输出的类型取决于输入的参数类型

TypeScript也支持动态函数声明,而且函数的输出类型取决于输入类型,这一切都得益于TypeScript的静态类型系统

一般来说,声明重载的函数类型时,每个重载的签名都必须可以赋值给实现的签名。但是,声明实现的签名时一般kennel会更宽泛一些,保证所有重载的签名口可以赋值给实现的签名

let reserve: Reserve = (
  from: any,
  toOrDestinatoin: any,
  destination?: any
) => {
  //....
}

上述代码是可行;重载时应尽可能让实现的签名具体一些,这样容易实现函数

浏览器DOM API有大量重载;例如创建html元素的createElement,其参数为表示html标签的字符串,返回值为对应类型的html元素;TypeScript内置了每个html元素的类型。例如:

  • 表示<a>元素的HTMLAnchorElement
  • 表示<canvas>元素的HTMLCanvasElement
  • 表示<table>元素的HTMLTableElement
  • ......

通过重载来实现createElement,如下:

type CreateElement = {
    (tag: 'a'): HTMLAnchorElement       // 重载参数的类型,与字符串字面量类型匹配
    (tag: 'canvas'): HTMLCanvasElement
    (tag: 'table'): HTMLTableElement
    (tag: string): HTMLElement          // 如果传入自定义元素或者实验性元素而TS还没有对应的类型声明就返回HTMLElement
}
​
// 重载方法的实现
function createElement(tag: 'a'): HTMLAnchorElement
function createElement(tag: 'canvas'): HTMLCanvasElement
function createElement(tag: 'table'): HTMLTableElement
function createElement(tag: string): HTMLElement {
    // ...
}

TypeScript是按照声明的顺序解析重载。完整的类型签名并不只限于重载调用函数的方式,还可以描述函数的属性