函数是 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 的重要部分。
tsTryletz = 100;functionaddToZ (x ,y ) {returnx +y +z ;}
函数类型
函数类型
让我们将类型添加到我们之前简单的示例中
tsTryfunctionadd (x : number,y : number): number {returnx +y ;}letmyAdd = function (x : number,y : number): number {returnx +y ;};
我们可以为每个参数添加类型,然后为函数本身添加返回类型。TypeScript 可以通过查看 return 语句来推断出返回类型,因此在许多情况下,我们也可以选择省略它。
编写函数类型
现在我们已经为函数添加了类型,让我们通过查看函数类型的每个部分来写出函数的完整类型。
tsTryletmyAdd : (x : number,y : number) => number = function (x : number,y : number): number {returnx +y ;};
函数的类型包含两个部分:参数的类型和返回类型。在写出整个函数类型时,这两个部分都是必需的。我们像写参数列表一样写出参数类型,为每个参数指定一个名称和一个类型。这个名称只是为了提高可读性。我们也可以写成
tsTryletmyAdd : (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,而是当调用函数时,编译器会检查用户是否为每个参数提供了值。编译器还假设这些参数是将传递给函数的唯一参数。简而言之,传递给函数的参数数量必须与函数期望的参数数量匹配。
tsTryfunctionbuildName (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 中通过在想要设置为可选的参数末尾添加一个 ? 来获得此功能。例如,假设我们希望上面的姓氏参数是可选的
tsTryfunctionbuildName (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"。
tsTryfunctionbuildName (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
位于所有必需参数之后的默认初始化参数被视为可选参数,就像可选参数一样,在调用其各自的函数时可以省略。这意味着可选参数和尾部默认参数将在其类型上共享共性,因此两者
tsfunction buildName(firstName: string, lastName?: string) {// ...}
和
tsfunction buildName(firstName: string, lastName = "Smith") {// ...}
共享相同的类型 (firstName: string, lastName?: string) => string。lastName 的默认值在类型中消失,只留下参数是可选的事实。
与普通的可选参数不同,默认初始化参数**不需要**出现在必填参数之后。如果默认初始化参数出现在必填参数之前,用户需要显式地传递undefined来获取默认初始化的值。例如,我们可以用只在firstName上进行默认初始化的方式来编写我们之前的例子。
tsTryfunctionbuildName (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 中,您可以将这些参数收集到一个变量中。
tsTryfunctionbuildName (firstName : string, ...restOfName : string[]) {returnfirstName + " " +restOfName .join (" ");}// employeeName will be "Joseph Samuel Lucas MacKinzie"letemployeeName =buildName ("Joseph", "Samuel", "Lucas", "MacKinzie");
剩余参数被视为无限数量的可选参数。在为剩余参数传递参数时,您可以使用任意数量的参数;您甚至可以不传递任何参数。编译器将使用省略号 (...) 后面的名称构建一个传递参数的数组,允许您在函数中使用它。
省略号也用于具有剩余参数的函数的类型中。
tsTryfunctionbuildName (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 是一个在函数被调用时设置的变量。这使得它成为一个非常强大和灵活的功能,但它也需要始终了解函数执行的上下文。这非常容易让人困惑,尤其是在返回函数或将函数作为参数传递时。
让我们看一个例子
tsTryletdeck = {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。
tsTryletdeck = {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 参数是函数参数列表中第一个的伪参数。
tsfunction f(this: void) {// make sure `this` is unusable in this standalone function}
让我们在上面的示例中添加几个接口,Card 和 Deck,以使类型更清晰,更容易重用。
tsTryinterfaceCard {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 来注释回调函数类型。
tsTryinterfaceUIElement {addClickListener (onclick : (this : void,e :Event ) => void): void;}
this: void 表示 addClickListener 预计 onclick 是一个不需要 this 类型的函数。其次,用 this 来注释你的调用代码。
tsTryclassHandler {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 的类型。
tsTryclassHandler {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。如果你想要两者,那么你必须使用箭头函数。
tsTryclassHandler {info : string;onClickGood = (e :Event ) => {this.info =e .message ;};}
这是有效的,因为箭头函数使用外部 this,所以你总是可以将它们传递给期望 this: void 的东西。缺点是每个 Handler 类型的对象都会创建一个箭头函数。另一方面,方法只创建一次并附加到 Handler 的原型。它们在所有 Handler 类型的对象之间共享。
重载
JavaScript 本质上是一种非常动态的语言。一个 JavaScript 函数根据传入参数的形状返回不同类型的对象的情况并不少见。
tsTryletsuits = ["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 接受什么以及它返回什么。
tsTryletsuits = ["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 将导致错误。