函数是 JavaScript 中任何应用程序的基本构建块。它们是构建抽象层的方式,模拟类、信息隐藏和模块。在 TypeScript 中,虽然有类、命名空间和模块,但函数仍然在描述如何“执行”方面发挥着关键作用。TypeScript 还为标准 JavaScript 函数添加了一些新功能,使它们更易于使用。
函数
首先,就像在 JavaScript 中一样,TypeScript 函数可以创建为命名函数或匿名函数。这使您可以为您的应用程序选择最合适的方法,无论您是在 API 中构建函数列表还是构建一个一次性函数来传递给另一个函数。
快速回顾一下这两种方法在 JavaScript 中的样子
tsTry
// Named functionfunctionadd (x ,y ) {returnx +y ;}// Anonymous functionletmyAdd = function (x ,y ) {returnx +y ;};
就像在 JavaScript 中一样,函数可以引用函数体之外的变量。当它们这样做时,据说它们捕获了这些变量。虽然理解这是如何工作的(以及使用此技术时的权衡)超出了本文的范围,但对这种机制如何工作的牢固理解是使用 JavaScript 和 TypeScript 的重要部分。
tsTry
letz = 100;functionaddToZ (x ,y ) {returnx +y +z ;}
函数类型
函数类型
让我们将类型添加到我们之前简单的示例中
tsTry
functionadd (x : number,y : number): number {returnx +y ;}letmyAdd = function (x : number,y : number): number {returnx +y ;};
我们可以为每个参数添加类型,然后为函数本身添加返回类型。TypeScript 可以通过查看 return 语句来推断出返回类型,因此在许多情况下,我们也可以选择省略它。
编写函数类型
现在我们已经为函数添加了类型,让我们通过查看函数类型的每个部分来写出函数的完整类型。
tsTry
letmyAdd : (x : number,y : number) => number = function (x : number,y : number): number {returnx +y ;};
函数的类型包含两个部分:参数的类型和返回类型。在写出整个函数类型时,这两个部分都是必需的。我们像写参数列表一样写出参数类型,为每个参数指定一个名称和一个类型。这个名称只是为了提高可读性。我们也可以写成
tsTry
letmyAdd : (baseValue : number,increment : number) => number = function (x : number,y : number): number {returnx +y ;};
只要参数类型一致,它就被认为是函数的有效类型,无论你在函数类型中为参数指定什么名称。
第二部分是返回类型。我们使用箭头 (=>
) 在参数和返回类型之间明确区分哪个是返回类型。如前所述,这是函数类型中必不可少的一部分,因此如果函数不返回值,你应该使用 void
而不是省略它。
需要注意的是,只有参数和返回类型构成了函数类型。捕获的变量不会反映在类型中。实际上,捕获的变量是任何函数的“隐藏状态”的一部分,不构成其 API。
推断类型
在使用示例时,你可能会注意到,即使你只在一侧添加了类型,TypeScript 编译器也能推断出类型
tsTry
// The parameters 'x' and 'y' have the type numberletmyAdd = function (x : number,y : number): number {returnx +y ;};// myAdd has the full function typeletmyAdd2 : (baseValue : number,increment : number) => number = function (x ,y ) {returnx +y ;};
这被称为“上下文类型”,是类型推断的一种形式。这有助于减少保持程序类型化的工作量。
可选参数和默认参数
在 TypeScript 中,每个参数都被认为是函数所需的。这并不意味着它不能被赋予 null
或 undefined
,而是当调用函数时,编译器会检查用户是否为每个参数提供了值。编译器还假设这些参数是将传递给函数的唯一参数。简而言之,传递给函数的参数数量必须与函数期望的参数数量匹配。
tsTry
functionbuildName (firstName : string,lastName : string) {returnfirstName + " " +lastName ;}letExpected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.result1 =buildName ("Bob"); // error, too few parametersletExpected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.result2 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult3 =buildName ("Bob", "Adams"); // ah, just right
在 JavaScript 中,每个参数都是可选的,用户可以根据需要省略它们。当他们这样做时,它们的值是 undefined
。我们可以在 TypeScript 中通过在想要设置为可选的参数末尾添加一个 ?
来获得此功能。例如,假设我们希望上面的姓氏参数是可选的
tsTry
functionbuildName (firstName : string,lastName ?: string) {if (lastName ) returnfirstName + " " +lastName ;else returnfirstName ;}letresult1 =buildName ("Bob"); // works correctly nowletExpected 1-2 arguments, but got 3.2554Expected 1-2 arguments, but got 3.result2 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult3 =buildName ("Bob", "Adams"); // ah, just right
任何可选参数都必须位于必需参数之后。如果我们想让名字可选,而不是姓氏,我们需要更改函数中参数的顺序,将名字放在列表的最后。
在 TypeScript 中,我们还可以设置一个参数的值,如果用户没有提供值,或者如果用户传递了 undefined
来代替它,该参数将被分配。这些被称为默认初始化参数。让我们以之前的例子为例,将姓氏默认为 "Smith"
。
tsTry
functionbuildName (firstName : string,lastName = "Smith") {returnfirstName + " " +lastName ;}letresult1 =buildName ("Bob"); // works correctly now, returns "Bob Smith"letresult2 =buildName ("Bob",undefined ); // still works, also returns "Bob Smith"letExpected 1-2 arguments, but got 3.2554Expected 1-2 arguments, but got 3.result3 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult4 =buildName ("Bob", "Adams"); // ah, just right
位于所有必需参数之后的默认初始化参数被视为可选参数,就像可选参数一样,在调用其各自的函数时可以省略。这意味着可选参数和尾部默认参数将在其类型上共享共性,因此两者
ts
function buildName(firstName: string, lastName?: string) {// ...}
和
ts
function buildName(firstName: string, lastName = "Smith") {// ...}
共享相同的类型 (firstName: string, lastName?: string) => string
。lastName
的默认值在类型中消失,只留下参数是可选的事实。
与普通的可选参数不同,默认初始化参数**不需要**出现在必填参数之后。如果默认初始化参数出现在必填参数之前,用户需要显式地传递undefined
来获取默认初始化的值。例如,我们可以用只在firstName
上进行默认初始化的方式来编写我们之前的例子。
tsTry
functionbuildName (firstName = "Will",lastName : string) {returnfirstName + " " +lastName ;}letExpected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.result1 =buildName ("Bob"); // error, too few parametersletExpected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.result2 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult3 =buildName ("Bob", "Adams"); // okay and returns "Bob Adams"letresult4 =buildName (undefined , "Adams"); // okay and returns "Will Adams"
剩余参数
必填参数、可选参数和默认参数都有一个共同点:它们一次只讨论一个参数。有时,您想将多个参数作为一个组来处理,或者您可能不知道一个函数最终将接受多少个参数。在 JavaScript 中,您可以使用在每个函数体中可见的arguments
变量直接处理参数。
在 TypeScript 中,您可以将这些参数收集到一个变量中。
tsTry
functionbuildName (firstName : string, ...restOfName : string[]) {returnfirstName + " " +restOfName .join (" ");}// employeeName will be "Joseph Samuel Lucas MacKinzie"letemployeeName =buildName ("Joseph", "Samuel", "Lucas", "MacKinzie");
剩余参数被视为无限数量的可选参数。在为剩余参数传递参数时,您可以使用任意数量的参数;您甚至可以不传递任何参数。编译器将使用省略号 (...
) 后面的名称构建一个传递参数的数组,允许您在函数中使用它。
省略号也用于具有剩余参数的函数的类型中。
tsTry
functionbuildName (firstName : string, ...restOfName : string[]) {returnfirstName + " " +restOfName .join (" ");}letbuildNameFun : (fname : string, ...rest : string[]) => string =buildName ;
this
学习如何在 JavaScript 中使用this
是一种必经之路。由于 TypeScript 是 JavaScript 的超集,TypeScript 开发人员也需要学习如何使用this
以及如何发现它何时使用不当。幸运的是,TypeScript 允许您使用两种技术来捕获this
的错误使用。但是,如果您需要学习this
在 JavaScript 中的工作原理,请先阅读 Yehuda Katz 的 理解 JavaScript 函数调用和“this”。Yehuda 的文章很好地解释了this
的内部工作原理,所以我们这里只介绍基础知识。
this
和箭头函数
在 JavaScript 中,this
是一个在函数被调用时设置的变量。这使得它成为一个非常强大和灵活的功能,但它也需要始终了解函数执行的上下文。这非常容易让人困惑,尤其是在返回函数或将函数作为参数传递时。
让我们看一个例子
tsTry
letdeck = {suits : ["hearts", "spades", "clubs", "diamonds"],cards :Array (52),createCardPicker : function () {return function () {letpickedCard =Math .floor (Math .random () * 52);letpickedSuit =Math .floor (pickedCard / 13);return {suit : this.suits [pickedSuit ],card :pickedCard % 13 };};},};letcardPicker =deck .createCardPicker ();letpickedCard =cardPicker ();alert ("card: " +pickedCard .card + " of " +pickedCard .suit );
请注意,createCardPicker
是一个自身返回函数的函数。如果我们尝试运行这个例子,我们会得到一个错误,而不是预期的警报框。这是因为由 createCardPicker
创建的函数中使用的 this
将被设置为 window
而不是我们的 deck
对象。这是因为我们单独调用了 cardPicker()
。像这样顶层的非方法语法调用将使用 window
作为 this
。(注意:在严格模式下,this
将是 undefined
而不是 window
)。
我们可以通过确保在返回函数以供以后使用之前,将函数绑定到正确的 this
来解决这个问题。这样,无论它以后如何使用,它仍然能够看到原始的 deck
对象。为此,我们将函数表达式更改为使用 ECMAScript 6 箭头语法。箭头函数捕获创建函数时的 this
,而不是调用函数时的 this
。
tsTry
letdeck = {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 herereturn () => {letpickedCard =Math .floor (Math .random () * 52);letpickedSuit =Math .floor (pickedCard / 13);return {suit : this.suits [pickedSuit ],card :pickedCard % 13 };};},};letcardPicker =deck .createCardPicker ();letpickedCard =cardPicker ();alert ("card: " +pickedCard .card + " of " +pickedCard .suit );
更棒的是,如果您将 noImplicitThis
标志传递给编译器,TypeScript 会在您犯此错误时向您发出警告。它会指出 this.suits[pickedSuit]
中的 this
类型为 any
。
this
参数
不幸的是,this.suits[pickedSuit]
的类型仍然是 any
。这是因为 this
来自对象字面量内的函数表达式。为了解决这个问题,你可以提供一个显式的 this
参数。this
参数是函数参数列表中第一个的伪参数。
ts
function f(this: void) {// make sure `this` is unusable in this standalone function}
让我们在上面的示例中添加几个接口,Card
和 Deck
,以使类型更清晰,更容易重用。
tsTry
interfaceCard {suit : string;card : number;}interfaceDeck {suits : string[];cards : number[];createCardPicker (this :Deck ): () =>Card ;}letdeck :Deck = {suits : ["hearts", "spades", "clubs", "diamonds"],cards :Array (52),// NOTE: The function now explicitly specifies that its callee must be of type DeckcreateCardPicker : function (this :Deck ) {return () => {letpickedCard =Math .floor (Math .random () * 52);letpickedSuit =Math .floor (pickedCard / 13);return {suit : this.suits [pickedSuit ],card :pickedCard % 13 };};},};letcardPicker =deck .createCardPicker ();letpickedCard =cardPicker ();alert ("card: " +pickedCard .card + " of " +pickedCard .suit );
现在 TypeScript 知道 createCardPicker
预计在 Deck
对象上调用。这意味着 this
现在是 Deck
类型,而不是 any
,所以 noImplicitThis
不会导致任何错误。
this
参数在回调函数中
当将函数传递给稍后会调用的库时,你还会在回调函数中遇到 this
的错误。因为调用回调函数的库会像普通函数一样调用它,所以 this
将是 undefined
。通过一些操作,你可以使用 this
参数来防止回调函数的错误。首先,库作者需要用 this
来注释回调函数类型。
tsTry
interfaceUIElement {addClickListener (onclick : (this : void,e :Event ) => void): void;}
this: void
表示 addClickListener
预计 onclick
是一个不需要 this
类型的函数。其次,用 this
来注释你的调用代码。
tsTry
classHandler {info : string;onClickBad (this :Handler ,e :Event ) {// oops, used `this` here. using this callback would crash at runtimethis.info =e .message ;}}leth = newHandler ();Argument of type '(this: Handler, e: Event) => void' is not assignable to parameter of type '(this: void, e: Event) => void'. The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'Handler'.2345Argument of type '(this: Handler, e: Event) => void' is not assignable to parameter of type '(this: void, e: Event) => void'. The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'Handler'.uiElement .addClickListener (h .onClickBad ); // error!
通过注释 this
,你明确表示 onClickBad
必须在 Handler
实例上调用。然后 TypeScript 将检测到 addClickListener
需要一个 this: void
的函数。要修复错误,请更改 this
的类型。
tsTry
classHandler {info : string;onClickGood (this : void,e :Event ) {// can't use `this` here because it's of type void!console .log ("clicked!");}}leth = newHandler ();uiElement .addClickListener (h .onClickGood );
因为 onClickGood
将其 this
类型指定为 void
,所以它可以合法地传递给 addClickListener
。当然,这也意味着它不能使用 this.info
。如果你想要两者,那么你必须使用箭头函数。
tsTry
classHandler {info : string;onClickGood = (e :Event ) => {this.info =e .message ;};}
这是有效的,因为箭头函数使用外部 this
,所以你总是可以将它们传递给期望 this: void
的东西。缺点是每个 Handler
类型的对象都会创建一个箭头函数。另一方面,方法只创建一次并附加到 Handler
的原型。它们在所有 Handler
类型的对象之间共享。
重载
JavaScript 本质上是一种非常动态的语言。一个 JavaScript 函数根据传入参数的形状返回不同类型的对象的情况并不少见。
tsTry
letsuits = ["hearts", "spades", "clubs", "diamonds"];functionpickCard (x : any): any {// Check to see if we're working with an object/array// if so, they gave us the deck and we'll pick the cardif (typeofx == "object") {letpickedCard =Math .floor (Math .random () *x .length );returnpickedCard ;}// Otherwise just let them pick the cardelse if (typeofx == "number") {letpickedSuit =Math .floor (x / 13);return {suit :suits [pickedSuit ],card :x % 13 };}}letmyDeck = [{suit : "diamonds",card : 2 },{suit : "spades",card : 10 },{suit : "hearts",card : 4 },];letpickedCard1 =myDeck [pickCard (myDeck )];alert ("card: " +pickedCard1 .card + " of " +pickedCard1 .suit );letpickedCard2 =pickCard (15);alert ("card: " +pickedCard2 .card + " of " +pickedCard2 .suit );
这里,pickCard
函数将根据用户传入的内容返回两种不同的东西。如果用户传入一个表示牌堆的对象,该函数将抽取一张牌。如果用户抽取了牌,我们会告诉他们他们抽取了哪张牌。但是我们如何向类型系统描述这一点呢?
答案是为同一个函数提供多个函数类型作为重载列表。编译器将使用此列表来解析函数调用。让我们创建一个重载列表来描述我们的 pickCard
接受什么以及它返回什么。
tsTry
letsuits = ["hearts", "spades", "clubs", "diamonds"];functionpickCard (x : {suit : string;card : number }[]): number;functionpickCard (x : number): {suit : string;card : number };functionpickCard (x : any): any {// Check to see if we're working with an object/array// if so, they gave us the deck and we'll pick the cardif (typeofx == "object") {letpickedCard =Math .floor (Math .random () *x .length );returnpickedCard ;}// Otherwise just let them pick the cardelse if (typeofx == "number") {letpickedSuit =Math .floor (x / 13);return {suit :suits [pickedSuit ],card :x % 13 };}}letmyDeck = [{suit : "diamonds",card : 2 },{suit : "spades",card : 10 },{suit : "hearts",card : 4 },];letpickedCard1 =myDeck [pickCard (myDeck )];alert ("card: " +pickedCard1 .card + " of " +pickedCard1 .suit );letpickedCard2 =pickCard (15);alert ("card: " +pickedCard2 .card + " of " +pickedCard2 .suit );
通过此更改,重载现在为我们提供了对 pickCard
函数的类型检查调用。
为了让编译器选择正确的类型检查,它遵循与底层 JavaScript 类似的过程。它查看重载列表,并从第一个重载开始,尝试使用提供的参数调用该函数。如果它找到匹配项,它将选择此重载作为正确的重载。因此,通常将重载从最具体到最不具体排序。
请注意,function pickCard(x): any
部分不是重载列表的一部分,因此它只有两个重载:一个接受对象,一个接受数字。使用任何其他参数类型调用 pickCard
将导致错误。