本文章是由機器翻譯。

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 中的物件

手稿 JavaScript
類自動 {車輪 ;門 ;}var 車 = 新 Auto() ;car.wheels = 2;car.doors = 4 ; var 自動 = (函數 () {函數 Auto() {}返回汽車 ;})();var 車 = 新 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 簡單的建構函式

手稿 JavaScript
類自動 {車輪 ;門 ;建構函式 (w,d) {this.wheels = w ;this.doors = d;  }}var 車 = 新汽車 (2、 4) ; var 自動 = (函數 () {函數自動 (w,d) {this.wheels = w ;this.doors = d;  }返回汽車 ;})();var 車 = 新汽車 (2、 4) ;

 

圖 3 中,我修改了圖 2 中的代碼,將 wheels 參數 (w) 預設為 4 並通過在 doors 參數 (d) 右側插入問號使它成為可選的。 請注意,與上一個示例一樣,將實例屬性設置為參數的模式是使用「this」關鍵字的習慣作法。

圖 3 修改後的簡單建構函式

手稿 JavaScript
類自動 {車輪 ;門 ;建構函式 (w = 4,d?){this.wheels = w ;this.doors = d;  }}var 車 = 新 Auto() ; var 自動 = (函數 () {函數自動 (w,d) {this.wheels = w ;this.doors = d;  }返回汽車 ;})();var 車 = 新汽車 (4、 2) ;

此處是我希望在 .NET 語言中看到的功能:可以僅在建構函式中的參數名稱前添加 public 關鍵字來聲明類的屬性。 還可以使用 private 關鍵字完成相同的 auto 聲明,但是隱藏了類的屬性。

使用 TypeScript auto 屬性聲明功能擴展了預設值、可選參數和類型批註,這是一個好的快捷方式,可以提高您的工作效率。 比較圖 4 中的腳本,您可以清楚看到它們在複雜程度上的區別。

圖 4 Auto 聲明功能

手稿 JavaScript
類自動 {建構函式 (公共車輪 = 4,公共門?){  }}var 車 = 新 Auto() ;car.doors = 2 ; var 自動 = (函數 () {自動門輪子) {函數如果 (typeof 車輪 = = ="未定義") {車輪 = 4 ; }this.wheels = 車輪 ;this.doors = 門 ;  }返回汽車 ;})();var 車 = 新 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 的最大優勢之一,有四個固有類型:字串、 數位、 bool 和任何。 圖 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 創建聲明檔

手稿 JavaScript
函數 gradeAverage(grades: string[]) {總的 var = 0 ;var g = null ;var 我 =-1 ;為 (我 = 0 ; 我 < grades.length ; i++) {g = 年級 [i] ;總 + = getPointEquiv(grades[i]) ;  }var avg = 總 / grades.length ;返回 getLetterGrade(Math.round(avg));}函數 getPointEquiv(grade:{字串)var res ;switch(grade) {案例"A":{res = 4 ;            break;    }案例"B":{res = 3 ;            break;    }案例"C":{res = 2 ;            break;    }案例"D":{res = 1 ;            break;    }案例"F":{res = 0 ;            break;    }  }返回 res ;}函數 getLetterGrade(score:{數)if(score < 1) {返回"F";  }if(score > 3) {返回"A";  }if(score > 2 & & 分數 < 4) {返回"B";  }如果 (分數 > = 1 & & 分數 < = 2) {返回"C";  }if(score > 0 & & 分數 < 2) {返回"D";  }} 函數 gradeAverage(grades) {總的 var = 0 ;var g = null ;var 我 =-1 ;為 (我 = 0 ; 我 < grades.length ; i++) {g = 年級 [i] ;總 + = getPointEquiv(grades[i]) ;  }var avg = 總 / grades.length ;返回 getLetterGrade(Math.round(avg));}函數 getPointEquiv(grade) {var res ;switch(grade) {案例"A":{res = 4 ;            break;    }案例"B":{res = 3 ;            break;    }案例"C":{res = 2 ;            break;    }案例"D":{res = 1 ;            break;    }案例"F":{res = 0 ;            break;    }  }返回 res ;}函數 getLetterGrade(score) {if(score < 1) {返回"F";  }if(score > 3) {返回"A";  }if(score > 2 & & 分數 < 4) {返回"B";  }如果 (分數 > = 1 & & 分數 < = 2) {返回"C";  }if(score > 0 & & 分數 < 2) {返回"D";  }}

接下來,我將打開一個命令提示符並使用 TypeScript 命令列工具來創建該定義檔和生成的 JavaScript:

tsc c:\gradeAverage.ts –declarations

編譯器創建兩個檔:gradeAverage.d.ts 是聲明檔,gradeAverage.js 是 JavaScript 檔。 在需要 gradeAverage 功能的任何後續 TypeScript 檔中,我只在編輯器頂部添加一個引用,如下所示:

/ / < 引用 path="gradeAverage.d.ts">

然後在引用此庫時突出顯示所有類型和工具,對於您在 DefinitelyTyped GitHub 存儲庫中找到的任何主要的庫都是如此。

編譯器在聲明檔中引用的一個有用功能是自動遍歷引用。 這意味著如果您引用 jQueryUI 的聲明檔(它依次引用 jQuery),您的當前 TypeScript 檔將可以完成語句並看到函數簽名和類型,就好像您直接引用 jQuery 一樣。 您還可以創建一個聲明檔(例如「myRef.d.ts」),該檔包含對您要在解決方案中使用的所有庫的引用,然後在任何 TypeScript 代碼中只進行一次引用。

Windows 8 和手稿

由於 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,諾基亞開發人員冠軍、 MCP,佛羅里達州奧蘭多的 INETA 揚聲器和解決方案建築師 他已經發展基於 Microsoft 的解決方案過去 15 年。在過去 10 年中,他致力於開發大型 Web 應用程式,側重提高其工作效率和性能。在業餘時間,Boyer 參與奧蘭多 Windows Phone 和 Windows 8 使用者組工作,並在 tattoocoder.com 上發佈介紹最新技術的博客文章。

衷心感謝以下技術專家對本文的審閱:克里斯多夫 · Bennage