面向对象开发人员的 TypeScript 简单示例 第 1 部分






4.97/5 (18投票s)
面向对象程序员的 TypeScript 教程。
引言
随着微软转向 JavaScript 进行 Web 编程,在伟大的 Anders Hejlsberg 的领导下,他们决定创建一个可以编译成 JavaScript 以在所有 Web 浏览器中运行的面向对象语言,但又能为混乱的弱类型、非 OOP Web 编程世界带来一些秩序。
语法上,TypeScript 与 JavaScript 非常相似。事实上,JavaScript 程序无需任何更改即可在 TypeScript 中运行。但是,TypeScript 允许将 Web 编程带入面向对象开发领域。因此,我将 TypeScript 视为一种适配器 - 它将 JavaScript 适配到面向对象概念。
TypeScript 的另一项伟大成就是,它提供了最新的 ECMAScript 6(许多浏览器尚不支持)中的最新特性,并将其转译为早期 JavaScript 版本,例如 ECMAScript 5。因此,使用 TypeScript 开发会自动使您的代码兼容多浏览器和多平台。没有 TypeScript,开发人员将不得不使用其他工具来实现类似的结果,例如 BabelJS。
您在网上找到的大多数 TypeScript 教程都是面向想要学习 TypeScript 并需要了解面向对象概念的 JavaScript 开发人员的。
在这里,我采用了一种不同的方法 - 向面向对象开发人员解释 TypeScript。我的目的是提供非常简单的代码片段,以突出特定功能,以便经验丰富的 Java 或 C# 开发人员几乎无需阅读文字即可理解该语言的功能。
所需背景知识
我假设您对 OOP 概念和一种 OOP 语言(最好是 C# 或 Java)有一定的基本了解。我还期望您对 HTML 编程和 JavaScript 有一些非常基础的了解。对于那些需要复习 HTML 和 JavaScript 的人 - 这是我几年前写的一篇文章:HTML5, JavaScript, Knockout, JQuery, Silverlight/WPF/C# 戒瘾指南。第一部分 - JavaScript 和 DOM。
第一部分和第二部分将涵盖的主题
第一部分涵盖以下主题
- 设置环境。
- 创建一个简单的 TypeScript "Hello World" 项目。
- TypeScript 中的变量和类型。
- 带属性的 TypeScript 类。
- 在 TypeScript 中扩展类。
- 实现接口。
- 匿名类。
- TypeScript 中的泛型。
第二部分应涵盖以下主题(可能会有变动)
- 模块和命名空间。
- TypeScript 的类似 LINQ 的功能。
- JSON 到 TypeScript 映射器。
- Promise 和异步编程。
- TypeScript 的 RxJS。
- JSX
开发工具
我尝试了几种开发工具。对于 TypeScript 开发,我喜欢Visual Studio Code。但所有这些工具(包括 Visual Studio Code)都需要安装和配置大量额外的软件。解释所有这些会使本文充斥不必要的细节。所以,对于本文,我决定坚持使用 Visual Studio 2017。也许以后我会写另一篇文章介绍各种 TypeScript 环境。
如上所述,我使用 VS2017 作为开发工具,并在 Chrome 浏览器中运行大多数示例(尽管这可能无关紧要,因为所有 TypeScript 功能都应该是与浏览器无关的)。
设置 VS2017
尽管 VS2017 开箱即用地包含了大多数 TypeScript 功能,但我还是为 VS2017 安装了最新版本的 TypeScript(对我来说是 2.6 版本)。要下载它,您可以直接遵循TypeScript 扩展链接中的链接。
代码位置
本文的代码位于 GitHub 上,网址为:面向 OOP 开发人员的 TypeScript 第一部分代码。
TypeScript Hello World 示例
我们将从最小、最简单的 TypeScript 示例开始,它不仅能突出一些 TypeScript 的特性,最重要的是,它将教会我们如何创建和设置使用 TypeScript 的 VS2017 项目。
创建项目,设置属性
我建议将您的 TypeScript 项目创建为“空 ASP.NET Web 应用程序”项目。为此,请从文件菜单中选择“新建”->“项目”选项。在打开的对话框中,单击左侧的“Visual C#”->“Web”选项,然后在主面板中选择“ASP.Net Web Application (.NET Framework)”选项。不要忘记勾选“为解决方案创建目录”复选框;否则,创建的解决方案文件将位于主文件夹中(这可能不是您想要的):
现在,让我们创建名为“Default.html”的主 HTML 文件。右键单击解决方案资源管理器中的项目,然后选择“添加”->“新项”选项。在打开的对话框中,在左侧选择“Visual C#”->“Web”,然后在主面板中选择“HTML Page”。为项目选择名称“Default.html”,然后单击“添加”按钮:
您应该将新创建的 Default.html 文件设置为启动页。为此,请右键单击解决方案资源管理器中的文件,然后选择“设置为启动页”选项:
现在,通过右键单击项目并选择“添加”->“新建文件夹”来为所有 TypeScript 和 JavaScript 文件创建一个项目文件夹。将新文件夹命名为“Scripts”。
通过右键单击“Scripts”文件夹并选择“添加”->“新项”,在该文件夹中创建一个名为 HelloWorld.ts 的 TypeScript 文件。在打开的对话框中,使用顶部的搜索文本框搜索 TypeScript 模板。在中间部分选择“TypeScript File”选项,在底部将文件命名为 HelloWorld.ts,然后单击“添加”按钮:
现在,在 HelloWorld.ts 文件中放入以下代码
class HelloWorldPrinter { static Print(): void { alert("Hello World!"); } }
在 Default.html 文件的 body 标签中,添加以下脚本标签
<body> <script src="Scripts/HelloWorld.js"></script> <script> HelloWorldPrinter.Print(); </script> </body>
您实际上可以通过将 TypeScript 文件拖到 HTML 代码中来添加对其的引用,因此,与其编写 ` <script src="Scripts/HelloWorld.js"></script>`,不如将 HelloWorld.ts 文件拖到 `<body>` 标签内的目标位置。
现在尝试运行应用程序 - 按下“调试”按钮或使用“DEBUG”->“开始调试”菜单项。您的默认浏览器应该会启动(在我的例子中是 Chrome),并在警报窗口中显示“Hello World!”文本。
请注意,您可以在 HelloWorld.ts 文件中设置断点,并且它会被命中。多年前,Visual Studio 调试器只能与 MS Internet Explorer 浏览器一起使用,现在,显然它可以与其他浏览器一起使用,至少与 Chrome 一起使用。
启动应用程序的另一种方法是右键单击 Default.html 文件并选择“在浏览器中查看”菜单选项。但是,在这种情况下,您将无法调试 TypeScript(您仍然可以在浏览器中使用 JavaScript 调试器,但它只能在生成的 JavaScript 上工作,而不能在原始 TypeScript 上工作)。
现在,让我们看看项目的属性。特别地,我们关心“TypeScript Build”选项卡。
<img src="1217899/ProjProps.png" class="" alt="" />
请注意,“保存时编译”选项确保每次开发人员保存 TypeScript 文件时都会尝试生成 JavaScript 文件。
勾选“生成源映射”会在 JavaScript 文件与 TypeScript 之间生成映射文件,以允许 TypeScript 调试。
另一个有趣的属性是“ECMAScript 版本”。此属性指定 TypeScript 编译成的 JavaScript 版本。“ECMAScript 5”是一个非常合理的选择,因为大多数浏览器都支持它,而较新的选项(例如“ECMAScript 6”)支持程度不高。
这三个属性很重要,但 VS2017 默认情况下会为您设置正确的值。
代码
让我们再看一下 HelloWorld.ts 代码
class HelloWorldPrinter { static Print(): void { alert("Hello World!"); } }
与 C# 不同,您不需要在类或方法前加上 "public" 关键字 - 类和方法默认是公共的。
与 C# 或 Java 一样,TypeScript 拥有类,方法可以定义为静态(使用 "static" 关键字)或实例(不使用任何关键字)。
公共静态方法可以通过类名在类外部访问,正如我们在 Default.html 文件中所做的那样。
HelloWorldPrinter.Print();
几个重要说明
在 HelloWorld 示例中,我特意涵盖了创建项目、创建文件、更改项目属性的步骤。在本文的其余部分,我将专注于代码本身。
TypeScript 中的大多数基本编程语言构造都与 JavaScript 相似,而 JavaScript 又从 Java 中借用了它们,因此 `if`、`while` 和 `for` 以及其他构造与 C++、Java 和 C# 中的非常相似。
在其余的示例中,我将专注于 TypeScript 特有的功能和最佳实践,省略那些与 JavaScript 或上述 OO 语言相似的。例如,我不会描述 `while` 循环,因为它与上述语言中的完全相同。
变量
变量示例的代码位于 Variables/Variables.sln 解决方案下。请注意,代码并未编译 - 我特意引入了一些编译错误,以展示 TypeScript 中会导致错误的情况。
我们感兴趣的 TypeScript 文件位于项目文件夹 Scripts 下
- BasicTypeVariables.ts
- EnumVariables.ts
- ArrayVariables.ts
- TupleVariables.ts
基本类型
基本类型在 BasicTypeVariables.ts 文件中进行了演示。这是它的内容:
// define number let myNumber: number; // define string let myString: string; // define any (can take any type // - analogous to object in C#) let myAny1: any = null; // no type specified analogous to 'any' let myAny2; // initialised to boolean // even though the type is not specified // you cannot change the type later let myBool1 = true; // type specified and initialize to false let myBool2: boolean = false; // compiler error // cannot reassign the type to 'number' myString = 5; myAny1 = 5; //OK // OK myAny2 = "hello world"; // any can be reassigned any type (like object in C#) myAny1 = "Hi"; // casting: // the two expressions below // are equivalent. myAny2 as string; (<string>myAny2);
代码注释应该能让您大致了解情况。
请注意,为了定义一个变量,我们需要使用关键字 "let"(与 JavaScript 中的 "var" 不同)。我们也可以在这里使用 "var",但那样的话,我们的作用域就会被搞乱 - 就像在常规 JavaScript 中一样。然而,关键字 "let" 将定义变量的作用域限制在花括号包围的块内 - 这与主要的 OO 语言中的做法相同,也应该是这样的。
变量的类型可以在变量名后的冒号后指定。有几种基本类型,包括 **number**、**string** 和 **boolean**。还有一个特殊的类型 **any**。这种类型的变量可以 Assume 任何其他类型。类型 **any** 类似于 Java 或 C# 中的 **object** 类型。如果未为变量分配类型且未对其进行初始化,则假定其类型为 **any**。
如果变量未分配类型,但在声明行中初始化为某种 **number**、**string** 或 **boolean**,TypeScript 会假定其被定义为相应类型。
一旦 TypeScript 变量被假定为某种(非 **any**)类型,它就不能被赋予任何其他类型的值 - 编译器将报告错误。
枚举
可以使用与 C# 非常相似的方式在 TypeScript 中定义枚举。这是 EnumVariables.ts 文件的内容:
enum Season { Winter, Spring = 1, // can assign integer Summer, Fall } let summerNumber: number; // summerNumber = 2 summerNumber = Season.Summer; let summerName: string; // summerName = "Summer" summerName = Season[Season.Summer];
这里唯一的棘手之处是检索枚举的名称。可以通过数组索引表示法获取:`summerName = Season[Season.Summer];`。
数组
ArrayVariables.ts 文件展示了定义和初始化数组(或列表或向量)的方法:
let arrayOfStrings = ["str1", "str2", "str3"]; // myStr="str1" let myStr = arrayOfStrings[0]; // compiler error // cannot assign an array of numbers // to an array of string. arrayOfStrings = [1, 2, 3]; // can contain any types like List<object> in C# let arrayOfAny: any[] = [1, "str2", false]; // for any[] reassigning to an array // containing different types works fine arrayOfAny = [true, 2, "str3"]; // another way to define array of strings // by using Array<string> let arrayOfStrings2: Array<string>; // now arrayOfStrings2 contains ["str1", "str2", "str3", "str4"] arrayOfStrings2 = [...arrayOfStrings, "str4"]; let i = 5;
上面代码中不易理解的部分是包含所谓展开运算符 "..." 的部分。
arrayOfStrings2 = [...arrayOfStrings, "str4"];
展开运算符在语法上解包数组,将其转换为逗号分隔的成员。它在这里用于数组连接,也可用于创建具有可变数量输入参数的方法 - 类似于 C# 中的 **params** 关键字。
元组
这是 TupleVariables.ts 文件的内容:
let myTuple: [number, string, boolean]; myTuple = [123, "123", true]; // works fine myTuple = [123, 123, 123]; // compiler error
这里没有什么不寻常的。
类和接口
带属性的类的示例
Solution ClassAndProperties.sln 包含一个 `Person` 类的示例,该类有三个属性。
class Person { //#region // read-write property FirstName private _firstName: string; get FirstName(): string { return this._firstName; } set FirstName(value: string) { this._firstName = value; } //#endregion //#region // read-write property LastName private _lastName: string; get LastName(): string { return this._lastName; } set LastName(value: string) { this._lastName = value; } //#endregion // read-only property FullName get FullName(): string { let result: string = ""; if (this.FirstName) { result += this.FirstName; result += " "; } if (this.LastName) { result += this.LastName; } return result; } // constructor taking two optional arguments constructor(firstName?: string, lastName?: string) { this.FirstName = firstName; this.LastName = lastName; } }
`FirstName` 和 `LastName` 属性是读写属性,而 `FullName` 属性是只读属性。
让我们仔细看看其中一个属性:
//#region // read-write property FirstName private _firstName: string; get FirstName(): string { return this._firstName; } set FirstName(value: string) { this._firstName = value; } //#endregion
属性 `FirstName` 以私有字段 `_firstName` 作为其后备存储。getter 和 setter 在类作用域内定义为两个独立的方法(这与 C# 中的做法不同)。不幸的是(也与 C# 不同),您可以为同一属性的 getter 和 setter 设置不同的隐私设置:例如,将 `FirstName` 的 setter 设置为私有或受保护将导致编译器错误。
类的另一个有趣部分是构造函数:
// constructor taking two optional arguments constructor(firstName?: string, lastName?: string) { this.FirstName = firstName; this.LastName = lastName; }
变量名后的问号表示参数是可选的,即如果未传递,则假定其处于未定义状态。
TypeScript 不允许方法或构造函数重载,但它允许使用可选参数或带有默认值的参数。当然,这些参数应该始终放在参数列表的末尾。这种功能允许我们几乎匹配 OO 语言中的方法和构造函数重载。
为了以后不再回头讨论,这里有一个类似的构造函数示例,它使用默认值参数而不是可选参数:
constructor(firstName: string = "Unknown", lastName: string = "Unknown") { this.FirstName = firstName; this.LastName = lastName; }
另外,请注意,方法、属性和字段默认是公共的,但如果您想限制它们的可见性,则需要在变量名前加上 "protected" 或 "private" 关键字。
现在看看 Person.ts 文件的底部:
另一个需要注意的重要事项是,在类内部,类的方法、属性和字段只能通过 "this" 关键字访问。如果您尝试删除它们前面的 "this.",编译器将报告错误。
let people: Array<Person>; people = [new Person("Joe", "Doe"), new Person("Jane", "Dane")]; let result: string = ""; for (let person of people) { result += person.FullName + "\n"; } alert(result);
我创建了一个全局作用域变量 `people`,类型为 `Array<Person>;`。然后我为其分配了一个包含两个 `Person` 对象的数组。现在,看看对数组项的迭代:
for (let person of people) { ... }
这等同于 C# 的 `foreach(var person in people)` 迭代器。
如果运行示例,它将在警报对话框中显示这两个人的全名。
扩展类
Solution ExtendingClass.sln 展示了如何在 TypeScript 中扩展类。
此解决方案中的 `Person` 类与上一个类中的完全相同,但我们也引入了 `Employee` 类,该类扩展了 `Person` 类。
class Employee extends Person { private _employeeNumber: number; get EmployeeNumber() { return this._employeeNumber; } // property overriding sample get LastName() { return "SuperCaleFlageolisticExpialodocios"; } constructor(employeeNumber:number, firstName:string, lastName:string) { // call constructor of the super class super(firstName, firstName); this._employeeNumber = employeeNumber; } }
它引入了一个只读属性 `EmployeeNumber`,由私有字段 `_employeeNumber` 支持。
另外,请注意,TypeScript 中的所有方法和属性都是虚拟的,在子类中定义同名属性或方法会自动覆盖它们(无需使用 "override" 关键字)。为了证明这一点,我重写了 `Employee` 对象的 `LastName` 属性的 getter,使其始终返回“SuperCaleFlageolistExpialodocios”,而不是 `LastName` 的 getter。
// property overriding sample get LastName() { return "SuperCaleFlageolisticExpialodocios"; }
现在,看看 Employee.ts 文件的底部:
// function that takes Person object // and displays it within alert dialog function PrintPersonFullName(person: Person): void { alert(person.FullName); } // create Employee object let joeDoeEmployee: Person = new Employee(1, "Joe", "Doe"); // call PrintPersonFullName function on // the joeDoeEmployee object PrintPersonFullName(joeDoeEmployee);
我们定义了一个函数 `PrintPersonFullName`,它接受一个 `Person` 对象,但我们传递了一个 `Employee` 对象。该函数只是在警报对话框中显示对象的 `FullName` 属性。我们将看到的完整名称是“Joe SuperCaleFlageolisticExpialodocios”,而不是“Joe Doe”,这意味着 `LastName` 属性是虚拟的并且被重写了。
另一个有趣的事情是在构造函数中调用 `super(...)` 方法。它调用超类(父类)的构造函数。
constructor(employeeNumber:number, firstName:string, lastName:string) { // call constructor of the super class super(firstName, firstName); this._employeeNumber = employeeNumber; }
这等同于 Java 中的 `super` 或 C# 中的 `base`。
最后,看看我们的 Default.html,脚本定义的位置:
<body> <script src="Scripts/Person.js"></script> <script src="Scripts/Employee.js"></script> </body>
请注意,我们必须按照它们被使用的顺序导入脚本。在导入 Person.js 之前导入 Employee.js 将导致错误。我们稍后将对此进行更详细的讨论。
实现接口
查看 ImplementingInterface 项目,位于 Scripts 文件夹下。它演示了如何实现多个接口。
`IFirstNameContainer` 接口包含 `FirstName` 属性。
interface IFirstNameContainer { FirstName: string; }
`ILastNameContainer` 接口包含 `LastName` 属性。
interface ILastNameContainer { LastName: string; }
`IMiddleNameContainer` 包含 `MiddleName` 属性。
interface IMiddleNameContainer { // '?' makes it optional property MiddleName?: string; }
请注意,'?' 问号使属性成为可选的,即接口不会“坚持”类必须拥有它。
现在,`IPerson` 接口实现了上面列出的所有三个接口。
interface IPerson extends IFirstNameContainer, ILastNameContainer, IMiddleNameContainer { }
我们的 `Person` 类实现了 `IPerson` 接口。
class Person implements IPerson { //#region // read-write property FirstName private _firstName: string; get FirstName(): string { return this._firstName; } set FirstName(value: string) { this._firstName = value; } //#endregion //#region // read-write property LastName private _lastName: string; get LastName(): string { return this._lastName; } set LastName(value: string) { this._lastName = value; } //#endregion // constructor taking two optional arguments constructor(firstName?: string, lastName?: string) { this.FirstName = firstName; this.LastName = lastName; } }
请注意,`Person` 没有实现 `MiddleName` 属性,但 TypeScript 仍然没有显示任何问题,因为该属性是可选的。
在 Person.ts 文件的底部,我使用箭头(lambda)表示法定义了一个名为 `PrintFullName` 的函数,而不是使用通常的 JavaScript 方式。
// defining a method (function) // using error notations let PrintFullName = (person: IPerson) => { let result: string = ""; if (person.FirstName) { result += person.FirstName + " "; } if (person.MiddleName) { result += person.MiddleName + " "; } if (person.LastName) { result += person.LastName; } alert(result); }
最后,我调用此函数在警报对话框中显示全名。
PrintFullName(new Person("Joe", "Doe"));
请注意,我本可以将 `PrintFullName` 函数定义为接受接口的有趣组合:
let PrintFullName = (person: IFirstNameContainer & ILastNameContainer & IMiddleNameContainer)...
这是一个很棒的功能 - C# 中没有。总的来说,我们不必定义联合接口,我们可以要求方法参数同时满足多个接口。
另一件有趣的事情是,我们的 `Person` 类实际上不必实现接口,以便我们的方法 `PrintFullName` 仍然可以工作(尽管该方法是为与接口一起工作而构建的,而不是与类对着干)。您可以注释掉 `implements IPerson` 部分,并验证一切仍然正常工作。这是因为编译器将检查接口所需的所有属性是否仍然存在,并将允许编译继续进行。
匿名类
我在 AnonymouseClasses.sln 解决方案中演示了匿名类的功能(顺便说一句,C# 中没有此功能,但它是 Java 的一部分)。
此项目中的 `IPerson` 接口非常简单。
interface IPerson { FirstName: string; LastName: string; }
现在看看 Person.ts 文件的顶部:
// create anonymous class let person = new class implements IPerson { private _firstName: string; get FirstName(): string { return this._firstName; } set FirstName(value: string) { this._firstName = value; } private _lastName: string; get LastName(): string { return this._lastName; } set LastName(value: string) { this._lastName = value; } constructor(firstName: string, lastName: string) { this.FirstName = firstName; this.LastName = lastName; } }("Joe", "Doe"); // call constructor
我们创建一个匿名类对象,该对象实现 `IPerson` 接口,并在创建对象的同时提供实现。
Generics
GenericsSample.sln 提供了一个非常简单的使用泛型的示例。
总的来说,我们为一个实现 `IUniqueIdInterface` 的对象创建了一个非常原始的缓存。
interface IUniqueIdContainer { readonly UniqueId: string; }
这是缓存的代码:
class UniqueIdContainerItemsCache<T extends IUniqueIdContainer> { private _mapObject = new Object(); // add new item to the cache // throw exception if such id already exists AddItem(item: T) { if (this._mapObject.hasOwnProperty(item.UniqueId)) { throw "item with the same id already exists in cache"; } this._mapObject[item.UniqueId] = item; } // get item by id. // if no item with such id exists, return null GetItem(uniqueId: string) : T { if (this._mapObject.hasOwnProperty(uniqueId)) { return this._mapObject[uniqueId] as T; } return null; } }
我们定义了 `IUniqueIdContainer` 接口的一个具体实现。
// UniqueObject class implements IUniqueContainer // interface class UniqueObject implements IUniqueIdContainer { UniqueId: string; constructor(uniqueId: string) { this.UniqueId = uniqueId; } }
然后我们为此类对象创建一个具体的缓存。
// define a cache storing UniqueObjects let uniqueObjectCache = new UniqueIdContainerItemsCache<UniqueObject>();
最后,我们向缓存添加一个项目并从中获取一个项目进行测试。
// add an item to the cache uniqueObjectCache.AddItem(new UniqueObject("1")); // get an item from the cache by id let objFromCache = uniqueObjectCache.GetItem("1");
摘要
本文的目的是为具有面向对象背景的开发人员提供 TypeScript 教程。
在这里,我讨论了该语言的核心 OO 功能。在下一部分中,我希望讨论一些更深入、更复杂的范例,包括从 JSON 映射和异步编程。
历史
于 2017 年 12 月 3 日对语法和拼写进行了改进。