寫在前面
作者並沒有任何可以作為背書的履歷來證明自己寫作這份手冊的分量。
其內容大都來自於TypeScript官方資料或者搜尋引擎獲得,期間摻雜少量作者的私見,並會標明。
大部分內容來自於http://www.infoq.com/minibooks/typescript-c-sharp-programmers
你甚至可以認為這就是對這本英文小冊子的翻譯,實際上80%如此。
寫給那些已經有程式設計基礎,尤其是掌握c語言、c#、java這一型別的靜態型別語言的同好。
鳴謝
先謝國家,雖然並不知道要謝些什麼。
感謝我的太太和我的貓,他們的陪伴讓我沉溺於電腦世界卻不孤獨。
感謝那些上班時間奮戰在QQ群裡的程式鬥士們,他們讓我始終秉持一顆藝術家的心。
感謝微軟,TypeScript是微軟的註冊商標哦。
真正的開篇文字
只要你是對html5這個話題感興趣的程式設計師,我們就離不開JavaScript這個話題。而新興的TypeScript想必你也會有極大的興趣。JavaScript如今到處都是,web、伺服器(通過NodeJS)、移動應用(通過各種框架),所有這些,TypeScript都可以使用,並且可以為JavaScript擴充套件出物件導向和靜態型別的特徵。
TypeScript最初的靈感來自ECMAScript6,也就是未來的JavaScript。
TypeScript能讓我們提前使用未來的語言特性,甚至更多,例如泛型這種語言特性。
TypeScript程式碼,最終會編譯為地道的JavaScript,相容一切使用JavaScript的場合。
編譯過程主要是編譯時檢查,一點改寫,刪除型別批註和介面。刪除型別批註和介面這個過程稱為型別擦除(Type Erasure)。
我從網上找到一張很好的圖片用來說明型別擦除,如下。
TypeScript的創造者是微軟的一個開發團隊,該團隊的領導者是C#之父 和 Delphi之父,Anders Hejlsberg。
2012年10月1日釋出,完全開源。
當前版本已達1.8,已經擁有了很多生產力專案,TypeScript的主頁在http://www.typescriptlang.org/
我們後面將詳細介紹TypeScript並對比他和其他語言的異同,主要是C#。
關於TypeScript到底是Compiling 還是 Transpiling
這個話題很難說清楚,但是很有必要在提到TypeScript的時候講一下,這兩個詞:編譯Compiling,Transpiling有人譯作轉譯,這是一個英文計算機術語。一般認為轉譯是一種特殊的編譯,當將一種原始碼語言編譯成另外一種原始碼語言時,就稱為轉譯。
當編譯一個c#程式時,是由原始碼語言C#編譯為IL,這就不能稱為Transpiling,因為他們是完全不同的東西。
而編譯TypeScript程式時,他變成了另外一種原始碼JavaScript,這個就稱為Transpiling(轉譯)。
但無論如何,Transpiling是Compiling的特例,Transpiling也屬於Compiling。
所以TypeScript轉譯為Javascript,TypeScript編譯為JavaScript,都是沒有問題的講法。
TypeScript語言特性
這裡快速介紹一下TypeScript的關鍵語法,比如顯示型別批註、類、介面。
雖然C# Java程式設計師都很熟悉物件導向,但TypeScript並不基於C#,所以還是有所區別的。
TypeScript是靜態型別語言,需要編譯,擁有編譯時型別檢查的特性。
編譯時型別檢查能夠確保型別安全,並方便開發更智慧的自動完成功能,實際上TypeScript的各種開發工具都做得很不錯。
比如VisualStudio,編寫TypeScript檔案時,就比編寫JavaScript要聰明的多。這就是靜態型別帶來的好處。
TypeScript檔案
TypeScript檔案的副檔名為".ts",你可以使用很多工具編寫.ts檔案,比如visualstudio。更多資訊,請看官網http://www.typescriptlang.org/
官網還提供了一個線上編寫測試.ts檔案的環境http://www.typescriptlang.org/Playground/
一個TypeScript應用包含多個TypeScript程式碼檔案,一個程式碼檔案可以包含多個Class,Class也可以組成模組。
模組的概念和C#中組織型別的namespace比較接近。
執行時,TypeScript編譯得到的JavaScript可以通過Html標籤<Script>加入網頁中,也可以使用其他的模組載入工具,比如NodeJS就內建了模組載入工具。
不使用模組組織依賴的時候,一個TypeScript檔案依賴另一個TypeScript檔案,應該加上引用註釋
(這是可選的,一般也不使用命令列編譯,大部分圖形化工具不加也可以)。
當使用模組載入工具的時候(比如RequireJS,或者NodeJS內建載入工具),程式碼如下:
不知道啥時RequireJS的同學請自行補課。
當你使用import語句的時候,是有兩種模式的,CommonJS 和 AMD模式,他們編譯為JavaScript生成的程式碼不同,這個根據你使用不同的載入工具請自己設定TypeScript編譯選項。
注:RequireJS使用CommonJS模式,NodeJS使用AMD模式。
型別
TypeScript的基本型別有 string,number,boolean,null 和undefined.
因為JavaScript沒有整數小數這些區分,所以TypeScript也沒有新增,統一使用number。
另外TypeScript新增了一個any型別,當需要動態型別時使用。這個與C#的動態關鍵字是類似的。
TypeScript支援陣列型別,通過在型別後面新增方括號定義,比如string[].
也可以自定義型別,在講到class的時候我們細說。
型別推斷
當右側表示式足以確定變數的型別時,TypeScript可以自動確定變數的型別,這個特性C#也有。
比如這個程式碼
如果在VisualStudio中,滑鼠懸停,依然可以看到這三個變數被識別出來是各自不同的型別。
型別推斷還能處理更復雜的型別資訊,比如這個程式碼,無需型別標註即可獲得靜態型別的好處。
寫完這個程式碼以後,之後再鍵入example,再敲入.,也可以獲得自動完成提示,.name,.id.collection.
型別推斷始終是有侷限性的,基本上是依靠右側表示式來推斷。如果宣告變數時沒有推斷出來,事後賦值也推斷不出來了。
比如下面的程式碼。
當宣告變數時無法推斷出,變數即被標註為 any 型別,any型別完全沒有任何型別安全方面的保證。當然自動完成功能也沒有。
型別批註
TypeScript的型別是設計為批註,而不是定義,是可選的,所以和C#的型別寫在前面不同,型別是以可選的形式寫在後面的。
我們將上面的所有程式碼都加上型別標註
這些標註的新增和上面自動型別推斷的結果是一樣的。但是閱讀程式碼的人就可以一眼看出。
模組、型別、介面
TypeScript的模組用於程式碼組織,類似C#的namespace。一個模組可以包含多個類和介面。可以將類和介面私有化或者匯出,匯出的意思就是公開,讓其他模組可以訪問他們。
TypeScript的class和C#的class意義相同。實際上TypeScript的一個亮點就是他隱藏了JavaScript的原型設計,而是採用了更流行的基於型別的物件導向方式。你可以擴充套件其他的class,實現多個介面,新增建構函式,公開屬性和方法。這些都和c#的class很相似。
屬性和方法可以使用public 或private訪問修飾符 修飾。當在建構函式的引數上使用訪問修飾符的時候,他們會自動為該型別新增同名的屬性。請非常小心這個語法,這和很多語言都不同
如圖中Point的建構函式引數x,y 使用了public 訪問修飾符,所以會自動生成Point的成員變數x和y,這是TypeScript的特有語法。
圖中的export 關鍵字使類和介面在模組外部可見。實現介面使用implements關鍵字,繼承類使用extends關鍵字,這點和C#直接用一個冒號表達不同。
當你擴充套件一個類時,用super關鍵字呼叫基類的方法。用this關鍵字來呼叫當前類的屬性和方法。重寫基類的方法是可選的。建構函式必須要呼叫基類的建構函式,編譯器會提醒你的。
模組名稱包含點,一個模組可以定義在多個檔案中。模組的名字別太長,訪問的時候打字會比較累。下圖是一個比較累的例子
函式(或者叫功能)的引數
TypeScript的函式引數擁有豐富的特性:可選引數、預設引數、不定引數。
有一些和其他語言的設計不太相同。下面將一一說明。
雖然在設計上,這些引數特性都不是必要的,但是TypeScript的這些地方都有些特殊性,你得了解一下,以備看懂。
可選引數的設計和C#基本一致,符號用? 表示可選引數,可選引數必須出現在必選引數之後。
預設引數只要在定義時給附上值就行了,並且和c#不同,TypeScript的預設引數不需要是常量,執行時可解釋即可,後文會有說明
下圖是特殊的預設引數
不定引數可以指定任意數量,可以為零,這個設計也和c#類似,只是要新增三個點。
下面就是不定引數的一個典型使用場景
函式過載
Javascript是不允許重名函式的,TypeScript實現的過載和c#有很大的不同。
TypeScript的過載是要把所有的函式簽名寫出來,寫一份實現,並且最後一個函式簽名要能包含上面所有的函式簽名。
如下圖:
和C#三個過載的函式就擁有三個函式體不同,TypeScript的過載其實全是 overloadedMethod(input:any)這最後一個簽名的實現,上面的兩個只是兩個相容的簽名。但是配合可選引數還是能夠表現出和c#的函式過載類似的呼叫方式。函式編寫方式免不了要寫很多的if了。
列舉
TypeScript的列舉非常類似C#,你可以指定值。
也可以反過來通過列舉值取到列舉的名字
這裡面的細節就不再贅述了,你可以觀察列舉編譯為JavaScript以後是什麼樣子
泛型
對c#程式設計師來說,TypeScript的泛型很熟悉,基本上是一致的設計。
型別約束
C#使用where關鍵字標記型別約束,TypeScript在尖括號內使用extends關鍵字,效果相同。
下面的例子中IExample約束了泛型必須是IMyInterface和他的派生類。
如果像下圖這樣用的話,就能約束為同時繼承ClassOne和ClassTwo的型別。很費解吧,請特別注意。
這是因為本質上TypeScript的型別系統並不那麼嚴格,下面的章節會詳細解釋TypeScript的型別系統
你也可以使用泛型作為泛型的型別約束,如下
結構型別
C#的型別系統是強制標記的,物件的型別必須顯示宣告。即使兩個型別擁有完全相同的結構,他們也不是相同的型別。
TypeScript的型別系統是結構上的,建築結構,層次型的。結構相同的型別即可認為是同一型別。
下面是C#中的一個例子
ClassA 和 ClassB是完全不同的型別,他們之間是不同的,必須顯示繼承介面才能讓他們相容。
而在TypeScript中不是這樣,我們用如下的例子來證明。ClassA ClassB ExampleC 擁有簽名一致的函式,所以他們就可以相容。
TypeScript的結構型別系統意味著你在c#中的觀念不再成立,class name不是關鍵。這需要我們寫程式碼的時候時刻注意。
這玩意會讓程式碼千變萬化,如果你熟悉C#或者JAVA,這可能會讓你困惑。
看下面的例子,不需要class關鍵字,也會實實在在的產生型別。
在這個例子中,會產生一個匿名的型別
這個匿名的型別可以讓開發工具提供自動完成功能,編譯器也會檢查。如果你嘗試將objA的name賦值一個數值,編譯器會檢查到告訴你錯誤。編譯器還會為陣列推斷型別。
訪問修飾器
TypeScript的訪問修飾器可能會給你一種弱小的感覺,的確如此。他僅僅是一個編譯時功能。
模組中的一切均為私有,除非加上export關鍵字。沒有export關鍵字的型別僅能在模組內實用。
類的內部,一切均為公開,除非加上private 關鍵字。public只是為了看起來意圖明確。
TypeScript的訪問修飾器就是這樣而已,沒有c#的 internal 和 protected這種修飾器,想要c#類似的功能就放棄吧。
TypeScript特性
記憶體管理
當你執行TypeScript 程式時,他會被編譯為JavaScript程式來執行。JavaScript的記憶體管理和C#比較接近。記憶體在物件建立時分配,在物件不再使用時回收。不同的是垃圾回收機制在JavaScript的世界裡沒有標準統一的實現,這意味著你的JavaScript程式的記憶體效能相比C#難以預測。
比如說,在比較早的瀏覽器上,可能使用的是引用計數垃圾回收機制,當一個物件的引用計數達到0時,將回收記憶體。這種垃圾回收機制比較快速和即時。但是當發生迴圈引用時,引用計數將永遠也無法達到零。
比較新的瀏覽器使用標記與清掃垃圾回收機制來找出不可訪問到的物件,很大程度上避免了這個問題。這種垃圾回收機制比較緩慢,但他能避免迴圈引用導致的記憶體洩露。
關於兩種垃圾回收機制有很多的資料可以研究,這裡只是想告訴你,別相信你的直覺,瀏覽器會很不同。
資源釋放
在TypeScript中,通常都不會使用到非託管的資源,Node.JS中多一點。大部分瀏覽器將底層互動API設計為回撥控制,不向你暴露物件,不需要你自己管理非託管資源。比如下面接近感測器的API使用方法。
如你所見,使用回撥並不需要管理任何引用。Node.js中有時會碰到需要自己釋放的物件,你要保證釋放這些物件,否則就會記憶體洩露。你可以使用 try finally 塊,釋放程式碼寫在 finally 塊中。這樣就能保證就算髮生任何錯誤,釋放程式碼都會執行。
異常
在TypeScript中,你可以用throw關鍵字引發一個異常。
在JavaScript中,throw可以throw任何型別的東西。但是再TypeScript中,throw的必須是一個Error物件。
要自定義異常,可以繼承Error類。當你需要一個特定的異常行為或者你希望catch塊可以分辨異常型別時,自定義異常就會很有用。
處理異常需要使用try catch語句塊。大體上和c#的使用方法是很接近的,但是c#支援多個catch塊,TpyeScript不可以,你可以用error.name來區分異常型別。
如果需要一些即使發生異常也會呼叫的程式碼,你就需要finally語句塊了,他們的執行順序如下圖
陣列
TypeScript 從0.9開始,陣列就是可以指定內容的準確型別。用法是,用型別批註加上方括號。TypeScript會檢查加入到陣列中的條目的型別,也會推斷從陣列中檢索的條目的型別。
因為TypeScript沒有自己的框架庫,所以只能使用內建的JavaScript函式和瀏覽器介面,沒有像C#的List<T> 這樣的泛型庫。但這並不能阻止我們自己創造一個。下面是一個泛型列表類的例子,只是個演示。
上面這個List類演示了許多TypeScript的語言特性和使用方法,就像C#的List<T>那樣。下面是如何使用這個List類的例子。
日期與時間
TypeScript中的Date物件是基於JavaScript Date 物件的,他是由從1970年零點 utc時間開始的毫秒數。
Date物件能夠接受各種精度的初始化,如下。
你也可以使用RFC和ISO格式的字串初始化Date物件,當然,毫秒數也可以(從1970年1月1日開始)。
請注意,這不是時間戳,時間戳單位是秒,數字比這個少幾個零。
Now
你可以訪問現在的日期與時間,通過方法Date.Now();返回值是毫秒,你如果需要用Date物件去操作。需要用這個值去初始化一個Date物件
Date 的方法
當你有一個Date物件時,你可以用內部方法獲取日期的一部分。有兩套方法,一套本地,一套UTC。在下面的例項中,你看到我們把getUTCMonth的值加了1.因為返回值從零開始,所以一月是 0,2 月是 1,等等。
日期的各個部分為年、月、日、小時、分鐘、秒、毫秒。所有這些值都可以在獲取本地的和 UTC 的。還可以用 getDay 或 getUTCDay得到星期,從零開始,星期天為 0,星期一為1。
也可以利用各種格式顯示日期。如下。
事件
TypeScript中的事件是 DOM API(Document Object Model),DOM API事件是一套標準的滑鼠和鍵盤互動和物件和窗體事件。下面的事件列表並非詳盡無遺。
滑鼠事件
事件 | 觸發時機 |
onblur | 焦點離開目標元件 |
onclick | 目標元件檢測到滑鼠單擊 |
ondblclick | 目標元件檢測到滑鼠雙擊 |
onfocus | 目標元件獲得焦點 |
onmousedown | 目標元件檢測到滑鼠按下 |
onmousemove | 目標元件檢測到滑鼠指標移動 |
onmouseover | 滑鼠指標經過目標元件 |
onmouseout | 滑鼠指標離開目標元件 |
onmouseup | 目標元件檢測到滑鼠按鈕鬆開 |
鍵盤事件
事件 | 觸發時機 |
onkeydown | 目標元件鍵盤按下 |
onkeypress | 目標元件鍵盤按鍵按下並鬆開 |
onkeyup | 目標元件鍵盤松開 |
物件事件
事件 | 觸發時機 |
onload | 物件載入(文件或影象等) |
onresize | 物件尺寸改變 |
onscroll | 一個文件滾動 |
onunload | 文件關閉 |
表單事件
事件 | 觸發時機 |
onchange | 目標輸入框的內容改變 |
onreset | 表單重置(清空) |
onsubmit | 表單提交 |
自定義事件
TypeScript中自定義事件和DOM內建事件採用相同的機制釋出和偵聽。任何事件都可以有多個偵聽器和多個釋出者。下面是一個偵聽自定義事件的例子
下面的類釋出自定義的事件:
執行順序
事件按照他們被註冊的順序執行。這個因素很重要,因為事件處理程式是序列執行,不是同時,順序會影響邏輯。
當你註冊兩個事件偵聽器,並且第一個執行了5秒鐘。那麼第二個就不會執行,一直要等到第一個執行完畢之後才會被執行。如果第一個偵聽器出錯,第二個還是會執行。
你可以用setTimeOut時間引數為零的方式去執行你的長時間操作,這相當於多執行緒,就不會堵住其他的事件執行了。
框架
TypeScript捆綁了那些常用物件和方法。因為Web標準在不斷的演變,你可能會經常發現一些新的東西還沒有被包含在標準庫中。你可以檢視你計算機上的TypeScript標準庫檔案,他在SDK目錄中,通常可能是:
C:\Program Files (86) \Microsoft SDKs\TypeScript\lib.d.ts
你永遠也不應該修改這個檔案,如果你需要新的定義,我們可以繼續增加新的定義檔案。
下面的是 ClientRectList 在TypeScript庫檔案中的當前定義。
如果要為 ClientRectList新增一個新的isOrdered屬性,只需簡單的在你自己的程式中新增以下介面擴充套件程式,即可立即使用。
當它被新增到標準庫時,你自己的擴充套件會引發一個生成錯誤,到時把它刪除了就行了。
當你建立ClientRectList的例項時,你將能夠訪問過去的方法,以及獲取新的 isOrdered 屬性。
除了這些TypeScript標準庫的定義,TypeScript沒有捆綁任何其他的框架。在前一章中,我們談到了,你可以重建你的各種功能,比如像C#一樣,因為TypeScript有著豐富的語言功能。你也可以訪問所有現有的 JavaScript 框架,jQuery、Knockout、Angular和其他數以百計的框架。但是他們都用純 JavaScript開發,你不會得到增強的開發體驗,除非,你有一個匹配的定義檔案。幸運的是,有一個專案,他們致力於為所有的JavaScript框架提供TypeScript定義。Definitely Typed 專案,Github地址是:
https://github.com/borisyankov/DefinitelyTyped
建立定義
有時你會想使用JavaScript的庫、框架或者工具集。TypeScript編譯器難以理解這些外部的程式碼,因為他們沒有型別資訊。
動態定義
在程式中使用外部程式碼的最簡單方法是宣告一個any變數。TypeScript編譯器允許any物件呼叫任何方法於屬性。這會讓你通過編譯,但是沒有自動完成和型別檢查。
在此示例中的特殊關鍵字是declare。這會告訴編譯器,這個變數是外部的,這個僅僅是編譯時的關鍵字,編譯為JavaScript時會被擦除。
型別定義
為了使用外部JS程式碼能獲取更佳的開發體驗,你需要提供更全面的定義。可以宣告變數、 模組、 類和函式,定義外部程式碼的型別資訊。Declare關鍵字僅僅需要在定義的開頭使用一次。
如果希望外部程式碼可以由TypeScript擴充套件,定義為class,否則,定義為interface。
這兩種定義方法的唯一區別是能否繼承擴充套件。這兩種情況下,型別資訊都僅為編譯時使用,編譯為JavaScript後都會進行型別擦除。
很多時候,定義會組成一系列介面,組合成很大的一幅圖景。你甚至可以為TypeScript不能實現的JavaScript方法建立定義。
例如,它是如下圖這樣做:
在此示例中有一個名為move的不可思議的屬性,他可以作為函式使用。這樣一來,我們就可以宣告幾乎任何的JavaScript程式碼, jQuery、Knockout、Angular、RequireJS 和其他的老的JavaScript程式碼,讓你能在TypeScript中使用它們而不必重寫。
一些有用的小技巧
獲取執行時型別
如果在執行時,想要得到類的名稱,在C#中有反射這種方法,但TypeScript沒有明顯的內建方法。
靜態方法 getType,檢查編譯後的 JavaScript 函式,然後提取它的名字,這就是TypeScript中的類的名稱。
這種技術的限制是你不能獲得包含類和模組的完整名稱,這意味著:
MyModule.Example和 SomeOtherModule.Example 和沒包裝的叫做Example的類,它們所有的返回字串均為Example。
擴充套件原生物件
TypeScript內建的定義檔案描述了JavaScript原生物件和函式。你可以在lib.d.ts庫檔案中檢視它們。你會注意到大部分都是使用interface定義的,不希望你繼承它們。但我們還是可以擴充套件它們的。
例如,下面是給NodeList物件增加onclick事件。
但是這個程式碼會被TypeScript編譯器警告,告訴你,NodeList物件沒有onclick函式。
為了解決此為題,你需要在你自己的程式碼中新增定義程式碼。
這個介面宣告可以寫在任何引用的ts檔案中。
完