JavaScript

TypeScript: 让 .NET 开发人员适应 JavaScript

Shayne Boyer

 

毫无疑问,您在 Microsoft .NET Framework 上投入了大量资金,它确实是功能强大的平台,提供很多实用的工具。 如果您同时拥有了 C# 或 Visual Basic .NET 和 XAML 的知识,前途将不可限量。 但是,现在您需要考虑一种已经创立了一段时间的成熟语言,而且在过去几年内作为主要应用程序平台编程语言。 当然我要谈论的是 JavaScript。 JavaScript 应用程序越来越多,其功能也在不断增强。 Node.js 作为开发可扩展的 JavaScript 应用程序的整个平台已日益普及,它甚至可以在 Windows Azure 上部署。 而且,JavaScript 可以与 HTML5 结合使用,用于游戏开发、移动应用程序,甚至用于 Windows 应用商店应用程序。

作为 .NET 开发人员,您不能忽视 JavaScript 的功能,也不能忽视它在市场上的普及程度。 我向同事阐述这个观点时,经常听到他们抱怨 JavaScript 如何难用、没有提供强类型化、没有类结构等等。 我反驳他们说 JavaScript 是一种实用的语言,提供了很多模式来满足他们的需求。

这就是 TypeScript 所起的作用。 TypeScript 不是一种新语言。 它是 JavaScript 的超集 - 一种功能强大的类型化超集,这意味着所有 JavaScript 都是有效的 TypeScript 并且编译器生成的是 JavaScript。 TypeScript 是开源项目,与此项目有关的所有信息都可以在 typescriptlang.org 上找到。 撰写本文时,TypeScript 已推出预览版本 0.8.1。

在本文中,我将通过类、模块和类型来介绍 TypeScript 的基本概念,以说明 .NET 开发人员如何可以更轻松处理 JavaScript 项目。

如果您使用 C# 或 Visual Basic .NET 之类的语言,应该很熟悉类这个概念。 在 JavaScript 中,通过模式(如闭合和原型)来实现类和继承关系。 TypeScript 引入了您所熟悉的传统类型语法,并且编译器生成实现该意向的 JavaScript。 以下面的 JavaScript 代码段为例来说明:

var car;
car.wheels = 4;
car.doors = 4;

它似乎很简单明了。 但是,.NET 开发人员一直很犹豫是否要使用 JavaScript,因为它的对象定义很宽松。 car 对象以后可以添加其他属性,而不实施和不知道每个属性表示什么数据类型,因此在运行时引发异常。 TypeScript 类模型定义如何改变这种情况,我们如何继承和扩展 car 呢? 让我们看一下图 1 中的示例。

图 1 TypeScript 和 JavaScript 中的对象

TypeScript JavaScript
class Auto{  wheels;  doors;}var car = new Auto();car.wheels = 2;car.doors = 4; var Auto = (function () {  function Auto() { }  return Auto;})();var car = new Auto();car.wheels = 2;car.doors = 4;

在左侧是正确定义的名为 car 的类对象,它具有属性 wheels 和 doors。 在右侧,TypeScript 编译器生成的 JavaScript 几乎是相同的。 唯一的区别是 Auto 变量。

在 TypeScript 编辑器中,您添加其他属性必然会收到警告。 不能简单使用 car.trunk = 1 这样的语句来开始。 编译器会显示“不存在 Auto 的 trunk 属性”消息,这对必须找到此信息的任何人来说是件好事,因为 JavaScript 的灵活性 - 或按您的说法,因为 JavaScript 的“惰性”。

构造函数尽管在 JavaScript 中可用,它通过以下方式使用 TypeScript 工具再次得到增强:在编译时创建该对象,在没有在调用中传入正确元素和类型时不允许创建该对象。

您不仅可以将构造函数添加到类,还可以使参数成为可选的、设置默认值或设置属性声明的快捷方式。 让我们看一下仅显示 TypeScript 的功能如何强大的三个示例。

图 2 显示第一个示例,它是一个简单的构造函数,在其中通过传入 wheels 和 doors 参数(此处用 w 和 d 表示)来初始化类。 生成的 JavaScript(在右侧)几乎是等效的,但是随着您的应用程序的需求变化,有时并不是这样。

图 2 简单的构造函数

TypeScript JavaScript
class Auto{  wheels;  doors;  constructor(w, d){    this.wheels = w;    this.doors = d;  }}var car = new Auto(2, 4); var Auto = (function () {  function Auto(w, d) {    this.wheels = w;    this.doors = d;  }  return Auto;})();var car = new Auto(2, 4);

 

图 3 中,我修改了图 2 中的代码,将 wheels 参数 (w) 默认为 4 并通过在 doors 参数 (d) 右侧插入问号使它成为可选的。 请注意,与上一个示例一样,将实例属性设置为参数的模式是使用“this”关键字的习惯作法。

图 3 修改后的简单构造函数

TypeScript JavaScript
class Auto{  wheels;  doors;  constructor(w = 4, d?){    this.wheels = w;    this.doors = d;  }}var car = new Auto(); var Auto = (function () {  function Auto(w, d) {    this.wheels = w;    this.doors = d;  }  return Auto;})();var car = new Auto(4, 2);

此处是我希望在 .NET 语言中看到的功能: 可以仅在构造函数中的参数名称前添加 public 关键字来声明类的属性。 还可以使用 private 关键字完成相同的 auto 声明,但是隐藏了类的属性。

使用 TypeScript auto 属性声明功能扩展了默认值、可选参数和类型批注,这是一个好的快捷方式,可以提高您的工作效率。 比较图 4 中的脚本,您可以清楚看到它们在复杂程度上的区别。

图 4 Auto 声明功能

TypeScript JavaScript
class Auto{  constructor(public wheels = 4,    public doors?){  }}var car = new Auto();car.doors = 2; var Auto = (function () {  function Auto(wheels, doors) {    if (typeof wheels ===      "undefined") {      wheels = 4; }    this.wheels = wheels;    this.doors = doors;  }  return Auto;})();var car = new Auto();car.doors = 2;

 

TypeScript 中的类也提供继承关系。 继续以 Auto 示例为例,您可以创建扩展初始类的 Motorcycle 类。 在图 5 中,我还向基类添加了 drive 和 stop 函数。 在 TypeScript 中,使用几行代码就可完成添加 Motorcycle 类(它从 Auto 继承并设置 doors 和 wheels 的相应属性)。

图 5 添加 Motorcycle 类

class Auto{
  constructor(public mph = 0,
    public wheels = 4,
    public doors?){
  }
  drive(speed){
    this.mph += speed;
  }
  stop(){
    this.mph = 0;
  }
}
class Motorcycle extends Auto
{
  doors = 0;
  wheels = 2;
}
var bike = new Motorcycle();

在此处要指出的一个重要事项是:在编译器生成的 JavaScript 顶部,您将看到一个名为“___extends”的小函数(如图 6 中所示),它是曾注入生成的 JavaScript 中的唯一代码。 这是帮助执行继承功能的帮助器类。 顺便提一下,无论源代码是什么,此帮助器函数都具有完全相同的签名。因此,如果您要在多个文件中组织 JavaScript 并使用诸如 SquishIt 或 Web Essentials 等实用工具来合并脚本,根据实用工具如何更正重复的函数,系统可能显示一个错误。

图 6 编译器生成的 JavaScript

var __extends = this.__extends || function (d, b) {
  function __() { this.constructor = d; }
  __.prototype = b.prototype;
  d.prototype = new __();
}
var Auto = (function () {
  function Auto(mph, wheels, doors) {
    if (typeof mph === "undefined") { mph = 0; }
    if (typeof wheels === "undefined") { wheels = 4; }
    this.mph = mph;
    this.wheels = wheels;
    this.doors = doors;
  }
  Auto.prototype.drive = function (speed) {
    this.mph += speed;
  };
  Auto.prototype.stop = function () {
    this.mph = 0;
  };
  return Auto;
})();
var Motorcycle = (function (_super) {
  __extends(Motorcycle, _super);
  function Motorcycle() {
    _super.apply(this, arguments);
    this.doors = 0;
    this.wheels = 2;
  }
  return Motorcycle;
})(Auto);
var bike = new Motorcycle();

模块

TypeScript 中的模块等效于 .NET Framework 中的命名空间。 它们是组织代码和封装业务规则以及流程的好方法,如果没有这个功能,就不可能做到这点(JavaScript 没有内置提供此功能)。 模块模式或动态命名空间和在 JQuery 中一样,是用于 JavaScript 中的命名空间的最常见模式。 TypeScript 模块简化了语法并产生相同的效果。 在 Auto 示例中,您可以在模块中包装代码并仅公开 Motorcycle 类,如图 7 中所示。

该 Example 模块封装基类,并且通过使用 export 关键字做前缀来公开 Motor­cycle 类。 这允许创建 Motorcycle 实例和使用它的所有方法,但是隐藏 Auto 基类。

图 7 在模块中包装 Auto 类

module Example {
  class Auto{
    constructor(public mph : number = 0,
      public wheels = 4,
      public doors?){
      }
      drive(speed){
      this.mph += speed;
      }
      stop(){
      this.mph = 0;
      }
  }
  export class Motorcycle extends Auto
  {
    doors = 0;
    wheels = 2;
  }
}
var bike = new Example.Motorcycle();

模块的另一个好处是您可以合并它们。 如果您创建另一个也名为 Example 的模块,TypeScript 假定第一个模块中的代码和新模块中的代码都可通过 Example 语句访问,就像在命名空间中一样。

模块为维护和组织代码提供了便利。 有了它们,维护大型应用程序对开发团队而言将不再是沉重的负担。

类型

对于不愿意使用 Java­Script 的开发人员来说,他们抱怨最多的缺陷之一是 Java­Script 不提供类型安全性。 但是,实际上 TypeScript 是提供类型安全性的(这正是它称为 TypeScript 的原因),它不仅仅是将变量声明为字符串或布尔值。

在 JavaScript 中,将 foo 赋给 x 接着在代码中将 11 赋给 x 是完全可行的,但是当您想尝试了解为什么在运行时得到经常存在的 NaN 时就会头疼不已。

类型安全性功能是 TypeScript 的最大优势之一,有四个固有类型: string、number、bool 和 any。 图 8 显示声明变量 s 的类型的语法以及在编译器知道您根据该类型可以执行什么操作后提供的 IntelliSense。

An Example of TypeScript IntelliSense
图 8 TypeScript IntelliSense 的示例

除了允许声明变量或函数的类型之外,TypeScript 还可以推断类型。 您可以创建仅返回字符串的函数。 知道该函数后,编译器和工具提供类型推断并自动显示可以对返回值执行的操作,如图 9 中所示。

An Example of Type Inference
图 9 类型推断的示例

此处的好处是您看到返回值为字符串,而不必猜测它。 当要使用开发人员在代码中引用的其他库(如 JQuery 或甚至是文档对象模型 (DOM))时,类型推断能提供很大帮助。

利用类型系统的另一方式是通过批注。 我们回想一下,原始 Auto 类是只使用 wheels 和 doors 进行声明的。 现在,通过批注,我们可以确保在 car 中创建 Auto 实例时设置正确的类型:

class Auto{
  wheels : number;
  doors : number;
}
var car = new Auto();
car.doors = 4;
car.wheels = 4;

不过,在生成的 JavaScript 中,批注被编译去掉,因此不必担心引入多余内容或其他依赖关系。 它的好处是强类型化并避免了在运行时通常发生的简单错误。

接口提供在 TypeScript 中提供的类型安全性的另一个示例。 接口允许您定义对象的形状。 在图 10 中,将名为 travel 的新方法添加到了 Auto 类,它接受类型为 Trip 的参数。

图 10 Trip 接口

interface Trip{
  destination : string;
  when: any;
}
class Auto{
  wheels : number;
  doors : number;
  travel(t : Trip) {
  //..
}
}
var car = new Auto();
car.doors = 4;
car.wheels = 4;
car.travel({destination: "anywhere", when: "now"});

如果您尝试使用不正确的结构调用 travel 方法,设计时编译器将返回错误。 相比之下,如果您将 JavaScript 中的此代码输入比如说 .js 这样的文件,则在运行应用程序前,很可能您不会看到错误。

图 11 中,您可以看到利用类型批注不仅为初始开发人员而且为维护源代码的所有后续开发人员提供了很大帮助。

Annotations Assist in Maintaining Your Code
图 11 批注对维护代码的帮助

现有的代码和库

如何处理现有 JavaScript 代码,或如果您喜欢在 Node.js 上构建应用程序或使用诸如 toastr、Knockout 或 JQuery 的库怎么办? TypeScript 中的声明文件可以提供帮助。 首先,请记住所有 JavaScript 都是有效的 TypeScript。 因此,如果您在原来代码基础上生成了新代码,可以直接将代码复制到设计器,编译器将为您逐一生成 JavaScript。 更好的方法是创建您自己的声明文件。

对于主要的库和框架,Boris Yankov 先生(Twitter 网址为 twitter.com/borisyankov)在 GitHub 上创建了一个有用的存储库 (github.com/borisyankov/DefinitelyTyped),该库包含用于一些最常见的 JavaScript 库的声明文件。 这正是 TypeScript 团队所希望的。 顺便说一下,Node.js 声明文件已由 TypeScript 团队创建并作为源代码的一部分提供。

创建声明文件

如果您找不到用于您的库的声明文件或要使用自己的代码,将需要创建声明文件。 首先您将 JavaScript 代码复制到 TypeScript 一侧并添加类型定义,然后使用命令行工具来生成要引用的定义文件 (*.d.ts)。

图 12 显示 JavaScript 中用于计算年级平均分数的简单脚本。 我将该脚本复制到了编辑器的左侧并添加了类型的批注,将使用 .ts 扩展名保存文件。

图 12 创建声明文件

TypeScript JavaScript
function gradeAverage(grades : string[]) {  var total = 0;  var g = null;  var i = -1;  for(i = 0; i < grades.length; i++) {      g = grades[i];      total += getPointEquiv(grades[i]);  }  var avg = total / grades.length;  return getLetterGrade(Math.round(avg));}function getPointEquiv(grade : string) {  var res;  switch(grade) {    case "A": {      res = 4;      break;    }    case "B": {      res = 3;      break;    }    case "C": {      res = 2;      break;    }    case "D": {      res = 1;      break;    }    case "F": {      res = 0;      break;    }  }  return res;}function getLetterGrade(score : number) {  if(score < 1) {    return "F";  }  if(score > 3) {    return "A";  }  if(score > 2 && score < 4) {    return "B";  }  if(score >= 1 && score <= 2) {    return "C";  }  if(score > 0 && score < 2) {    return "D";  }} function gradeAverage(grades){  var total = 0;  var g = null;  var i = -1;  for(i = 0; i < grades.length; i++) {      g = grades[i];      total += getPointEquiv(grades[i]);  }  var avg = total / grades.length;  return getLetterGrade(Math.round(avg));}function getPointEquiv(grade) {  var res;  switch(grade) {    case "A": {      res = 4;      break;    }    case "B": {      res = 3;      break;    }    case "C": {      res = 2;      break;    }    case "D": {      res = 1;      break;    }    case "F": {      res = 0;      break;    }  }  return res;}function getLetterGrade(score) {  if(score < 1) {    return "F";  }  if(score > 3) {    return "A";  }  if(score > 2 && score < 4) {    return "B";  }  if(score >= 1 && score <= 2) {    return "C";  }  if(score > 0 && score < 2) {    return "D";  }}

接下来,我将打开一个命令提示符并使用 TypeScript 命令行工具来创建该定义文件和生成的 JavaScript:

tsc c:\gradeAverage.ts –declarations

编译器创建两个文件: gradeAverage.d.ts 是声明文件,gradeAverage.js 是 JavaScript 文件。 在需要 gradeAverage 功能的任何后续 TypeScript 文件中,我只在编辑器顶部添加一个引用,如下所示:

/// <reference path="gradeAverage.d.ts">

然后在引用此库时突出显示所有类型和工具,对于您在 DefinitelyTyped GitHub 存储库中找到的任何主要的库都是如此。

编译器在声明文件中引用的一个有用功能是自动遍历引用。 这意味着如果您引用 jQueryUI 的声明文件(它依次引用 jQuery),您的当前 TypeScript 文件将可以完成语句并看到函数签名和类型,就好像您直接引用 jQuery 一样。 您还可以创建一个声明文件(例如“myRef.d.ts”),该文件包含对您要在解决方案中使用的所有库的引用,然后在任何 TypeScript 代码中只进行一次引用。

Windows 8 和 TypeScript

由于 HTML5 是 Windows 应用商店应用程序开发的首选语言,开发人员想知道 TypeScript 是否可以用于这类应用程序。 简单地说可以,但是需要进行一些设置才能这样做。 撰写本文时,通过 Visual Studio 安装程序或其他扩展提供的工具尚未完全启用 Visual Studio 2012 中的 JavaScript Windows 应用商店应用程序模板。

typescript.codeplex.com 上的源代码中提供三个重要的声明文件:winjs.d.ts、winrt.d.ts 和 lib.d.ts。 通过引用这些文件,您可以访问在此环境中使用的 WinJS 和 WinRT JavaScript 库,以访问相机、系统资源等。 您还可以添加对 jQuery 的引用以获得我在本文中所述的 IntelliSense 和类型安全性功能。

图 13 是一个简单的示例,它显示如何使用这些库来访问用户的地理位置并填充 Location 类。 该代码然后创建一个 HTML 图像标记并从 Bing 地图 API 添加静态地图。

图 13 用于 Windows 8 的声明文件

/// <reference path="winjs.d.ts" />
/// <reference path="winrt.d.ts" />
/// <reference path="jquery.d.ts" />
module Data {
  class Location {
    longitude: any;
    latitude: any;
    url: string;
    retrieved: string;
  }
  var locator = new Windows.Devices.Geolocation.Geolocator();
  locator.getGeopositionAsync().then(function (pos) {
    var myLoc = new Location();
    myLoc.latitude = pos.coordinate.latitude;
    myLoc.longitude = pos.coordinate.longitude;
    myLoc.retrieved = Date.
now.toString();
    myLoc.url = "http://dev.virtualearth.
net/REST/v1/Imagery/Map/Road/"
      + myLoc.latitude + "," + myLoc.longitude
      + "15?mapSize=500,500&pp=47.620495,-122.34931;21;AA&pp="
      + myLoc.latitude + "," + myLoc.longitude
      + ";;AB&pp=" + myLoc.latitude + "," + myLoc.longitude
      + ";22&key=BingMapsKey";
    var img = document.createElement("img");
    img.setAttribute("src", myLoc.url);
    img.setAttribute("style", "height:500px;width:500px;");
    var p = $("p");
    p.append(img);
  });
};

总结

TypeScript 添加到 JavaScript 开发中的功能很小,但是对于习惯了那些在常规 Windows 应用程序开发所用语言中的类似功能的 .NET 开发人员来说,这些小功能给他们带来很多好处。

TypeScript 不是什么新技术,也不打算发展成为新技术。 但是对于那些仍在犹豫是否使用 JavaScript 的人来说,TypeScript 可以为他们掌握 JavaScript 提供很大帮助。

Shayne Boyer 是佛罗里达州奥兰多的 Telerik MVP、Nokia 开发人员奖励计划获奖人员、MCP、INETA 讲师和解决方案架构师。 他从事基于 Microsoft 的解决方案的开发工作有 15 年了。 在过去 10 年中,他致力于开发大型 Web 应用程序,侧重提高其工作效率和性能。 在业余时间,Boyer 参与奥兰多 Windows Phone 和 Windows 8 用户组工作,并在 tattoocoder.com 上发布介绍最新技术的博客文章。

衷心感谢以下技术专家对本文的审阅: Christopher Bennage