Skip to content

Latest commit

 

History

History
919 lines (618 loc) · 47.3 KB

2.md

File metadata and controls

919 lines (618 loc) · 47.3 KB

二、类型、变量和函数技术

TypeScript 通过一种简单的语法将强类型引入 JavaScript,被安德斯·海尔斯伯格称为“语法糖”。

本章介绍了 TypeScript 语言中用于将强类型应用于 JavaScript 的语法。它面向以前没有使用过 TypeScript 的读者,涵盖了从标准 JavaScript 到 TypeScript 的过渡。如果您已经有了使用 TypeScript 的经验,并且对下面列出的主题有很好的理解,那么一定要快速通读,或者跳到下一章。

我们将在本章中讨论以下主题:

  • 基本类型和类型语法:字符串、数字和布尔值
  • 推断型和鸭型
  • 数组和枚举
  • 任何类型和显式转换
  • 函数和匿名函数
  • 可选和默认功能参数
  • 参数数组
  • 函数回调和函数签名
  • 函数范围规则和重载

基本类型

JavaScript 变量可以保存许多数据类型,包括数字、字符串、数组、对象、函数等等。JavaScript 中对象的类型由它的赋值决定——所以如果一个变量被赋值了一个字符串值,那么它就是字符串类型。然而,这会在我们的代码中引入许多问题。

JavaScript 不是强类型的

正如我们在第 1 章TypeScript–工具和框架选项中看到的,JavaScript 对象和变量可以动态更改或重新分配。作为一个例子,考虑下面的 JavaScript 代码:

var myString = "test";
var myNumber = 1;
var myBoolean = true;

我们首先定义三个变量,命名为myStringmyNumbermyBooleanmyString变量被设置为"test"的字符串值,因此将为类型string。同样的,myNumber设置为1的值,因此为number类型,myBoolean设置为true,为boolean类型。现在让我们开始将这些变量相互赋值,如下所示:

myString = myNumber;
myBoolean = myString;
myNumber = myBoolean;

我们首先将myString的值设置为myNumber的值(这是1的数值)。然后我们将myBoolean的值设置为myString的值(现在是1的数值)。最后,我们将myNumber的值设置为myBoolean的值。这里发生的是,即使我们从三种不同类型的变量开始——字符串、数字和布尔值——我们也能够将这些变量中的任何一种重新分配给其他类型。我们可以将数字赋给字符串,将字符串赋给布尔值,或者将布尔值赋给数字。

虽然在 JavaScript 中这种类型的赋值是合法的,但它表明 JavaScript 语言不是强类型的。这可能会导致我们的代码出现不必要的行为。我们的部分代码可能依赖于这样一个事实,即一个特定的变量持有一个字符串,如果我们无意中给这个变量赋值,我们的代码可能会以意想不到的方式开始中断。

TypeScript 是强类型的

另一方面,TypeScript 是一种强类型语言。一旦将变量声明为string类型,就只能为其分配string值。所有使用这个变量的代码都必须把它当作一个类型string来对待。这有助于确保编写的代码能够按照预期运行。虽然强类型似乎对简单的字符串和数字没有任何用处,但是当我们对对象、对象组、函数定义和类应用相同的规则时,它确实变得很重要。如果你写了一个函数,期望第一个参数是string,第二个参数是number,如果有人调用你的函数,第一个参数是boolean,第二个参数是别的什么,那就不能怪你了。

JavaScript 程序员一直非常依赖文档来理解如何调用函数,以及正确的函数参数的顺序和类型。但是,如果我们可以将所有这些文档都包含在集成开发环境中呢?然后,当我们编写代码时,我们的编译器会自动向我们指出,我们以错误的方式使用了对象和函数。这肯定会让我们成为更高效、更高效的程序员,让我们生成的代码错误更少?

TypeScript 正是这么做的。它引入了一种非常简单的语法来定义变量或函数参数的类型,以确保我们以正确的方式使用这些对象、变量和函数。如果我们违反了这些规则中的任何一条,TypeScript 编译器将自动生成错误,指示我们错误的代码行。

TypeScript 就是这样得名的。它是带有强类型的 JavaScript 因此是 TypeScript。让我们看一下这个非常简单的语言语法,它支持 TypeScript 中的“类型”。

类型语法

声明变量类型的 TypeScript 语法是在变量名后面加一个冒号(:),然后表示其类型。考虑以下 TypeScript 代码:

var myString : string = "test";
var myNumber: number = 1;
var myBoolean : boolean = true;

这段代码片段是前面 JavaScript 代码的 TypeScript 等价物,并显示了用于声明myString变量类型的 TypeScript 语法示例。通过包含一个冒号和关键字string ( : string),我们告诉编译器myString变量是类型string。同样的,myNumber变量为number类型,myBoolean变量为boolean类型。TypeScript 为这些基本的 JavaScript 类型分别引入了stringnumberboolean关键词。

如果我们试图为不同类型的变量赋值,TypeScript 编译器将生成编译时错误。给定前面代码中声明的变量,下面的 TypeScript 代码将生成一些编译错误:

myString = myNumber;
myBoolean = myString;
myNumber = myBoolean;

Type syntax

分配不正确的类型时,TypeScript 会生成错误

TypeScript 编译器正在生成编译错误,因为我们试图混合这些基本类型。第一个错误是由编译器产生的,因为我们不能给类型为string的变量赋值number。类似地,第二个编译错误表明我们不能将string值赋给类型为boolean的变量。再次,产生第三个错误是因为我们不能将boolean值赋给类型为number的变量。

TypeScript 语言引入的强类型语法意味着我们需要确保赋值运算符左侧的类型(=)与赋值运算符右侧的类型相同。

要修复前面的 TypeScript 代码,并消除编译错误,我们需要执行类似于以下的操作:

myString = myNumber.toString();
myBoolean = (myString === "test");
if (myBoolean) {
    myNumber = 1;
}

我们的第一行代码已经更改为调用myNumber变量(类型为number)上的.toString()函数,以便返回类型为string的值。这一行代码不会产生编译错误,因为等号两边的类型相同。

我们的第二行代码也做了修改,赋值操作符的右侧返回比较结果myString === "test",它将返回类型为boolean的值。因此,编译器将允许该代码,因为赋值的两边都解析为类型为boolean的值。

如果myBoolean变量的值是true,我们的代码片段的最后一行已经被修改为只给myNumber变量赋值1(类型为number)。

安德斯赫尔斯伯格将这一特征描述为“句法糖”。在可比较的 JavaScript 代码上加一点糖,TypeScript 使我们的代码符合强类型规则。每当您违反这些强类型规则时,编译器都会为您的违规代码生成错误。

推断分型

TypeScript 还使用了一种称为推断类型的技术,在这种情况下,您没有显式指定变量的类型。换句话说,TypeScript 将在您的代码中找到变量的第一个用法,找出该变量首先被初始化为什么类型,然后在代码块的其余部分中为该变量假定相同的类型。例如,考虑以下代码:

var myString = "this is a string";
var myNumber = 1;
myNumber = myString;

我们首先声明一个名为myString的变量,并为其赋值。TypeScript 标识该变量已被赋予类型为string的值,因此将推断该变量的任何进一步使用都属于类型string。我们的第二个变量myNumber有一个编号。同样,TypeScript 推断这个变量的类型是number类型。如果我们试图将最后一行代码中的myString变量(类型为string)分配给myNumber变量(类型为number), TypeScript 将生成一条熟悉的错误消息:

error TS2011: Build: Cannot convert 'string' to 'number'

产生此错误是因为 TypeScript 的推断类型规则。

鸭式

TypeScript 还对更复杂的变量类型使用了一种叫做 duck-typing 的方法。打鸭的意思是如果它长得像鸭子,叫声像鸭子,那么它很可能就是鸭子。考虑以下 TypeScript 代码:

var complexType = { name: "myName", id: 1 };
complexType = { id: 2, name: "anotherName" };

我们从一个名为complexType的变量开始,这个变量被分配了一个简单的带有nameid属性的 JavaScript 对象。在我们的第二行代码中,我们可以看到我们正在将这个complexType变量的值重新分配给另一个同样具有idname属性的对象。编译器将在这个实例中使用 duck-type 来判断这个赋值是否有效。换句话说,如果一个对象与另一个对象具有相同的属性集,那么它们被认为是同一类型的。

为了进一步说明这一点,让我们看看如果我们试图给我们的complexType变量分配一个不符合这个鸭式输入的对象,编译器会有什么反应:

var complexType = { name: "myName", id: 1 };
complexType = { id: 2 };
complexType = { name: "anotherName" };
complexType = { address: "address" };

这段代码片段的第一行定义了我们的complexType变量,并为它分配了一个包含idname属性的对象。从这一点来看,TypeScript 将对我们试图分配给complexType变量的任何值使用这种推断类型。在第二行代码中,我们试图分配一个有id属性但没有name属性的值。在第三行代码中,我们再次尝试分配一个有name属性但没有id属性的值。在我们代码片段的最后一行,我们完全错过了标记。编译此代码将生成以下错误:

error TS2012: Build: Cannot convert '{ id: number; }' to '{ name: string; id: number; }':

error TS2012: Build: Cannot convert '{ name: string; }' to '{ name: string; id: number; }':

error TS2012: Build: Cannot convert '{ address: string; }' to '{ name: string; id: number; }':

从错误消息中我们可以看到,TypeScript 正在使用 duck-typing 来确保类型安全。在每条消息中,编译器通过明确说明它所期望的,为我们提供了关于违规代码有什么问题的线索。complexType变量同时具有idname属性。要给complexType变量赋值,该值需要同时具有idname属性。通过处理每一个错误,TypeScript 都明确指出了每一行代码的问题。

请注意,以下代码不会生成任何错误消息:

var complexType = { name: "myName", id: 1 };
complexType = { name: "name", id: 2, address: "address" };

同样,我们的第一行代码定义了complexType变量,正如我们之前看到的,有一个id和一个name属性。现在,看这个例子的第二行。我们使用的对象实际上有三个属性:nameidaddress。即使我们增加了一个新的address属性,编译器也只会检查我们的新对象是否同时有一个id和一个name。因为我们的新对象具有这些属性,因此将匹配变量的原始类型,所以 TypeScript 将允许通过 duck-typing 进行这种赋值。

推断类型和鸭式类型是 TypeScript 语言的强大功能——为我们的代码带来强大的类型,而不需要使用显式类型,即冒号:,然后是类型说明符语法。

阵列

除了字符串、数字和布尔的基本 JavaScript 类型之外,TypeScript 还有另外两种数据类型:数组和枚举。让我们看看定义数组的语法。

一个数组简单地用[]符号标记,类似于 JavaScript,每个数组都可以被强类型化以保存特定的类型,如下面的代码所示:

var arrayOfNumbers: number[] = [1, 2, 3];
arrayOfNumbers = [3, 4, 5];
arrayOfNumbers = ["one", "two", "three"];

在这段代码片段的第一行,我们定义了一个名为arrayOfNumbers的数组,并进一步指定该数组的每个元素必须是number类型。然后第二行重新分配这个数组来保存一些不同的数值。

然而,这个片段的最后一行将生成以下错误消息:

error TS2012: Build: Cannot convert 'string[]' to 'number[]':

该错误消息警告我们变量arrayOfNumbers被强类型化为只接受类型number的值。我们的代码试图将一个字符串数组赋给这个数字数组,因此产生了一个编译错误。

任何类型

所有这些类型检查都很好,但是 JavaScript 足够灵活,允许变量混合和匹配。以下代码片段实际上是有效的 JavaScript 代码:

var item1 = { id: 1, name: "item 1" };
item1 = { id: 2 };

我们的第一行代码给变量item1分配了一个带有id属性和name属性的对象。第二行然后将这个变量重新分配给一个具有id属性但不是name属性的对象。不幸的是,正如我们之前看到的,TypeScript 将为前面的代码生成编译时错误:

error TS2012: Build: Cannot convert '{ id: number; }' to '{ id: number; name: string; }'

TypeScript 为这样的场合介绍了any类型。指定对象的类型为any本质上放松了编译器严格的类型检查。以下代码显示了如何使用any类型:

var item1 : any = { id: 1, name: "item 1" };
item1 = { id: 2 };

注意我们的第一行代码是如何变化的。我们将变量item1的类型指定为: any类型,这样我们的代码就可以毫无错误地编译。如果没有: any的类型说明符,代码的第二行通常会产生错误。

显式铸造

与任何强类型语言一样,有时需要显式指定对象的类型。这个的概念将在下一章中被更彻底地展开,但是值得在这里快速记下明确的选角。可以使用< >语法将一个对象转换为另一个对象的类型。

这不是严格意义上的演员阵容;它更像是 TypeScript 编译器在运行时使用的断言。您使用的任何显式转换都将在生成的 JavaScript 中编译掉,并且不会影响运行时的代码。

让我们修改前面的代码片段以使用显式强制转换:

var item1 = <any>{ id: 1, name: "item 1" };
item1 = { id: 2 };

请注意,在这个片段的第一行,我们已经用右侧的<any>显式强制转换替换了赋值左侧的: any类型说明符。这段代码告诉编译器显式强制转换,或者将右侧的{ id: 1, name: "item 1" }对象显式视为any类型。因此,item1变量也有any的类型(由于 TypeScript 的推断类型规则)。这样我们就可以将一个只有{ id: 2 }属性的对象分配给第二行代码中的变量item1。这种在赋值的右侧使用< >语法的技术被称为显式转换。

虽然any类型是 TypeScript 语言的一个必要特性,但它的使用应该尽可能受到限制。这是确保与 JavaScript 兼容所必需的语言快捷方式,但是过度使用any类型会很快导致难以发现的编码错误。不要使用any类型,试着找出你正在使用的对象的正确类型,然后使用这个类型。我们在我们的编程团队中使用一个首字母缩略词: S.F.I.A.T. (发音为 sviat 或 sveat)。只需找到任意类型的接口。虽然这听起来很傻——它让人们明白了any类型应该总是被一个接口所取代——所以找到它就好了。接口是在 TypeScript 中定义自定义类型的一种方式,我们将在下一章中介绍接口。请记住,通过积极尝试定义对象的类型,我们正在构建强类型代码,从而保护我们自己免受未来编码错误和 bug 的影响。

列举

枚举是从 C#等其他语言中借用的一种特殊类型,为特殊数字的问题提供了解决方案。枚举将人类可读的名称与特定数字相关联。考虑以下代码:

enum DoorState {
    Open,
    Closed,
    Ajar
}

在这段代码中,我们定义了一个名为DoorStateenum来表示门的状态。该门状态的有效值为OpenClosedAjar。在引擎盖下(在生成的 JavaScript 中),TypeScript 将为这些人类可读的枚举值中的每一个赋值。在本例中,DoorState.Open枚举值将等于0的数值。同样,枚举值DoorState.Closed将等于1的数值,DoorState.Ajar枚举值将等于2。让我们快速浏览一下如何使用这些枚举值:

window.onload = () => {
    var myDoor = DoorState.Open;
    console.log("My door state is " + myDoor.toString());
};

window.onload函数中的第一行创建一个名为myDoor的变量,并将其值设置为DoorState.Open。第二行只是将myDoor的值记录到控制台。这个console.log函数的输出将是:

My door state is 0

这清楚地表明,TypeScript 编译器已经用数值0替换了DoorState.Open的枚举值。现在让我们以稍微不同的方式使用这个枚举:

window.onload = () => {
    var openDoor = DoorState["Closed"];
    console.log("My door state is " + openDoor.toString());
};

该代码片段使用字符串值“Closed”来查找enum类型,并将结果枚举值分配给openDoor变量。该代码的输出将是:

My door state is 1

该示例清楚地显示了DoorState.Closed的枚举值与DoorState["Closed"]的枚举值相同,因为两个变量都解析为1的数值。最后,让我们看看当我们使用数组类型语法引用枚举时会发生什么:

window.onload = () => {
    var ajarDoor = DoorState[2];
    console.log("My door state is " + ajarDoor.toString());
};

这里,我们基于DoorState枚举的第二个索引值,将变量openDoor分配给一个枚举值。然而,这段代码的输出令人惊讶:

My door state is Ajar

您可能一直期望输出只是简单的2,但是在这里我们得到了字符串"Ajar",这是我们原始枚举名称的字符串表示。这实际上是一个巧妙的小技巧——允许我们访问枚举值的字符串表示。这之所以可能,是因为由 TypeScript 编译器生成的 JavaScript。让我们来看看 TypeScript 编译器生成的闭包:

var DoorState;
(function (DoorState) {
    DoorState[DoorState["Open"] = 0] = "Open";
    DoorState[DoorState["Closed"] = 1] = "Closed";
    DoorState[DoorState["Ajar"] = 2] = "Ajar";
})(DoorState || (DoorState = {}));

这种奇怪的语法正在构建一个具有特定内部结构的对象。正是这个内部结构允许我们以我们刚刚探索的各种方式使用这个枚举。如果我们在调试 JavaScript 的时候询问这个结构,我们会看到DoorState对象的内部结构如下:

DoorState
{...}
    [prototype]: {...}
    [0]: "Open"
    [1]: "Closed"
    [2]: "Ajar"
    [prototype]: []
    Ajar: 2
    Closed: 1
    Open: 0

DoorState对象有一个名为"0"的属性,其字符串值为"Open"。不幸的是,在 JavaScript 中数字0不是一个有效的属性名,所以我们不能通过简单地使用DoorState.0来访问这个属性。相反,我们必须使用DoorState[0]DoorState["0"]来访问该属性。DoorState对象还有一个名为Open的属性,设置为数值0。单词Open在 JavaScript 中是一个有效的属性名,所以我们可以使用DoorState["Open"]或者简单的DoorState.Open来访问这个属性,它们在 JavaScript 中等同于同一个属性。

虽然底层的 JavaScript 可能有点令人困惑,但是我们需要记住的关于枚举的一切是,它们是一种方便的方法,可以将一个容易记住的、人类可读的名称定义为一个特殊的数字。使用人类可读的枚举,而不仅仅是在我们的代码中散布各种特殊的数字,也使代码的意图更加清晰。使用名为DoorState.OpenDoorState.Closed的应用范围值远比简单,记住为Open设置一个值为0,为Closed设置一个值为1,为ajar设置一个值为3。除了使我们的代码更易读、更易维护之外,每当这些特殊的数值发生变化时,使用枚举还可以保护我们的代码库——因为它们都是在一个地方定义的。

枚举的最后一个注意事项–如果需要,我们可以手动设置数值:

enum DoorState {
    Open = 3,
    Closed = 7,
    Ajar = 10
}

这里,我们已经覆盖了枚举的默认值,将DoorState.Open设置为3DoorState.Closed设置为7DoorState.Ajar设置为10

Const enums

随着 TypeScript 1.4 的发布,我们也可以定义const枚举如下:

const enum DoorStateConst {
    Open,
    Closed,
    Ajar
}

var myState = DoorStateConst.Open;

这些类型的枚举主要是出于性能原因而引入的,并且最终的 JavaScript 不会像我们之前看到的那样包含DoorStateConst枚举的完整闭包定义。让我们快速看一下从这个DoorStateConst枚举生成的 JavaScript:

var myState = 0 /* Open */;

请注意,我们根本没有完整的【JavaScript 闭包。编译器只是将DoorStateConst.Open枚举解析为其内部值0,并完全移除了const enum定义。

因此,对于 const enums,我们不能像在前面的代码示例中那样引用枚举的内部字符串值。考虑以下示例:

// generates an error
console.log(DoorStateConst[0]);
// valid usage
console.log(DoorStateConst["Open"]);

第一个console.log语句现在将生成一个编译时错误,因为我们没有针对常量枚举的[0]属性的完整闭包。然而,这个const枚举的第二种用法是有效的,并将生成以下 JavaScript:

console.log(0 /* "Open" */);

当使用 const enums 时,只要记住编译器会去掉所有的 enum 定义,并简单地将 enum 的数值直接替换到我们的 JavaScript 代码中。

功能

JavaScript 使用function关键字、一组大括号和以及一组大括号来定义函数。一个典型的 JavaScript 函数将编写如下:

function addNumbers(a, b) {
    return a + b;
}

var result = addNumbers(1, 2);
var result2 = addNumbers("1", "2");

这个代码片段是相当不言自明的;我们定义了一个名为addNumbers的函数,它接受两个变量并返回它们的和。然后我们调用这个函数,传入12的值。变量result的值将是1 + 2,也就是3。现在看看最后一行代码。这里,我们调用addNumbers函数,传入两个字符串作为参数,而不是数字。变量result2的值将是一个字符串"12"。这个字符串值看起来可能不是想要的结果,因为函数的名称是addNumbers

将前面的代码复制到 TypeScript 文件中不会产生任何错误,但是让我们在前面的 JavaScript 中插入一些类型规则,使其更加健壮:

function addNumbers(a: number, b: number): number {
    return a + b;
};

var result = addNumbers(1, 2);
var result2 = addNumbers("1", "2");

在这个 TypeScript 代码中,我们在addNumbers函数(ab的两个参数中都添加了一个:number类型,并且我们还在( )大括号之后添加了一个:number类型。将类型描述符放在这里意味着函数本身的返回类型是强类型的,以返回类型number的值。然而,在 TypeScript 中,最后一行代码将导致编译错误:

error TS2082: Build: Supplied parameters do not match any signature of call target:

生成这个错误消息是因为我们已经明确声明函数应该只接受参数ab的数字,但是在我们的错误代码中,我们传递了两个字符串。因此,TypeScript 编译器无法匹配名为addNumbers的函数的签名,该函数接受两个类型为string的参数。

匿名函数

JavaScript 语言也有匿名函数的概念。这些是动态定义的函数,不指定函数名。考虑以下 JavaScript 代码:

var addVar = function(a, b) {
    return a + b;
};

var result = addVar(1, 2);

这段代码定义了一个没有名称的函数,并添加了两个值。因为该函数没有名称,所以被称为匿名函数。这个匿名函数然后被分配给一个名为addVar的变量。然后addVar变量可以作为一个带有两个参数的函数被调用,返回值将是执行匿名函数的结果。在这种情况下,变量result的值为3

现在让我们在 TypeScript 中重写前面的 JavaScript 函数,并添加一些类型语法,以确保该函数只接受两个类型为number的参数,并返回一个类型为number的值:

var addVar = function(a: number, b: number): number {
    return a + b;
}

var result = addVar(1, 2);
var result2 = addVar("1", "2");

在这段代码中,我们创建了一个匿名函数,该函数只接受参数abnumber类型的参数,并且还返回一个number类型的值。ab参数的类型,以及函数的返回类型,现在都使用:number语法。这是 TypeScript 注入语言的简单“语法糖”的另一个例子。如果我们编译这段代码,TypeScript 将拒绝最后一行的代码,在那里我们尝试用两个字符串参数调用匿名函数:

error TS2082: Build: Supplied parameters do not match any signature of call target:

可选参数

当我们调用一个有预期参数的 JavaScript 函数,而我们没有提供这些参数,那么函数内参数的值将是undefined。作为一个例子,考虑下面的 JavaScript 代码:

var concatStrings = function(a, b, c) {
    return a + b + c;
}

console.log(concatStrings("a", "b", "c"));
console.log(concatStrings("a", "b"));

这里,我们定义了一个名为concatStrings的函数,该函数接受三个参数:abc,并简单地返回这些值的总和。如果我们用所有三个参数调用这个函数,就像在这个截屏的最后一行中看到的那样,我们将以登录到控制台的字符串"abc"结束。然而,如果我们只提供两个参数,如这个片段的最后一行所示,字符串"abundefined"将被记录到控制台。同样,如果我们调用一个函数并且不提供参数,那么这个参数,在我们的例子中是c,将只是undefined

TypeScript 引入问号?语法来表示可选参数。考虑下面的 TypeScript 函数定义:

var concatStrings = function(a: string, b: string, c?: string) {
    return a + b + c;
}

console.log(concatStrings("a", "b", "c"));
console.log(concatStrings("a", "b"));
console.log(concatStrings("a"));

这是我们之前使用的原始concatStrings JavaScript 函数的强类型版本。注意第三个参数的语法中添加了?字符:c?: string。这表明第三个参数是可选的,因此,除了最后一行,前面的所有代码都将干净地编译。最后一行将产生一个错误:

error TS2081: Build: Supplied parameters do not match any signature of call target.

产生该错误是因为我们试图仅使用单个参数调用concatStrings函数。然而,我们的函数定义至少需要两个参数,只有第三个参数是可选的。

可选参数必须是函数定义中的最后一个参数。只要非可选参数在可选参数之前,您可以有任意多的可选参数。

默认参数

可选参数函数定义的一个微妙的变体,允许我们指定参数的默认值,如果它不是从调用代码作为参数传入的。让我们修改前面的函数定义,使用一个可选参数:

var concatStrings = function(a: string, b: string, c: string = "c") {
    return a + b + c;
}

console.log(concatStrings("a", "b", "c"));
console.log(concatStrings("a", "b"));

该函数定义现在删除了?可选参数语法,而是为最后一个参数c:string = "c"赋值"c"。通过使用默认参数,如果我们没有为名为c的最终参数提供值,concatStrings函数将替代"c"的默认值。因此c的论点不会是。最后两行代码的输出都是"abc"

注意使用默认参数语法会自动使参数可选。

自变量变量

JavaScript 语言允许用可变数量的参数调用函数。每个 JavaScript 函数都可以访问一个名为arguments的特殊变量,该变量可以用来检索已经传递到函数中的所有参数。作为一个例子,考虑下面的 JavaScript 代码:

function testParams() {
    if (arguments.length > 0) {
        for (var i = 0; i < arguments.length; i++) {
            console.log("Argument " + i + " = " + arguments[i]);
        }
    }
}

testParams(1, 2, 3, 4);
testParams("first argument");

在这段代码中,我们定义了一个没有任何命名参数的函数名testParams。但是,请注意,我们可以使用名为arguments的特殊变量来测试该函数是否用任何参数调用。在我们的示例中,我们可以简单地循环通过arguments数组,并使用数组索引器arguments[i]将每个参数的值记录到控制台。console.log 调用的输出如下:

Argument 0 = 1

Argument 1 = 2

Argument 2 = 3

Argument 3 = 4

Argument 0 = first argument

那么,我们如何在 TypeScript 中表达可变数量的函数参数呢?答案是使用所谓的 rest 参数,或三个点()语法。以下是等效的testParams函数,用 TypeScript 表示:

function testParams(...argArray: number[]) {
    if (argArray.length > 0) {
        for (var i = 0; i < argArray.length; i++) {
            console.log("argArray " + i + " = " + argArray[i]);
            console.log("arguments " + i + " = " + arguments[i]);
        }
    }

}

testParams(1);
testParams(1, 2, 3, 4);
testParams("one", "two");

请注意我们的testParams函数使用的…argArray: number[]语法。该语法告诉 TypeScript 编译器,该函数可以接受任意数量的参数。这意味着我们对该函数的使用,即使用testParams(1)testParams(1,2,3,4)调用该函数,都将正确编译。在这个版本的testParams函数中,我们增加了两行console.log,只是为了说明arguments数组可以通过命名的 rest 参数argArray[i]或者通过普通的 JavaScript 数组arguments[i]来访问。

然而这个例子的最后一行将会产生一个编译错误,因为我们已经定义了 rest 参数只接受数字,并且我们正试图用字符串调用这个函数。

使用argArrayarguments之间的细微差别是参数的推断类型。因为我们已经明确指定了argArray属于number类型,所以 TypeScript 会将argArray数组中的任何一项都视为一个数字。但是内部arguments数组没有推断类型,因此将被视为any类型。

我们也可以在函数定义中将正常参数和 rest 参数组合在一起,只要 rest 参数是参数列表中最后定义的,如下所示:

function testParamsTs2(arg1: string,
    arg2: number, ...ArgArray: number[]) {
}

这里,我们有两个名为arg1arg2的正常参数,然后是一个argArray静止参数。错误地将 rest 参数放在参数表的开头会产生编译错误。

函数回调

JavaScript 最强大的特性之一——事实上也是 Node 所基于的技术——是回调函数的概念。回调函数是传递给另一个函数的函数。请记住,JavaScript 不是强类型的,所以变量也可以是函数。看看一些 JavaScript 代码就能最好地说明这一点:

function myCallBack(text) {
    console.log("inside myCallback " + text);
}

function callingFunction(initialText, callback) {
    console.log("inside CallingFunction");
    callback(initialText);
}

callingFunction("myText", myCallBack);

这里,我们有一个名为myCallBack的函数,它获取一个参数,并将其值记录到控制台。然后,我们定义一个名为callingFunction的函数,它接受两个参数:initialTextcallback。该功能的第一行只是将"inside CallingFunction"记录到控制台。callingFunction的第二行是有趣的部分。它假设callback参数实际上是一个函数,并调用它。它也将initialText变量传递给callback 功能。如果我们运行这段代码,我们将在控制台上记录两条消息,如下所示:

inside CallingFunction

inside myCallback myText

但是如果我们不传递函数作为回调会发生什么呢?前面的代码中没有任何内容向我们发出callingFunction的第二个参数必须是函数的信号。如果我们无意中用字符串调用了callingFunction函数,而不是用函数作为第二个参数,如下所示:

callingFunction("myText", "this is not a function");

我们会得到一个 JavaScript 运行时错误:

0x800a138a - JavaScript runtime error: Function expected

然而,具有防御意识的程序员会在调用参数之前首先检查callback参数是否实际上是一个函数,如下所示:

function callingFunction(initialText, callback) {
    console.log("inside CallingFunction");
    if (typeof callback === "function") {
        callback(initialText);
    } else {
        console.log(callback + " is not a function");
    }
}

callingFunction("myText", "this is not a function");

注意这段代码片段的第三行,我们在调用callback变量之前检查了它的类型。如果它不是一个函数,我们会在控制台上记录一条消息。在这个片段的最后一行,我们正在执行callingFunction,但是这次传递了一个字符串作为第二个参数。

代码片段的输出将是:

inside CallingFunction

this is not a function is not a function

使用函数回调时,那么,JavaScript 程序员需要做两件事;首先,了解哪些参数实际上是回调和其次,围绕回调函数的无效使用进行编码。

功能签名

强制强类型化的 TypeScript“语法糖”不仅适用于变量和类型,也适用于函数签名。如果我们可以用代码记录我们的 JavaScript 回调函数,然后在用户向我们的函数传递错误类型的参数时警告用户我们的代码,会怎么样?

TypeScript 通过函数签名来实现这一点。函数签名引入了胖箭头语法() =>,以定义函数应该是什么样子。让我们在 TypeScript 中重写前面的 JavaScript 示例:

function myCallBack(text: string) {
    console.log("inside myCallback " + text);
}

function callingFunction(initialText: string,
    callback: (text: string) => void)
{
    callback(initialText);
}

callingFunction("myText", myCallBack);
callingFunction("myText", "this is not a function");

我们的第一个函数定义myCallBack现在强有力地将text参数键入为string类型。我们的callingFunction功能有两个参数;initialText,类型为string,和callback,现在有了新的函数签名语法。让我们更仔细地看看这个函数签名:

callback: (text: string) => void

这个函数定义的意思是,callback参数被输入(通过:语法)为一个函数,使用胖箭头语法() =>。此外,该函数采用名为text的参数,该参数的类型为string。在胖箭头语法的右边,我们可以看到一个新的 TypeScript 基本类型,叫做void。Void 是一个关键字,表示函数不返回值。

因此,callingFunction函数将只接受一个接受单个字符串参数且不返回任何内容的函数作为其第二个参数。编译前面的代码将在代码片段的最后一行正确地突出显示一个错误,这里我们传递一个字符串作为第二个参数,而不是一个回调函数:

error TS2082: Build: Supplied parameters do not match any signature of call target:

Type '(text: string) => void' requires a call signature, but type 'String' lacks one

给定回调函数的前面的函数签名,下面的代码也会生成编译时错误:

function myCallBackNumber(arg1: number) {
    console.log("arg1 = " + arg1);
}

callingFunction("myText", myCallBackNumber);

这里,我们定义了一个名为myCallBackNumber的函数,它以一个数字作为唯一的参数。当我们试图编译这段代码时,我们会得到一条错误消息,指出callback参数,也就是我们的myCallBackNumber函数,也没有正确的函数签名:

Call signatures of types 'typeof myCallBackNumber' and '(text: string) => void' are incompatible.

myCallBackNumber的函数签名实际上是(arg1:number) => void,而不是所需的(text: string) => void,因此出现了错误。

在函数签名中,参数名称(arg1text)不需要相同。只有参数的数量、类型和函数的返回类型需要相同。

这是 TypeScript 的一个非常强大的特性——在代码中定义函数的签名应该是什么,并在用户没有用正确的参数调用函数时发出警告。正如我们在 TypeScript 简介中看到的,当我们使用第三方库时,这一点非常重要。在我们能够在 TypeScript 中使用第三方函数、类或对象之前,我们需要定义它们的函数签名。这些函数定义被放入一个特殊类型的 TypeScript 文件中,称为声明文件,并以.d.ts扩展名保存。我们将深入查看第四章申报文件的编写和使用中的申报文件。

函数回调和作用域

JavaScript 使用词法范围规则来定义变量的有效范围。这意味着变量的值是由它在源代码中的位置定义的。嵌套函数可以访问在其父作用域中定义的变量。作为一个例子,考虑下面的 TypeScript 代码:

function testScope() {
    var testVariable = "myTestVariable";
    function print() {
        console.log(testVariable);
    }
}

console.log(testVariable);

这段代码片段定义了一个名为testScope的函数。变量testVariable在该函数中定义。print函数是testScope的子函数,因此可以访问testVariable变量。然而,代码的最后一行将生成编译错误,因为它试图使用变量testVariable,该变量在词汇上的作用域仅在testScope函数的主体内有效:

error TS2095: Build: Could not find symbol 'testVariable'.

很简单,对吧?嵌套函数可以根据其在源代码中的位置访问变量。这一切都很好,但是在大型 JavaScript 项目中,有许多不同的文件和代码的许多区域被设计为可重用的。

让我们来看看这些范围规则是如何成为问题的。对于这个示例,我们将使用一个典型的回调场景——使用 jQuery 执行异步调用来获取一些数据。考虑以下 TypeScript 代码:

var testVariable = "testValue";

function getData() {
    var testVariable_2 = "testValue_2";
    $.ajax(
        {
            url: "/sample_json.json",
            success: (data, status, jqXhr) => {
                console.log("success : testVariable is "
                    + testVariable);
                console.log("success : testVariable_2 is" 
                    + testVariable_2);
            },
            error: (message, status, stack) => {
                alert("error " + message);
            }
        }
   );
}

getData();

在这段代码中,我们定义了一个名为testVariable的变量,并设置了它的值。然后我们定义一个叫做getData的函数。getData函数设置另一个名为testVariable_2的变量,然后调用 jQuery $.ajax函数。$.ajax功能配置有三个属性:urlsuccesserrorurl属性是一个简单的字符串,指向我们项目目录中的一个sample_json.json文件。success属性是一个匿名函数回调,它只是将testVariabletestVariable_2的值记录到控制台。最后,error属性也是一个匿名函数回调,只是弹出一个警告。

这段代码按预期运行,成功函数会将以下结果记录到控制台:

success : testVariable is :testValue

success : testVariable_2 is :testValue_2

目前为止一切顺利。现在,让我们假设我们正在尝试重构前面的代码,因为我们正在进行相当多的类似的$.ajax调用,并且希望在其他地方重用success回调函数。我们可以轻松切换出这个匿名函数,并为我们的success回调创建一个命名函数,如下所示:

var testVariable = "testValue";

function getData() {
    var testVariable_2 = "testValue_2";
    $.ajax(
        {
            url: "/sample_json.json",
            success: successCallback,
            error: (message, status, stack) => {
                alert("error " + message);
            }
        }
   );
}

function successCallback(data, status, jqXhr) {
    console.log("success : testVariable is :" + testVariable);
    console.log("success : testVariable_2 is :" + testVariable_2);
}

getData();

在这个示例中,我们创建了一个名为successCallback的新函数,其参数与之前的匿名函数相同。我们还修改了$.ajax调用,简单地传入这个函数,作为success属性的回调函数:success: successCallback。如果我们现在编译这段代码,TypeScript 会生成一个错误,如下所示:

error TS2095: Build: Could not find symbol ''testVariable_2''.

由于我们已经改变了代码的词法范围,通过创建一个命名函数,新的successCallback函数不再能够访问变量testVariable_2

在一个微不足道的例子中发现这种错误是相当容易的,但是在更大的项目中,当使用第三方库时,这种错误变得更加难以追踪。因此,值得一提的是,在使用回调函数时,我们需要了解这个词法范围。如果您的代码期望一个属性有一个值,而它在回调后没有值,那么请记住查看调用代码的上下文。

功能过载

由于 JavaScript 是动态语言,我们经常可以用不同的参数类型调用同一个函数。考虑以下 JavaScript 代码:

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

console.log("add(1,1)=" + add(1,1));
console.log("add(''1'',''1'')=" + add("1", "1"));
console.log("add(true,false)=" + add(true, false));

这里,我们定义了一个简单的add函数,该函数返回其两个参数xy的总和。这段代码的最后三行简单地记录了add函数不同类型的结果:两个数字、两个字符串和两个布尔值。如果我们运行这段代码,我们将看到以下输出:

add(1,1)=2

add('1','1')=11

add(true,false)=1

TypeScript 引入了特定的语法来指示同一函数的多个函数签名。如果我们要在 TypeScript 中复制前面的代码,我们需要使用函数重载语法:

function add(arg1: string, arg2: string): string;
function add(arg1: number, arg2: number): number;
function add(arg1: boolean, arg2: boolean): boolean;
function add(arg1: any, arg2: any): any {
    return arg1 + arg2;
}

console.log("add(1,1)=" + add(1, 1));
console.log("add(''1'',''1'')=" + add("1", "1"));
console.log("add(true,false)=" + add(true, false));

这段代码片段的第一行为接受两个字符串并返回一个stringadd函数指定了一个函数重载签名。第二行指定另一个使用数字的函数重载,第三行使用布尔值。第四行包含函数的实际体,并使用any的类型说明符。这个片段的最后三行显示了我们将如何使用这些函数签名,并且类似于我们之前使用的 JavaScript 代码。

在前面的代码片段中有三个有趣的地方。首先,代码片段前三行的函数签名实际上都没有函数体。其次,最终的函数定义使用了any的类型说明符,最终包含了函数体。函数重载语法必须遵循这种结构,并且包括函数体的最终函数签名必须使用any类型说明符,因为任何其他东西都会产生编译时错误。

第三点需要注意的是,我们正在限制add函数,通过使用这些函数重载签名,只接受两个相同类型的参数。如果我们尝试混合我们的类型;例如,如果我们用booleanstring调用函数,如下所示:

console.log("add(true,''1'')", add(true, "1"));

TypeScript 会产生编译错误:

error TS2082: Build: Supplied parameters do not match any signature of call target:

error TS2087: Build: Could not select overload for ''call'' expression.

这似乎与我们最终的函数定义相矛盾。在原始的 TypeScript 示例中,我们有一个接受(arg1: any, arg2: any)的函数签名;所以,理论上,这应该叫做当我们试图增加一个boolean和一个number的时候。然而,函数重载的 TypeScript 语法不允许这样做。请记住,函数重载语法必须包括对函数体使用any类型,因为所有重载最终都会调用该函数体。但是,在函数体之上包含函数重载向编译器表明,这些是调用代码应该可用的唯一签名。

联合类型

随着 TypeScript 1.4 的发布,我们现在能够使用管道符号(|)来表示联合类型,从而组合一个或两个类型。因此,我们可以将前面代码片段中的add函数重写改写如下:

function addWithUnion(
    arg1: string | number | boolean,
    arg2: string | number | boolean
     ): string | number | boolean
    {
    if (typeof arg1 === "string") {
        // arg1 is treated as a string here
        return arg1 + "is a string";
    }
    if (typeof arg1 === "number") {
        // arg1 is treated as a number here
        return arg1 + 10;
    }
    if (typeof arg1 === "boolean") {
        // arg1 is treated as a boolean here
        return arg1 && false;
    }
}

这个名为addWithUnion的函数有两个参数,arg1arg2。这些参数现在使用联合类型语法来指定这些参数可以是string, numberboolean。还要注意,我们的函数返回类型再次使用联合类型,这意味着函数也将返回这些类型之一。

型卫士

在前面代码片段中的addWithUnion函数的主体中,我们使用语句typeof arg1 === "string"检查arg1参数的类型是否是字符串。这被称为类型守卫,意味着arg1类型将被视为if语句块内的string。在下一个if语句的正文中,arg1的类型将被视为一个数字,允许我们将10添加到它的值中,在最后一个 if 语句的正文中,编译器将该类型视为一个boolean

类型别名

我们还能够定义类型、联合类型或函数定义的别名。类型别名用type关键字表示。因此,我们可以将前面的add函数编写如下:

type StringNumberOrBoolean = string | number | boolean;

function addWithAliases(
    arg1: StringNumberOrBoolean,
    arg2: StringNumberOrBoolean
     ): StringNumberOrBoolean {

}

这里,我们定义了一个名为StringNumberOrBoolean的类型别名,它是stringnumberboolean类型的类型联合。

类型别名也可以用于函数签名,如下所示:

type CallbackWithString = (string) => void;

function usingCallback(callback: CallbackWithString) {
    callback("this is a string");
}

这里,我们定义了一个名为CallbackWithString的类型别名,它是一个接受单个string参数并返回一个void的函数。我们的usingCallback函数接受函数签名中的这个类型别名作为 callback参数的类型。

总结

在本章中,我们讨论了 TypeScript 的基本类型、变量和函数技术。我们看到了 TypeScript 如何在普通 JavaScript 代码之上引入“语法糖”,以确保强类型变量和函数签名。我们还看到了 TypeScript 如何使用 duck-typing 和显式强制转换,最后讨论了 TypeScript 函数、函数签名和重载。在下一章中,我们将基于这些知识,看看 TypeScript 如何将这些强类型规则扩展到接口、类和泛型中。