Functions
函数
介绍
函数是JavaScript中任何应用程序的基本构建块。他们是如何建立抽象层,模仿课程,信息隐藏和模块。在TypeScript中,虽然有类,名称空间和模块,但函数在描述如何执行
操作时仍然起着关键作用。TypeScript还为标准JavaScript函数增加了一些新功能,使它们更易于使用。
功能
首先,就像在JavaScript中一样,TypeScript函数既可以作为命名函数创建,也可以作为匿名函数创建。这允许您为应用程序选择最合适的方法,无论您是在API中创建函数列表还是在一次性函数中切换到另一个函数。
为了快速回顾这两种方法在JavaScript中的样子:
// Named function
function add(x, y) {
return x + y;
}
// Anonymous function
let myAdd = function(x, y) { return x + y; };
就像在JavaScript中一样,函数可以引用函数体外的变量。当他们这样做时,他们会对capture
这些变数说。理解这种工作方式以及使用这种技术时的权衡不在本文的讨论范围之内,要深刻理解这种机制是如何处理JavaScript和TypeScript的重要内容。
let z = 100;
function addToZ(x, y) {
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; };
函数的类型具有相同的两部分:参数的类型和返回类型。在写出整个函数类型时,这两个部分都是必需的。我们写出参数类型就像参数列表一样,给每个参数一个名称和一个类型。这个名字只是为了提高可读性。我们可以写下:
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };
只要参数类型对齐,它就被认为是该函数的有效类型,而不管您在函数类型中给出参数的名称。
第二部分是返回类型。我们通过=>在参数和返回类型之间使用粗箭头()来明确哪个是返回类型。如前所述,这是函数类型的必需部分,所以如果函数没有返回值,则可以使用void而不是将其关闭。
值得注意的是,只有参数和返回类型组成了函数类型。捕获的变量不会反映在类型中。实际上,捕获的变量是任何函数的“隐藏状态”的一部分,并不构成其API。
推断类型
在玩这个例子时,你可能注意到TypeScript编译器可以计算出类型,如果你在等式的一边有类型而不是另一边:
// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters 'x' and 'y' have the type number
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };
这被称为“上下文类型”,这是一种类型推断的形式。这有助于减少保持程序类型的努力量。
可选和默认参数
在TypeScript中,假定每个参数都是该函数所需的。这并不意味着它不能给予null
或undefined
,而是,当函数被调用时,编译器会检查用户已提供给每个参数的值。编译器还假设这些参数是传递给函数的唯一参数。简而言之,赋予函数的参数数量必须与函数期望的参数数量相匹配。
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob" // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr." // error, too many parameters
let result3 = buildName("Bob", "Adams" // ah, just right
在JavaScript中,每个参数都是可选的,并且用户可以在他们认为合适的时候关闭它们。当他们这样做时,他们的价值是undefined
。我们可以通过在?
我们希望成为可选参数的参数的末尾添加一个TypeScript来获得此功能。例如,假设我们希望上面的姓氏参数是可选的:
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", "Adams", "Sr." // error, too many parameters
let result3 = buildName("Bob", "Adams" // ah, just right
任何可选参数都必须遵循所需参数。如果我们想让名字可选而不是姓氏,我们需要改变函数中参数的顺序,将名字的最后名字放在列表中。
在TypeScript中,我们还可以设置一个值,如果用户没有提供参数,或者用户通过undefined
它,则会分配参数。这些被称为默认初始化参数。我们来看前面的例子,默认为姓"Smith"
。
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob" // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr." // error, too many parameters
let result4 = buildName("Bob", "Adams" // ah, just right
在所有必需参数之后出现的默认初始化参数被视为可选参数,并且与可选参数一样,在调用它们各自的功能时可省略。这意味着可选参数和尾随默认参数将在它们的类型中共享,所以两者都是
function buildName(firstName: string, lastName?: string) {
// ...
}
且
function buildName(firstName: string, lastName = "Smith") {
// ...
}
共享相同的类型(firstName: string, lastName?: string) => string。该lastName类型中的默认值消失,只留下参数是可选的。
不同于普通的可选参数,默认初始化参数并不需要
必要的参数后发生。如果默认初始化参数位于必需参数之前,则用户需要
显式传递undefined
以获取默认初始化值。例如,我们可以写上我们的最后一个例子,只有一个默认的初始化器firstName
:
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob" // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr." // error, too many parameters
let result3 = buildName("Bob", "Adams" // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams" // okay and returns "Will Adams"
其余参数
必需参数,可选参数和默认参数都有一个共同点:它们一次只讨论一个参数。有时候,你想用多个参数作为一个组,或者你可能不知道一个函数最终需要多少个参数。在JavaScript中,您可以直接使用arguments
每个函数体内可见的变量处理参数。
在TypeScript中,您可以将这些参数一起收集到一个变量中:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" "
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie"
Rest参数
被视为可选参数的无限数量。当传递一个rest参数的参数时,可以使用尽可能多的参数; 你甚至可以不通过。编译器将用省略号(...
)后给出的名称构建传入的参数数组,允许您在函数中使用它。
省略号也用于具有其余参数的函数类型:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" "
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
this
学习如何this
在JavaScript中使用是成功的。由于TypeScript是JavaScript的超集,因此TypeScript开发人员还需要了解如何使用this
以及如何在未正确使用时进行识别。幸运的是,TypeScript允许您this
通过几种技术捕获不正确的用法。但是,如果您需要了解this
JavaScript的工作方式,请首先阅读Yehuda Katz的“ 理解JavaScript函数调用”和“this
”。耶胡达的文章this
很好地解释了内部运作,所以我们将在这里介绍基本知识。
this 和箭头功能
在JavaScript中,this
是一个调用函数时设置的变量。这使得它成为一个非常强大和灵活的特性,但它的代价是必须知道函数正在执行的上下文。这是非常混乱的,特别是在返回函数或传递函数作为参数时。
我们来看一个例子:
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(
alert("card: " + pickedCard.card + " of " + pickedCard.suit
注意这createCardPicker
是一个本身返回一个函数的函数。如果我们尝试运行该示例,则会出现错误,而不是预期的警报框。这是因为在this
创建的函数中使用的createCardPicker
将被设置为window
而不是我们的deck
对象。那是因为我们cardPicker()
自己要求。像这样的顶级非方法语法调用将window
用于this
。(注意:在严格模式下,this
将undefined
取而代之window
)。
我们可以通过确保函数被绑定到正确的位置来解决这个问题,this
然后再返回稍后使用的函数。这样,无论以后如何使用它,它仍然能够看到原始deck
对象。为此,我们将函数表达式更改为使用ECMAScript 6箭头语法。Arrow函数捕获this
函数的创建位置而不是它被调用的地方:
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
// NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
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(
alert("card: " + pickedCard.card + " of " + pickedCard.suit
更好的是,如果你将--noImplicitThis
标志传递给编译器,TypeScript会在你犯这个错误时发出警告。它会指出,this
在this.suits[pickedSuit]
为类型any
。
this 参数
不幸的是,这种类型this.suits[pickedSuit]
仍然存在any
。这是因为this
来自对象文本中的函数表达式。要解决这个问题,你可以提供一个明确的this
参数。this
参数是在参数列表中首先出现的伪参数:
function f(this: void) {
// make sure `this` is unusable in this standalone function
}
让我们在上面的示例中添加几个接口,Card
并且Deck
使类型更清晰,更易于重用:
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: The function now explicitly specifies that its callee must be of type 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(
alert("card: " + pickedCard.card + " of " + pickedCard.suit
现在TypeScript知道,createCardPicker
期望在一个Deck
对象上被调用。这意味着现在this
是类型的Deck
,而不是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;
onClickBad(this: Handler, e: Event) {
// oops, used this here. using this callback would crash at runtime
this.info = e.message;
}
}
let h = new Handler(
uiElement.addClickListener(h.onClickBad // error!
有了this
注释,你可以明确指出onClickBad
必须在一个实例上调用Handler
。然后TypeScript将检测到addClickListener
需要一个函数this: void
。要修复错误,请更改以下类型this
:
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// can't use this here because it's of type void!
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.random() * 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[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit
let pickedCard2 = pickCard(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 the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * 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[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit
let pickedCard2 = pickCard(15
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit
通过这个改变,重载现在给我们提供了对pickCard
函数进行类型检查的调用。
为了让编译器选择正确的类型检查,它遵循与底层JavaScript相似的过程。它查看重载列表,并继续尝试使用提供的参数调用该函数。如果发现匹配,它会将此超载选作正确的超载。出于这个原因,它习惯于从最具体到最不具体的顺序重载。
请注意,该function pickCard(x): any
作品不是重载列表的一部分,因此它只有两个重载:一个取对象和一个取数字。pickCard
使用任何其他参数类型调用都会导致错误。