这一次,彻底掌握TypeScript(四)函数


theme: github

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

这是我彻底掌握 TypeScript 的第四篇,这次我们主要分享 TypeScript 中的函数,点击下面链接可以查看之前文章。

往期文章:

这一次,彻底掌握TypeScript(一)基本类型&语法

这一次,彻底掌握TypeScript(二)接口与类

这一次,彻底掌握TypeScript(三)断言与类型别名

众所周知函数是 JavaScript 中的一等公民,其在 JavaScript 中扮演着举足轻重的角色,在 JavaScript 中我们可以使用箭头函数、function 关键字来创建一个函数,那么 TypeScript 中的函数又是怎样的呢?迭代器函数、生成器函数在 TypeScript 中又是如何定义的?

一、TypeScript 中函数与 JavaScript 中的区别

TypeScript JavaScript
含有类型 无类型
箭头函数 剪头函数(ES2015)
函数类型 无函数类型
必填参数和可选参数 所有参数都是可选的
默认参数 默认参数
剩余参数 剩余参数
函数重载 无函数重载

二、函数的定义

TypeScript 中函数的定义方式有如下几种:

// 函数类型的定义及实现
function add1(x: number, y: number) {
  return x + y;
}

// 通过变量名定义函数类型,后需指定具体实现
let add2: (x: number, y: number) => number; // 无法描述函数重载

// 通过类型别名定义函数类型,后需指定具体实现
type add3 = (x: number, y: number) => number; // 无法描述函数重载
type add4 = {
  (x: number, y: number): number
}

// 通过接口定义函数类型,后需指定具体实现
interface add5 {
  (x: number, y: number): number;
}

// 函数构造方法
let greet5 = new Function('name', 'return "hello " + name')

但需要注意函数构造方法并不被推荐,当我们用这种方式编写函数实例我们发现其类型为 Function,具体表现为一个可调用对象,具有 Function.prototype 的所有原型方法。但是这里并没有体现出参数和返回值的类型,因此可以使用任何参数类型调用函数,因此并不是类型安全的。

三、可选参数与默认参数

// 可选参数
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

// 默认参数
function createUserId(
  name: string = "semlinker",
  id: number,
  age?: number
): string {
  return name + id;
}

此处需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。

四、剩余参数

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);

一个函数最多只能有一个剩余参数,而且必须位于参数列表的最后。

五、注解 this 的类型

TypeScript 支持通过在函数的第一个参数中声明 this 类型的方式对 this 的使用符合预期,举个🌰:

function fancyDate(this: Date) {
  return `${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`
}

这儿的 this 不是常规的参数,而是保留字,是函数签名的一部分。

如果想强制显式注解函数中的 this 类型,可以在 tsconfig.json 中启用 noImpicitThis 设置(strict 模式包括 noImpicitThis 设置)。

六、迭代器

从概念上讲,迭代器是一个对象,它允许我们遍历某些容器(列表、数组,…)。在 Javascript 中,这个概念转换为定义了有 next 方法的对象,该方法返回一个具有 value 和 done 属性的对象。

  • value:迭代序列中的下一个值。如果存在 when done === false,那么它就是迭代器的返回值。
  • done:布尔值。指示序列是否已完成。

在 TypeScript 中提供了如下迭代器接口:

interface IteratorYieldResult<TYield> {
  done?: false
  value: TYield
}
interface IteratorReturnResult<TRutern> {
  done: true
  value: TRutern
}
type IteratorResult<T, TRutern = any> = IteratorYieldResult<T> | IteratorReturnResult<TRutern>

interface Iterator<T, TReturn = any, TNext = undefined> {
  // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
  next(...args: [] | TNext[]): IteratorResult<T, TReturn>
  return?(value?: TReturn): IteratorResult<T, TReturn> 
  throw?(e?: any): IteratorResult<T, TReturn> 
} 

迭代器接口中还有另外两个可选方法,return 和 throw. 基本上,return允许我们向迭代器发出信号,表明它应该完成(将 done 设置为 true)并返回其返回值,throw 允许您将错误传递给它可能知道如何处理的迭代器。

在这儿我们还需要提到另一个概念可迭代对象。可迭代对象是实现了 Symbol.interator 方法的任何对象。这意味着对象(或它的原型链中的任何对象)必须有一个方法,由 Symbol.iterator 键索引,返回一个迭代器。TypeScript 中对于可迭代对象的类型描述如下:

interface Interable<T> {
  [Symbol.iterator](): Iterator<T>
}

除了 Iterable 接口,我们还有一个名为 IterableIterator 的接口,表示既是可迭代对象,又是迭代器,这在描述生成器函数的时候非常有用。

interface IterableIterator<T> extends Iterator<T> {
  [Symbol.iterator](): IterableIterator<T>
}

我们可以使用迭代器在二叉树上实现 BFS(广度优先遍历算法)。

首先,我们定义我们的二叉树:

class BinaryTreeNode<T> {
  constructor(
    public value: T,
    public left?: BinaryTreeNode<T>,
    public right?: BinaryTreeNode<T>,
  ){}
  getChildren(): BinaryTreeNode<T>[] {
    const children: BinaryTreeNode<T>[] = []
    if (this.left) children.push(this.left)
    if (this.right) children.push(this.right)
  }
}

class Tree<T = unknown> implements {
  constructor(
    private root: BinaryTreeNode<T>
  ){}
  [Symbol.iterator](): Iteratro<BinaryTreeNode<T>> {
    return new BfsIterator(this.root)
  } 
}

这里我们实现了一个最简单的二叉树,紧接着我们来实现迭代器:

class BfsIterator<T> implements Iterator<BinaryTreeNode<T>, number | undefined> {
  private currentRow: BinaryTreeNode<T>[];
  private currentNodeIndex: number;
  private numberOfNodes: number;
  private done: boolean;
  constructor(root: BinaryTreeNode<T>) {
    this.currentRow = [root]
    this.currentNodeIndex = 0
    this.done = false
    this.numberOfNode = 0
  }
  next(): IteratorResult<BinaryTreeNode<T>, number | undefined> {
    if (this.done) {
      return {
        done: true,
        value: undefined
      }
    }
    if (this.currentNodeIndex === this.currentRow.length) {
      this.currentRow = this.getNewRow()
      this.currentNodeIndex = 0
      if (this.currentRow.length === 0) {
       this.done = true
       return {
        done: true,
        value: this.numberOfNode
       } 
      }
    }
    const result: IteratorYieldResult<BinaryTreeNode<T>> = {
      done: false,
      value: this.currentRow[this.currentNodeIndex]
    }
    this.currentNodeIndex += 1;
    this.numberOfNodes += 1;
    return result
  }
  private getNewRow(): BinaryTreeNode<T>[] {
    return this.currentRow.map((node) => node.getChildren()).flat()
  }
}

七、生成器函数

迭代器允许我们完全控制迭代某个结构,我们可以决定是否以及何时获得迭代序列的下一个元素,同时向迭代器的消费者隐藏我们如何获取这些元素的实现细节。然而,一切都是有代价的,迭代器实现起来可能相当棘手,因为我们必须跟踪控制执行流程的状态,以便我们可以将迭代器标记为完成。生成器就是为了简化迭代器的创建流程提出的,我们首先来写一个简单的斐波那契数列生成器函数:

function* createFibonacciGenerator() {
  let a = 1
  let b = 1
  while (true) {
    yield a;
    [a, b] = [b, a + b]
  }
}

生成器的用法如上所示,生成器使用 yield 关键字产出值。使用方让生成器提供下一个值时(例如,调用 next),yield 把结果发给使用方,然后停止执行,直到使用方要求提供下一个值为止。我们查看这里 createFibonacciGenerator 函数的类型为 function createFibonacciGenerator(): Generator<number, void, unknown>,那么 Generator 类型到底是什么呢?

interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext> {
  // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
  next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
  return(value: TReturn): IteratorResult<T, TReturn>;
  throw(e: any): IteratorResult<T, TReturn>;
  [Symbol.iterator](): Generator<T, TReturn, TNext>;
}

大家可以发现这里的 Generator 接口与 IterableIterator 非常相像,那么它们之间有什么不同呢?这里有非常全面的讨论。

要命令生成器执行我们的代码,我们只需要调用next:

let fibonacciGenerator = createFibonacciGenerator() // Generator<number, void, unknown>
fibonacciGenerator.next() // { value: 1, done: false }
fibonacciGenerator.next() // { value: 1, done: false }
fibonacciGenerator.next() // { value: 2, done: false }
fibonacciGenerator.next() // { value: 3, done: false }

八、函数的重载

在静态类型的语言中例如 C++、Java 都有函数重载的概念,它们本质上还是使用相同名称和不同参数数量或类型的多个函数,函数重载的好处在于我们不需要为相似或相同功能的函数选择不同的名称,这样增强了函数的可读性, TypeScript 中的函数重载与 C++、Java 中的有所不同,举个🌰:

// 重载签名
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
// 在一个最宽泛的版本中实现函数重载
function add(a: string | number, b: string | number) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

TypeScript 中函数的重载总的来说分为两步:

  • 声明重载的函数签名
  • 在组合后的函数签名中实现函数体

我们用更能说明这边的示例来进行演示:

// 函数重载签名列表
type Student {
  (name: string, classroom: string, age: number): void
  (name: string, age: number): void
}

// 在组合后的签名进行实现
let student: Student = (name: string, classroomOrAge: string | number, age?: number) => {
  if (typeof classroomOrAge === 'string') {
    ...
  }
  if (age !== undefined) {
    ...
  }
}

与类的方法的重载类似,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义,因此,在定义重载的时候,一定要把最精确的定义放在最前面。

需要注意的是 TypeScript 中的函数重载没有任何运行时开销,它只允许你记录希望调用函数的方式,并且编译器会检查其余代码,举个🌰,上述示例编译成 JavaScript 后的内容如下所示:

"use strict";
function add(a, b) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

参考资料

极客时间《TypeScript开发实战》专栏

《深入理解TypeScript》

Iterators in TypeScript

Generators in Typescript

一份不可多得的 TS 学习指南(1.8W字)

Typescript使用手册

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容