作爲編程語言的TypeScript
關於TypeScript,首先要認識的一點就是:它是Anders Hejlsberg的作品。Anders是第一流的編程語言設計師,也是第一流的編譯器
實現者。作爲Object Pascal和C#之父,Anders這次仍然採用了此前的做法:他設計了一種新的語言,並實現了這種語言的編譯器,
來改進一種已有的語言。但這次又和此前有所不同,此前無論是Object Pascal還是C#,編譯的目標代碼都是機器碼,而TypeScript
的目標代碼則是JavaScript。
當然,如果把瀏覽器看作是虛擬機,而JavaScript看作是在這種虛擬機上運行的目標代碼也無不可。總而言之,使用TypeScript這種
語言撰寫的源代碼需要經過TypeScript編譯器的編譯,而產生的目標代碼是標準的JavaScript。但這還不是TypeScript在語言設計層
面上的特別之處,特別之處有兩點。
TypeScript支持on-the-fly編譯,即寫一句TypeScript就可以立即得到對應的JavaScript代碼,這個特性和CoffeeScript類似。但它比
CoffeeScript支持更強的上下文推導,不需要完整的語句寫完,就可以生成對應的、不完整的JavaScript代碼。
TypeScript是JavaScript的超集(superset),“任何合法的JavaScript都是合法的TypeScript。”這種設計很明顯是借鑑了C++對於C
做擴充時採用的做法,它兼容已有的JavaScript代碼的決定給很多JavaScript程序員向TypeScript轉型時鋪就堅實的第一步——他們
可以從自己已有的代碼出發,通過一點一點的改動來體會到TypeScript帶來的好處,同時,時刻保留說“這樣就夠了”,然後停止的權
利。直到掌握了比較全面的TypeScript技術以後,才從一開始就採用TypeScript來撰寫代碼,而只取用編譯結果。實際上,“任何合
法的JavaScript都是合法的TypeScript”這種說法並不準確,準確的說法是“任何合法的ECMAScript 6都是合法的TypeScript”。當然,
ECMAScript 6還是一個正在修訂的語言規範,而TypeScript在現階段生成的任何目標代碼,涉及可能會引起ECMAScript 6的新特性
的,都採用了向下兼容的ECMAScript 5規範作爲準則。但對於各個瀏覽器自行對JavaScript做的那部分擴充,TypeScript不保證予
以支持。
TypeScript特性簡介
前面已說過,TypeScript的設計目標是作爲JavaScript或者說ECMAScript 6的超集。換句話說,如同C++的初始目標是作爲“更好的
C”一樣,TypeScript也可以看作是“更好的JavaScript”,那麼好在哪裏呢?其實用C++和C的關係來做類比,還是很恰當的。
TypeScript充分利用了JavaScript原有的對象模型並在此基礎上做了擴充,添加了較爲嚴格的類型檢查機制,添加了模塊支持和API
導出的能力。比起JavaScript,TypeScript提供了更多在語言層面上的支持,使得程序員能夠以更加標準化的語法來表達語義上的約
束,從而降低了程序出錯的機率;TypeScript也使得代碼組織和複用變得更加有序,使得開發大型Web應用有了一套標準方法。
對象模型的擴展
JavaScript支持極爲廣泛的對象模型,除了null和undefined以外,幾乎所有的其他實體都可以視爲對象,即使是數值、字符串和布爾
型亦可以隱式使用其對應的包裝器而直接作爲對象用於一般場合。函數和數組這樣在其他的編程語言中不被視爲對象的實體,在
JavaScript亦視爲“一等對象”(first-class object),除了利用實體本身的場合,例如使用數值索引或調用函數體以外,還可以作爲普
通的對象拿來添加屬性和方法。JavaScript對象支持在任意時刻動態添加屬性和方法,並且支持修改和擴充內建對象。一句話,
JavaScript提供了大量進行對象操作的基礎設施(facilities)和基本工具(utilities),正是這些內容構成了JavaScript豐富而靈活的
對象模型。TypeScript主要從兩個方面對JavaScript對象模型進行擴展:一是在覈心語言方面,二是在類概念的模塑方面。
聲明語義學
在TypeScript中書寫涉及DOM對象的JavaScript代碼,一般來說不會遇到問題。但這並不是因爲TypeScript語言中對DOM對象有
所“瞭解”,而是因爲TypeScript默認會加載名爲lib.d.ts的聲明文件,其中默認已包含了所有的DOM對象的聲明。換言之,當你寫下這
個語句:
之時,編譯器實際上已隱式地在最前面加上了一句:
這種聲明稱爲環境聲明(ambient declaration),遇到環境聲明以後,TypeScript便會試圖從聲明的來源(如聲明庫中)分析和推導
對象的類型信息。如果找不到任何的來源,它便默認該對象的類型爲any。但無論如何,環境聲明都不會向生成的JavaScript里加任
何語句。事實上,所有的TypeScript聲明都不會生成對應的JavaScript語句,因爲JavaScript對象模型中的聲明是可選的。這裏也可
以看出TypeScript遵循“除非必要,不生成多餘的語句”的哲學。但聲明在TypeScript中除了有着預先提供類型信息的重要作用之外,
編譯器還能根據這些信息完成強大的類型推導,以及精準的靜態類型檢查。
數據語義學
TypeScript中的數據要求帶有明確的類型,如果設定爲一種類型,卻要將該類型內不合法的值賦給它,則靜態類型檢查機制會將這
樣的語句標示爲錯誤。
可以採用interface關鍵字定義具名結構類型,這個特性類似於在JavaScript中採用字面量來定義JavaScript對象。所不同的是,在
TypeScript中每個組分都必須指定類型,不過組分可以是可選的,在實際提供字面量對象時,可選組分可以不提供。
同樣地,interface的數據類型定義,以及在TypeScript中爲數據指定的任何靜態類型聲明,都不會在生成任何的JavaScript目標代碼
有任何體現。例如上面這段代碼生成的JavaScript目標代碼僅僅是:
函數對象的類型主要由它的簽名式(signature)決定,包括各個形式參數的名字、類型和返回值類型。
值得注意的是,函數對象的返回值可以是void,這是void類型唯一可以出現的地方,而它的唯一可能取值是undefined。
函數語義學
TypeScript中的函數除了在JavaScript的函數對象模型的基礎上添加了靜態類型檢查,體現了函數的數據方面之外,還在函數本身的
性質上增加了不少新特性,例如函數缺省參數值:
它會對應地生成以下的JavaScript目標代碼,從這裏可以清晰地看到它生成代碼的邏輯是通過判斷參數有無定義來進行的:
TypeScript支持有限的函數重載,爲何說是有限的呢?因爲一般意義上的函數重載是根據函數簽名式的不同,在函數被實際調用時
根據實際參數的類型來綁定到特定的重載函數的。其背後的實現機制,大多數是所謂的名稱重整(name mangling)。但TypeScript
中的函數重載不能這樣做,它只支持能夠以共用實現體爲基礎的重載,無論聲明瞭多少個同名且簽名式不同的函數,它都只能有一
個實現體,且這個實現體必須對所有的重載版本都有意義。這樣說可能比較令人費解,
看個例子好了:
只要看一下上述TypeScript代碼生成的JavaScript目標代碼,就明白了大半:
原因就在於TypeScript在實現重載時並沒有使用名稱重整機制,而JavaScript又不支持重載,所以只能做出這樣非常大的折中方案
了。
TypeScript中最引人注目的一個函數對象特性是支持所謂的“箭頭記法”(arrow notation),即Lamda表達式。例如下面的三個函數是
等價的:
但箭頭記法最重要的用途還是在需要使用回調函數的場合。此時最易犯的一個錯誤就是this的作用域並非保留在被調用函數所在的局
部作用域,而成了函數調用方所在的作用域。還是通過一個例子來看:
此時,點擊頁面,彈出的警告框顯示的值是“NAN”,顯然有問題。而問題就出在這裏的this指的是函數調用方作用域,此處成了全局
作用域,結果自然不對。此時只要改用箭頭記法,問題就迎刃而解:
這個記法是ECMAScript 6引入的,查看一下TypeScript生成的目標代碼,就可以瞭解它是採用了迂迴的辦法在實現達到效果的同時
又保持向下的兼容性。
class和繼承語義學
TypeScript對JavaScript對象模型最重要的擴充,自然在於它補充了JavaScript中所沒有引入的“類”的概念。是的,在JavaScript中沒
有類,只有對象,要實現所謂的“類式操作”(classical operations),如封裝、多態等,要通過若干基礎設施,如原型、構造函數等
來完成。這些對於非常熟悉JavaScript的程序員來說,也許都是可以完成的任務,但對於新手來說就困難重重了。並且,即使是高
手,一段時間不寫相關的代碼也很容易遺忘和出錯。但TypeScript卻提供了標準的機制,將普通程序員熟悉的、C++和C#中常用的
類概念映射到JavaScript中去,這樣就大大降低了在JavaScript進行類式操作的難度。由於相關的概念理解起來並不困難,但技術內
容卻非常多,所以這裏只介紹幾點較關鍵的。
首先,用一句話來概括在TypeScript中class的核心語義:所有的class都是一個立即函數,所有的數據成員都是這個函數實例的屬
性,所有的方法都是這個函數原型的屬性,所
有的靜態成員都是這個函數的構造函數的屬性。各就各位,不會出錯。只要看一下class生成的JavaScript目標代碼就很明瞭,假設
有個根據工齡計算工資的Human類定義如下:
它生成的JavaScript目標代碼如下:
值得說明的是,TypeScript支持所謂的“存取器”。採用存取器,可以將函數封裝,並且以數據屬性的形式暴露出來。例如可以爲上述
類增加一個獲取工資數額的存取器:
對應的JavaScript代碼比較複雜,瀏覽器需要支持ECMAScript 5才能運行:
代碼組織和重構
TypeScript中引入了模塊的概念,這類似於C++中的名字空間。它可以把聲明、數據、函數和類封裝在模塊中,並採用export關鍵字
導出,供模塊外部的代碼取用。之所以說它和命名空間比較相似,一是因爲同名的模塊可以自動合併,甚至可以分別存儲在多個文
件中;二是因爲模塊的名字可以分成不同層次,在層次較多時還可以命名簡化的別名。但無論模塊怎麼組織,最終生成的還是標準
的、可直接取用的JavaScript代碼。正是靠着模塊化、可插拔的結構,TypeScript才得以在維護一個較小的語言核心的前提下,對廣
泛使用的庫如jQuery、CommonJS和Node.js等提供了完整的支持。由於TypeScript並不是採用字符串匹配的粗糙方式來推導變量和
函數的名字,對TypeScript代碼進行命名的重構就如同微軟的其他編程語言一樣容易。只需要選中要重新命名的實體,並鍵入新的
名字,而不需要擔心名字相同而意義不同的其他實體也被同時重命名了。
小結
TypeScript是現今所有對JavaScript的改進中,唯一完全兼容並作爲它的超集存在的解決方案。並且,TypeScript幾乎是改進了
JavaScript對象模型的方方面面,本文介紹的只是其中比較重要的一部分技術,還有很多細節還需要讀者自己去探索。現在,
TypeScript的最新版本是0.8.1,並且開放了全部的源代碼。很有意思的是,TypeScript本身就是用TypeScript實現的,這種遞歸式的
結構也是編譯器大牛們很鍾愛的方式之一,因爲當年Bjarne Stroustrup也用C++本身來寫C++編譯器。熟悉TypeScript源碼和規範不
僅讓我們可以更快地掌握這門新語言,也能夠更深入地瞭解如何利用它來解決一些更復雜的問題,例如如何擴充它來支持一些特定
的瀏覽器才提供的JavaScript特性等。總而言之,TypeScript可以說是最有前途的JavaScript擴展甚至替代的解決方案之一,有志於
前端技術的朋友們應該儘快地熟悉起來。