- OOP。ES4新加了interface和class。慣用prototype+closure模擬各式OOP的老大多半惡向膽邊生了。熟讀SICP一類地下刊物的老大們也多半輕蔑滴拋出了殺手級名言:Object is just poor man’s closure。其實我挺歡迎這些特性。幾乎每套 流行 框架都要用模擬OOP。不用流行類庫的老大們寫的第一段代碼也多半如下:
function makeClass(){
return function(args){
if ( this instanceof arguments.callee ) {
if ( typeof this.init == "function" )
this.init.apply( this, args.callee ? args : arguments );
} else
return new arguments.callee( arguments );
};
}
- 繼承用extends關鍵詞。實現interface用implements關鍵詞。常見的關鍵詞static, final, override 通通支持。用慣Java和C#的老大可以笑了。
- 用關鍵詞dynamic修飾的類裏可以在runtime添加活刪改屬性。支持getter和setter,所以Ruby和C#的老大們可以興奮了。通過關鍵字meta支持類似Ruby的method_missing()方法。這點相當不錯。好多Ruby裏流行的慣用法,尤其是用來實現DSL的慣用法可以派上用場了。
- 類的屬性支持三種不同的修飾:DontDelete :該屬性固定,不能被動態移除; DontEnum:該屬性不會在for(var p in object)的循環中出現; 和ReadOnly:該屬性的值是常數。一旦確定,不能更該。普通class裏的屬性是DontDelete和DontEnum。用dynamic class申明的動態類的屬性既非DontDelete,也非DontEnum。
- Generic function。也就是人們常說的multimethod,或者multiple dispatch method。這對用慣了C++操作符重載,CLOS和Dylan裏generic method的老大們來說,正是重大利好消息啊。Generic function不是新鮮概念。1986年的OOPSLA會議上,一幫LISP Hacker提交了著名的論文CommonLoops – Merging Lisp and Object-Oriented Programming,裏面正式提到了multimethod這個術語。據CLOS的作者Gregor Kiczales(擁護AOP的老大們應該覺得很親切吧?)回憶,他的合作者Larry Masinter最早合成了multimethod這個詞。流行的OO語言,比如Java/C#/C++/Python/Ruby什麼的,通常只支持單分派方法。也就是說方法分派只與方法的調用者或者某一個參數(比如python裏第一個參數self)有關。當我們看到caller.foo()時,我們就知道運行時調用的是對象caller裏的方法foo()。可惜現實世界裏的關係沒有那麼簡單。Wikipedia用物體碰撞作例子。當我們想實現兩類物體的碰撞時,到底在哪裏實現我們的方法呢?比如說,我們想讓飛船同隕石碰撞,到底把collide()方法放到spaceship裏,還是放到asteroid裏呢?如果要限定碰撞的類別呢?比如說友方飛船不能碰撞,但可以同敵方飛船碰撞呢?如果我們希望多個物體碰撞呢?如果我們的代碼寫好後,允許代碼的用戶添加新的物體和碰撞過程,該怎麼辦呢?Generic function就是用來解決這類多分派問題的。運行時調用generic function時,會根據該函數的*所有*參數決定分派對象。總的規則是越具體的類型佔用越高的優先級。比如說foo(Number)比foo(Object)有更高的優先級,因爲Number是Object的子類,比Object具體。另外,generic 函數裏所有參數的分派權重一樣,所謂的對稱多分派。Groovy採用了不對稱多分派。系統會先比較第一個參數。如果不能決定,再比較第二個。。。
再看個常用的Visitor模式。我們用普通的表達式處理作例子。
interface VisitableExpression{
function accept(visitor: ExpressionVisitor); //是滴,ES4開始支持靜態類型申明瞭
}
class AddExpression implements VisitableExpression{
var lefOperand;
var rightOperand;
function accept(visitor:ExpressionVisitor){
visitor.visitAddExpression(this);
}
}
class IntExpression implements VisitableExpression{
var value;
function accept(visitor:ExpressionVisitor){
visitor.visitIntExpression(this);
}
}
interface ExpressionVisitor{
function visitAddExpression(expression:VisitableExpression);
function visitIntExpression(expression: :VisitableExpression);
}
- 繼承用extends關鍵詞。實現interface用implements關鍵詞。常見的關鍵詞static, final, override 通通支持。用慣Java和C#的老大可以笑了。
這段代碼問題不少。添加新的Visitor容易了。比如說求值Visitor,打印Visitor,或者後門注入Visitor。但添加新的ComplexExpression呢?這下每一個現有的Visitor都要改動。如果我想改動ExpressionVisitor裏方法的簽名呢—本來是件很美好的事,非被VisitableExpression的僵硬結構搞成醜聞。有了generic function,Visitor模式基本消失:
generic function evaluate(e: IntExpression){...}
generic function collide(a, b);
generic function collide(f: FriendSpaceship, e: EnemySpaceship){...}
generic function collide(as: [asteroid], p: Planet){...} //一堆隕石同行星碰撞
generic function convert(token:Token, type:Char, target:ASCII){…}
new Complex( a.real + b.real, a.imag + b.imag )
generic intrinsic function +( a: Complex, b: AnyNumber )
a + Complex(b)
generic intrinsic function +( a: AnyNumber, b: Complex )
Complex(a) + b
- 類型系統。當然OOP裏提到的class和interface也是類型系統的一部分(所謂的Nominal Type),但它們用的太廣泛,就單獨提出來聊了。
- 加入了Record和Array類型。{a:int, b:char}是個Record。[int]是整數數組。是滴,ES4引入了int和char,不再像以前一樣,浮點數和string包辦一切了。不光如此,ES4也引入了Wrapper。比如boolean的wrapper是Boolean,double的wrapper是Double。這點俺其實不理解。當初Java區分primitive和wrapper類型是處於性能的考慮。ES4有必要這麼做麼?
- Annotation。也就是前面看到的類型申明。比如var a:int;表示申明瞭類型爲int的變量a。類型申明是可選的。函數參數,變量申明,函數返回值,常數申明時,都可以加上類型標註。這和普通的靜態語言沒有什麼區別。這保證了ES3裏沒有類型申明的代碼同ES4兼容。可選類型支持所謂的漸進式開發。我們可以從完全動態的程序開始,隨着業務模型的穩定逐漸加入類型申明。編譯期的類型系統也有助於IDE開發,編譯器驗證代碼,文檔生成。。。好處還是很多的。
- 函數類型。function (this:C, int): boolean代表了C類裏接受int參數,返回boolean的函數。這個有什麼用呢?用ES3的時候,我們常常通過傳遞匿名函數來
實現輕量級取代Strategy模式。問題是,所有匿名函數都是一個類型:Function。有了函數類型,我們就可以限制可以接受的函數了。這點想必也受Haskell/ML老大們的青睞。不過申明類型時寫那麼一長串也是自虐,所以類型定義粉墨登場:
- 加入了Record和Array類型。{a:int, b:char}是個Record。[int]是整數數組。是滴,ES4引入了int和char,不再像以前一樣,浮點數和string包辦一切了。不光如此,ES4也引入了Wrapper。比如boolean的wrapper是Boolean,double的wrapper是Double。這點俺其實不理解。當初Java區分primitive和wrapper類型是處於性能的考慮。ES4有必要這麼做麼?
- Type definition。類型定義類似C/C++裏的typedef。我們用關鍵詞type定義新的類型:Type IntComparator function(a: int, b:int): int。這段代碼定義了一個新的函數類型IntComparator。如果我們寫了一個給整數數組排序的函數sortInt(intArray: [int], comparator:IntComparator),就不用擔心接受一個比較字符串的函數了。ES4的typedef比C/C++的要強大。任何一個type申明都同時定義了該類型的元數據。元數據實現了meta-object interface,據在運行時通過反射讀取。同時,定義的類型可以嵌套,但不允許遞歸(包括交互遞歸)。比如說我們定義類型Person:type Person = { name:{ last:string, first:string }, born:Date, spouse:* }。如果spouse的類型是Person,現在ES4環境運行時會陷入死循環,吃掉大量內存。將來多半編譯器會報錯。下圖的run.exe就是ES4的執行環境。
有了類型,怎麼能沒有子類型(subtype)呢?S <: T表示類型S是類型T的子類型。子類型有一堆判斷規則。有興趣地可以到白皮書第16頁觀賞。另外,有了類型,自然得考慮類型間的轉換。於是兩個強大的關鍵詞,like(慣使Eiffel的老大可以笑了)和wrap粉墨登場。關鍵字like用於弱化的類型判別,頗有模式匹配的風格。可以申明var v: like { x: int, y: int }。那任何形如{int, int}類型的值都可以賦給變量v。比如說var p:Point, 那v = p是合法的賦值語句。like也可以做爲操作符試用,比如說{a:10, b:20} is like Point會返回true。這好比數據的duck typing。我們不管值的類型是什麼,只要關心它的“形狀”。這樣的話,我們既可以設定數據的結構,又不用被數據的類型限制得太死。關鍵詞wrap同like類似,但把變量的類型強行“包裝”被比較的類型。比如下面的代碼裏,變量v的類型就轉換爲{x:int, y:int}的類型:
var v: wrap { x: int, y: int } = { x: 10, y: 20 }
var w: { x: int, y: int } = v
當作操作符用時,給定兩變量v和t,如果v is t返回true,則 v wrap t返回變量v,不然如果v is like t爲真,則返回一個新對象o, 使得v is o爲真。再不然就拋出TypeError的異常羅。可以想象這兩個操作符會衍生出許多靈活的應用。有興趣的可以到這裏看更多的例子。 - Parametric Types。也就是我們說的類型模板,或者泛型。比如下面的代碼創建了一個容納任何類型的Pair:class Pair.<T> {
var first: T, second: T
}
函數,類,接口,和類型都支持參數類型。類裏又能申明函數。衛生問題就出現了。比如下面的代碼裏,變量x的類型到底是什麼?
class Pair.<T> {
var first: T, second: T;
function f.<T>(){
var x: T;
}
}
所以這時用類型定義就派上用場了:
class Pair.<T> {
var first: T, second: T;
Type XT = T;
function f.<T>(){
var x: XT;
}
}
下面插播一首詩:衛生問題在支持宏的語言裏非常重要。《Beautiful Code》裏專門有一章討論它。強烈推薦。