Skip to content
0

TypeScript - 函数

函数介绍

函数是 JavaScript 应用程序的基础,它帮助你实现抽象层,模拟类,信息隐藏和模块。在 TypeScript 里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。TypeScript 为 JavaScript 函数添加了额外的功能,让我们可以更容易地使用。

基本示例

和 JavaScript 一样,TypeScript 函数可以创建有名字的函数和匿名函数。你可以随意选择适合应用程序的方式,不论是定义一系列 API 函数还是只使用一次的函数。

通过下面的例子可以迅速回想起这两种 JavaScript 中的函数:

javascript
// 命名函数
function add(x, y) {
  return x + y;
}

// 匿名函数
let myAdd = function (x, y) {
  return x + y;
};

在 JavaScript 里,函数可以使用函数体外部的变量。当函数这么做时,我们说它「捕获」了这些变量。至于为什么可以这样做以及其中的利弊超出了本文的范围,但是深刻理解这个机制对学习 JavaScript 和 TypeScript 会很有帮助。

javascript
let z = 100;

function addToZ(x, y) {
  return x + y + z;
}

函数类型

为函数定义类型

让我们为上面那个函数添加类型:

typescript
function add(x: number, y: number): number {
  return x + y;
}

let myAdd = function (x: number, y: number): number {
  return x + y;
};

我们可以给每个参数添加类型之后再为函数本身添加返回值类型。TypeScript 能够根据返回语句自动推断出返回值类型。

书写完整函数类型

现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。

typescript
let myAdd: (x: number, y: number) => number = function (
  x: number,
  y: number
): number {
  return x + y;
};

函数类型包含两部分:参数类型和返回值类型。当写出完整函数类型的时候,这两部分都是需要的。我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。这个名字只是为了增加可读性。我们也可以这么写:

typescript
let myAdd: (baseValue: number, increment: number) => number = function (
  x: number,
  y: number
): number {
  return x + y;
};

只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。

第二部分是返回值类型。对于返回值,我们在函数和返回值类型之前使用(=>)符号,使之清晰明了。如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void不能留空

函数的类型只是由参数类型和返回值组成的。函数中使用的捕获变量不会体现在类型里。实际上,这些变量是函数的隐藏状态并不是组成 API 的一部分。

推断类型

尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript 编译器会自动识别出类型:

typescript
let myAdd = function (x: number, y: number): number {
  return x + y;
};

let myAdd: (baseValue: number, increment: number) => number = function (x, y) {
  return x + y;
};

这叫做「按上下文归类」,是类型推论的一种。它帮助我们更好地为程序指定类型。

可选参数和默认参数

TypeScript 里的每个函数参数都是必须的。这不是指不能传递 nullundefined 作为参数,而是说编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。

typescript
function buildName(firstName: string, lastName: string) {
  return firstName + " " + lastName;
}

let result1 = buildName("Bob"); // Error, 参数过少
let result2 = buildName("Bob", "Adams", "Sr."); // Error, 参数过多
let result3 = buildName("Bob", "Adams"); // OK

JavaScript 里,每个参数都是可选的,可传可不传。没传参的时候,它的值就是 undefined。在 TypeScript 里我们可以在参数名旁使用 ? 实现可选参数的功能。比如,我们想让 lastName 是可选的:

typescript
function buildName(firstName: string, lastName?: string): string {
  if (lastName) return firstName + " " + lastName;
  else return firstName;
}

let result1 = buildName("Bob"); // 现在正常了
let result2 = buildName("Bob", "Adams", "Sr."); // Error, 参数过多
let result3 = buildName("Bob", "Adams"); // OK

可选参数必须跟在必须参数后面。如果上例我们想让 firstName 是可选的,那么就必须调整它们的位置,把 firstName 放在后面。

在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是 undefined 时。它们叫做有默认初始化值的参数。让我们修改上例,把lastName 的默认值设置为 "Smith"

typescript
function buildName(firstName: string, lastName = "Smith"): string {
  return firstName + " " + lastName;
}

let result1 = buildName("Bob"); // 返回 "Bob Smith"
let result2 = buildName("Bob", undefined); // 正常, 同样 "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // 错误, 参数过多
let result4 = buildName("Bob", "Adams"); // OK

与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined 值来获得默认值。例如,我们重写最后一个例子,让 firstName 是带默认值的参数:

typescript
function buildName(firstName = "Will", lastName: string): string {
  return firstName + " " + lastName;
}

let result1 = buildName("Bob"); // Error, 参数过少
let result2 = buildName("Bob", "Adams", "Sr."); // Error, 参数过多
let result3 = buildName("Bob", "Adams"); // OK,返回 "Bob Adams"
let result4 = buildName(undefined, "Adams"); // OK,返回 "Will Adams"

剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。在 JavaScript 里,你可以使用 arguments 来访问所有传入的参数。

在 TypeScript 里,你可以把所有参数收集到一个变量里:

typescript
function buildName(firstName: string, ...restOfName: string[]): string {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

剩余参数会被当做个数不限的可选参数。可以一个都没有,同样也可以有任意个。编译器创建参数数组,名字是你在省略号( ...)后面给定的名字,你可以在函数体内使用这个数组。

这个省略号也会在带有剩余参数的函数类型定义上使用到:

typescript
function buildName(firstName: string, ...restOfName: string[]): string {
  return firstName + " " + restOfName.join(" ");
}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this

学习如何在 JavaScript 里正确使用 this 就好比一场成年礼。由于 TypeScript 是 JavaScript 的超集,TypeScript 程序员也需要弄清 this 工作机制并且当有 bug 的时候能够找出错误所在。幸运的是,TypeScript 能通知你错误地使用了 this 的地方。如果你想了解 JavaScript 里的 this 是如何工作的,那么首先阅读 Yehuda Katz 写的 Understanding JavaScript Function Invocation and "this"。Yehuda 的文章详细的阐述了 this 的内部工作原理,因此我们这里只做简单介绍。

this 和箭头函数

JavaScript 里,this 的值在函数被调用的时候才会指定。这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。但众所周知,这不是一件很简单的事,尤其是在返回一个函数或将函数当做参数传递的时候。

下面看一个例子:

typescript
let deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array(52),
  createCardPicker: function () {
    return function () {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);

      return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
    };
  },
};

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

console.log("card: " + pickedCard.card + " of " + pickedCard.suit);

可以看到 createCardPicker 是个函数,并且它又返回了一个函数。如果我们尝试运行这个程序,会发现它并没有输出而是报错了。因为 createCardPicker 返回的函数里的 this 被设置成了 global 而不是 deck 对象。因为我们只是独立的调用了 cardPicker()。顶级的非方法式调用会将 this 视为 global

为了解决这个问题,我们可以在函数被返回时就绑好正确的this。这样的话,无论之后怎么使用它,都会引用绑定的deck 对象。我们需要改变函数表达式来使用 ECMAScript 6 箭头语法。箭头函数能保存函数创建时的 this 值,而不是调用时的值:

typescript
let deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array(52),
  createCardPicker: function () {
    // 注意:这里使用箭头函数
    return () => {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);

      return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
    };
  },
};

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

console.log("card: " + pickedCard.card + " of " + pickedCard.suit);

this 参数

在上述的例子中 this.suits[pickedSuit] 的类型为 any,这是因为 this 来自对象字面量里的函数表达式。修改的方法是,提供一个显式的 this 参数。this 参数是个假的参数,它出现在参数列表的最前面:

typescript
function f(this: void) {
  // 确保 this 在此独立函数中不可用
}

让我们往例子里添加一些接口,CardDeck,让类型重用能够变得清晰简单些:

typescript
interface Card {
  suit: string;
  card: number;
}

interface Deck {
  suits: string[];
  cards: number[];

  createCardPicker(this: Deck): () => Card;
}

let deck: Deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array(52),
  // NOTE: 函数现在显式指定其被调用方必须是 deck 类型
  createCardPicker: function (this: Deck) {
    return () => {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);

      return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
    };
  },
};

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

console.log("card: " + pickedCard.card + " of " + pickedCard.suit);

现在 TypeScript 知道 createCardPicker 期望在某个 Deck 对象上调用。也就是说 thisDeck 类型的,而非 any

this 参数在回调函数里

你可以也看到过在回调函数里的 this 报错,当你将一个函数传递到某个库函数里稍后会被调用时。因为当回调被调用的时候,它们会被当成一个普通函数调用,this 将为 undefined。稍做改动,你就可以通过 this 参数来避免错误。首先,库函数的作者要指定 this 的类型:

typescript
interface UIElement {
  addClickListener(onclick: (this: void, e: Event) => void): void;
}

this: void 意味着 addClickListener 期望传入的 onclick 方法不需要 this

typescript
interface UIElement {
  addClickListener(onclick: (this: void, e: Event) => void): void;
}

class Handler {
  type: string;

  onClickBad(this: Handler, e: Event) {
    this.type = e.type;
  }
}

let h = new Handler();

let uiElement: UIElement = {
  addClickListener() {},
};

uiElement.addClickListener(h.onClickBad); // error!

指定了 this 类型后,你显式声明 onClickBad 必须在 Handler 的实例上调用。然后 TypeScript 会检测到 addClickListener 要求函数带有 this: void。改变 this 类型来修复这个错误:

typescript
class Handler {
  type: string;

  onClickBad(this: void, e: Event) {
    console.log("clicked!");
  }
}

let h = new Handler();

let uiElement: UIElement = {
  addClickListener() {},
};

uiElement.addClickListener(h.onClickBad);

因为 onClickGood 指定了 this 类型为 void,因此传递 addClickListener 是合法的。当然了,这也意味着不能使用 this.info。如果你两者都想要,你不得不使用箭头函数了:

typescript
class Handler {
  type: string;
  onClickGood = (e: Event) => {
    this.type = e.type;
  };
}

这是可行的因为箭头函数不会捕获 this,所以你总是可以把它们传给期望 this: void 的函数。

函数重载

函数重载:函数名称相同,但是参数的个数或者类型不同。

JavaScript 本身是个动态语言。JavaScript 里函数根据传入不同的参数而返回不同类型的数据的场景是很常见的。

typescript
function getMessage(type: number | string): number | string {
  if (typeof type === "number") {
    return 27;
  } else {
    return "kele";
  }
}

// 报错,无法是返回类型是 number 还是 string
let msg: number = getMessage(1);

但是这样有个问题,就是这个函数的返回类型无论在什么时候都是 number | string 类型。

但是实际上它在参数为 number 的时候返回值一定是 number,在参数为 string 的时候一定是 string。出现这个问题的原因也很简单,编辑器并不能知道入参是什么类型的,返回值也不能确定,那这个函数的签名该怎么写?

这时可以为这个函数提供多个函数类型定义来进行函数重载,可以完美的解决这个问题。

typescript
// 重载签名
function getMessage(type: number): number;
// 重载签名
function getMessage(type: string): string;
// 实现签名
function getMessage(type: number | string): number | string {
  if (typeof type === "number") {
    return 27;
  } else {
    return "kele";
  }
}

let msg = getMessage(1);

前两个 getMessage 函数是重载签名,第三个是 getMessage 函数是实现签名。

一旦调用了 getMessage 函数,TS 会根据传入的参数类型进行判断,从而调用符合该参数类型的函数(第一个)。

函数重载有一下几种规则:

  • 有一个实现签名 + 一个或多个重载签名合成
  • 但外部调用函数重载定义的函数时,只能调用重载签名,不能调用实现签名
  • 调用重载签名的函数时,会根据传递的参数来判断你调用的是哪一个函数
  • 只有一个函数体,只有实现签名配备了函数体,所有的重载签名都只有签名,没有配备函数体

也就是实现签名是给看的,实际调用的是符合参数类型的重载签名,然后再调用实现签名的内部代码。

最近更新