TypeScript 之函数

picture

介绍

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


函数

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

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

// Named function
funnction add(x, y) {
    return x + y;
}
// Anonymous function
const myAdd = function(x, y) {
    return x + y;
}

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

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

函数类型

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

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

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

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


书写完整函数类型

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

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

函数类型包含两部分:

  • 1、参数类型
  • 2、返回值类型
    当写出完整函数类型的时候,这两部分是需要的。我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。这个名字只是为了增加可读性。我们也可以这么写:
let myAdd: (baseValue: number, increment: number) => = function(x: number, y: number): number {
    return x + y;
}

只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
第二部分是返回值类型。对于返回值,我们在函数和返回值类型之前使用 => 符号,使之清晰明了。如之前提到的,返回值类型是函数类型的必要部分,如果函数没有任何返回值,你也必须指定返回值类型为 void 而不能留空。
函数的类型只是由类型和返回值组成的。函数中使用的捕获变量不会体现在类型里。实际上,这些变量是函数的隐藏状态,并不是组成 API 的一部分。


推断类型

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

// myAdd has the full function type
let myAdd: (x: number, y: number) => number = function(x: number, y: number): number {
    return x + y;
}
// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => = function(x, y): number {
    return x + y;
}

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


可选参数和默认参数

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

function buildName(fristName: string, lastName: string) {
  return firstName + '' + lastName;
}

let result1 = buildName('Bob'); // error, too few parameters
let result2 = buildName('Bob', 'Admas', 'Sr.'); // error, too many parameters
let result3 = buildName('Bob', 'Admas'); // right

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

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

let result1 = buildName('Bob'); // works correctly now
let result2 = buildName('Bob', 'Admas', 'Sr.'); // Expected 1-2 arguments, but got 3.
let result3 = buildName('Bob', 'Admas'); // right

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

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

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

let result1 = buildName('Bob'); // Bob Smith
let result2 = buildName('Bob', undefined); // Bob Smith
let result3 = buildName('Bob', 'Admas', 'Sr.'); // Expected 1-2 arguments, but got 3.
let result4 = buildName('Bob', 'Admas'); //  Bob Admas

在所有必须参数后面的带初始默认话的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。也就是说可选参数与末尾的默认参数共享参数类型。

function buildName(firstName: string, lastName?: string) {
  // ...
}

function buildName(firstName: string, lastName = 'Smith') {
  // ...
}

共享同样的类型(firstName: string, lastName?: string) => string。默认参数的默认值消失了,只保留了它是一个可选参数的信息。

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

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

let result1 = buildName('Bob'); // error, too few parameters
let result2 = buildName('Bob', 'Admas', 'Sr.'); // error, too many parameters
let result3 = buildName('Bob', 'Admas'); // okay and returns "Bob Admas"
let result4 = buildName(undefined, 'Admas'); // okay and returns "Will Admas"

剩余参数

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

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

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + ' ' + restOfName.join(' ');
}
let employeeName = buildName('Joseph', 'Samuel', 'Lucas', 'MacKinzie');
// Joseph Samuel Lucas MacKinzie

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

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

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

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

This

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


this 和 箭头函数

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

下面看一个例子:

let deck = {
  suits: ['hearts', 'spades', 'clubs', 'diamonds'],
  cards: Array(52),
  createCardPicker: function() {
    return function() {
      let pickerCard = Math.floor(Math.random() * 52);
      let pickerSuit = Math.floor(pickerCard / 13);
      return {
        suit: this.suits[pickerSuit],
        card: pickerCard % 13
      };
    };
  }
};

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

alert('card: ' + pickedCard.card + ' of ' + pickedCard.suit);

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

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

let deck = {
  suits: ['hearts', 'spades', 'clubs', 'diamonds'],
  cards: Array(52),
  createCardPicker: function() {
    return () => {
      let pickerCard = Math.floor(Math.random() * 52);
      let pickerSuit = Math.floor(pickerCard / 13);
      return {
        suit: this.suits[pickerSuit],
        card: pickerCard % 13
      };
    };
  }
};

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

alert('card: ' + pickedCard.card + ' of ' + pickedCard.suit);

更好事情是, TypeScript 会警告你犯了一个错误,如果你给编译器设置了 --nolmplicitThis 标记。它会指出 this.suits[pickedSuit] 里的 this 的类型为 ay


this 参数

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

function f(this: void) {
  // make sure `this` is unusable in this standalone function
}

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

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),
  // 注意:该函数现在显式地指定它的被调用方必须是Deck类型
  createCardPicker: function(this: Deck) {
    return () => {
      let pickerCard = Math.floor(Math.random() * 52);
      let pickerSuit = Math.floor(pickerCard / 13);
      return {
        suit: this.suits[pickerSuit],
        card: pickerCard % 13
      };
    };
  }
};

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

alert('card: ' + pickedCard.card + ' of ' + pickedCard.suit);

现在 TypeScript 知道 createCardPicker 期望在某个 Deck 对象上调用。也就是说 thisDeck 类型的,而非 any 类型,因此 --noImplicitThis 不会报错了。


this 参数在回调函数里

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

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

this: void 表示 addClickListener 期望 onclick 是不需要此this 类型的函数。其次,用 this 注释您的调用代码。

class Handler {
  info: string;
  // oops, used this here. using this callback would crash at runtime
  onClickBad(this: Handler, e: Event) {
    this.info = e.message;
  }
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!

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

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

class Handler {
  info: string;
  // oops, used this here. using this callback would crash at runtime
  onClickGood(this: void, e: Event) {
    console.log('clicked');
  }
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);

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

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

这是可行的因为箭头函数不会捕获 this ,所以你总是可以把它们传给期望 this: void 的函数。 缺点是每个 Handler 对象都会创建一个箭头函数。 另一方面,方法只会被创建一次,添加到 Handler 的原型链上。 它们在不同 Handler 对象间是共享的。


重载

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

let suits = ['hearts', 'spades', 'clubs', 'diamonds'];

function pickCard(x): any {
  // Check to see if we're working with an object/array
  // if so, they gave us the deck and we'll pick the card
  if (typeof x == 'object') {
    let pickedCard = Math.floor(Math.round() * x.length);
    return pickedCard;
  }
  // Otherwise just let them pick the card
  else if (typeof x == 'number') {
    let pickedSuit = Math.floor(x / 13);
    return {
      suit: suits[pickedSuit],
      card: x % 13
    };
  }
}

let myDeck = [
  { suit: 'diamonds', card: 2 },
  { suit: 'spades', card: 10 },
  { suit: 'hearts', card: 4 }
];
let pickedCard1 = myDeck[pickedCard(myDeck)];
alert('card' + pickedCard1.card + 'of' + pickedCard1.suit);

let pickedCard2 = pickedCard(15);
alert('card' + pickedCard2.card + 'of' + pickedCard2.suit);

pickCard 方法根据传入参数的不同会返回两种不同的类型。如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。如果用户想抓牌,我们告诉他抓到了什么牌。但是这怎么在类型系统里表示呢。

方法是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。
下面我们来重载 pickCard 函数。

let suits = ['hearts', 'spades', 'clubs', 'diamonds'];
function pickCard(x: {suit: string; card: number;}[]): number;
function pickCard(x: number): {
  suit: string;
  card: number;
};
function pickCard(x): any {
  // Check to see if we're working with an object/array
  // if so, they gave us the deck and we'll pick up the card
  if (typeof x = 'object') {
    let pickedCard= Math.floor(Math.round() * x.length);
    return pickerCard;
  }
  // Otherwise just let them pick up the card
  else if (typeof x == 'number') {
    let pickefSuit = Math.floor(x / 13);
    return {
      suit: suits['pickerSuit'],
      card: x % 13
    };
  }
}

let myDeck = [
  { suit: 'diamonds', card: 2 },
  { suit: 'spades', card: 10 },
  { suit: 'hearts', card: 4 }
];
let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);
alert('card' + pickedCard2.card + 'of' + pickedCard2.suit);

这样改变后,重载的 pickCard 函数在调用的时候会进行正确的类型检测。

为了让编译器能够选择正确的检查类型,它与 JavaScript 里的处理流程相似。它查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。

注意,function pickCard(x): any 并不是重载列表的一部分,因此这里只要两个重载:一个接收对象,另一个接收数字,以其它参数调用 pickCard 会产生错误。


本文参考来源: TypeScript 函数

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352