[TEAP早期試讀]《深入淺出CoffeeScript》CoffeeScript中的OOP

寸志發表於2012-02-26

圖靈社群按
TEAP是什麼?TEAP是Turingbook Early Access Program的簡稱,即早期試讀,它公佈的是圖靈在途新書未經編輯的內容。一本書的翻譯週期約為3到6個月,如果在翻譯過程中,譯者就能與讀者進行溝通和交流,對整本書的翻譯品質是有幫助的。通過TEAP,讀者可以提前閱讀將來才能出版的內容,譯者也能收穫寶貴的反饋意見,改進翻譯,提高質量。
本書原名為CoffeeScript Accelerated JavaScript Development,中文名暫定為《深入淺出CoffeeScript》,本篇內容節選自書中第4章模組與類的第三、四小節。
因本人系初次翻譯這種非常正式的技術書籍,難免有不妥之處,若有問題或意見建議,歡迎大家與我交流,我的郵箱:island205@gmail.com,也可以在微博上@

類:原型函式

CoffeeScript的類定義語法與它物件的定義語法很像。這並不是巧合,當你定義一個類時,其實你定義的就是一個物件。具體來講,你定義的是一個原型。如果你定義了constructor函式,那它是唯一不屬於原型的屬性。
讓我們看一個例子,以闡明一個眾所周知的事實:單個毛球族製造的麻煩與全部毛球族的數量成正比:

Classes/Tribble.coffee

class Tribble
  constructor: ->
    @isAlive = true
     Tribble.count++

   # Prototype properties
   breed: -> new Tribble if @isAlive
   die: ->
     Tribble.count-- if @isAlive
     @isAlive = false

   # Class-level properties
   @count: 0
   @makeTrouble: -> console.log ('Trouble!' for i in [1..@count]).join(' ')

這裡有很多新語法,讓我們分塊討論。
每次新建一個毛球族時,Trible.count就加1(在這裡我們可以將其稱為@count,因為在類內部的this值就是類本身)。當呼叫Trible.makeTrouble()時,它會輸出Trible.count次“Trouble”。
測試一下:

Classes/Tribble.coffee

tribble1 = new Tribble
tribble2 = new Tribble
Tribble.makeTrouble() # "Trouble! Trouble!"

注意,在Tribble類的上下文中可以用@count來訪問Tribble.count,但在Tribble的方法中卻不可以。咋一看可能會有點莫名其妙,但是別忘記我們涉及到三個物件:Tribble物件本身(實際上就是contructor函式),Tribble.prototype,還有Tribble的例項。預設地,Tribble的屬性(除去contructor之外的)都會附加到原型上。當使用@字首時,就是明確表示我們想把該屬性新增到類物件本身上去 。
因為新增到原型上的函式(包括構造器)都是以各自的物件作為上下文被呼叫的,在這些函式中有@字首的變數指向的都是例項屬性。這就是我們在建構函式中定義@isAlive的原因:我們需要為每個例項新增各自的@isAlive屬性。然後我們就可以這樣做:

Classes/Tribble.coffee

tribble1.die()
Tribble.makeTrouble() # "Trouble!"

由於有if @isAlive的檢查,再次殺死tribble1就沒什麼影響了。而且眾所周知,毛球族(tribbles)是胎生的,因此要不了多久就會有新的生物注入到我們的程式中:

Classes/Tribble.coffee

tribble2.breed().breed().breed()
Tribble.makeTrouble() # "Trouble! Trouble! Trouble! Trouble!"

使用“extends”來繼承

到目前為止,我們討論了原型是如何使得在大量物件之間共享各種功能變得容易的,以及CoffeeScript的類如何提供一個有用的語法來將原型屬性綁到一起。如果這就是類所有能做的事情,那它似乎沒多大用處。但是當我們想使用繼承時類就會真正地閃閃發光起來。
JavaScript是通過某個稱為“原型鏈”的東西來實現繼承的。假設,A的原型B有自己的原型C。然後我們寫了這樣的程式碼:

a = new A
console.log a.flurb()

首先,執行時檢視這個特殊的類A的例項a上是否有一個flurb的屬性;如果沒有則檢視A的原型B;如果還是沒找到,則它繼續檢視B的原型C。簡而言之,它會遍歷整個原型鏈。
如果C上也沒有flurb會怎麼樣?那執行時會檢查原始物件的原型(即{}的原型)。也就是說,每個物件都繼承了{}的原型,但是之間可能會包含其他原型。
所有這些把原型給原型再給原型的賦值會變得有些混亂。這就是CoffeeScript中需要extends的原因。
我們做個申明:

class B extends A

然後B的原型就繼承自A的原型,另外還把A的類屬性拷貝給了B。因此如果我們現在不再繼續定義B,B的例項將有和A例項完全一樣的行為。(有一個例外:B.name是“B”而A.name是“A”——name是一個特殊的屬性。)
讓我們來看一個稍微深入點的例子:

class Pet
   constructor: -> @isHungry = true
   eat: -> @isHungry = false

class Dog extends Pet
eat: ->
  console.log '*crunch, crunch*'
   super()
fetch: ->
  console.log 'Yip yip!'
   @isHungry = true

Dog繼承了Pet的構造器,這意味著狗狗們生來就是餓的。當小狗吃東西時,它會發出一些聲音然後呼叫super()super()的意思是“呼叫父類同名的方法。”(精確地說就是Pet::eat.call this。)然後這隻狗就不餓了。
如果在子類上定義了一個建構函式,那它會覆蓋父類的建構函式。不過它隨時都可以用super()來呼叫父類的建構函式。在子類建構函式開始時就呼叫一下super()(或者更有可能用的super——參看“super”不是“super”,67頁。)通常是明智之舉。
信不信由你,你已經知道了關於類的所有需要了解的知識。由於這些東西都是基於CoffeeScript的,所以語法可能與JavaScript有較大差別,但是編譯後的程式碼是簡單易懂的。如果你是一個傳統OOP(物件導向程式設計)方法論的死忠,那下面這小節就是為你而設的了。

多型與轉型
類的一大應用就是多型。多型是一個高階的物件導向程式設計的術語——“某個物件可以被當作幾種物件,但不是全部種類的物件”。下面是一個典型的例子:

class Shape
   constructor: (@width) ->
   computeArea: -> throw new Error('I am an abstract class!')

class Square extends Shape
   computeArea: -> Math.pow @width, 2

class Circle extends Shape
   radius: -> @width / 2
   computeArea: -> Math.PI * Math.pow @radius(), 2

showArea = (shape) ->
   unless shape instanceof Shape
     throw new Error('showArea requires a Shape instance!')
   console.log shape.computeArea()

showArea new Square(2) # 4
showArea new Circle(2) # pi

注意到函式showArea會檢查傳入的物件是否是一個Shape的例項(使用instanceof關鍵字),但是它並不關心給它的是何種形狀(Shape)。Square或者Circle例項都行。儘管這只是一個小示例,但是很難想象一個豐富的幾何庫不採用這種方式。


“super”不是“super()”

下面的程式碼有什麼問題?

class Appliance
   constructor: (warranty) ->
     warrantyDb.save(this) if warranty

class Toaster extends Appliance
   constructor: (warranty) ->
     super()

當我們建立一個新的Toaster時,super()沒有照樣傳遞warranty引數而是直接呼叫父類的建構函式,這意味著新的烤麵包機(toaster)並不會被儲存到擔保(warranty)資料庫中。
我們可以使用super(warranty)來解決這個問題,同時也可以用另一種簡寫方式:super。沒有括號也沒有引數的super會傳遞當前函式的所有引數。如果你是個Ruby程式設計師,這一定很眼熟。如果不是,那你就把super想象為一個非常非常貪婪的關鍵字吧——如果你沒有告訴它你想傳遞哪些引數,它會傳遞所有的引數。


如果我們不使用instanceof檢查,這就會變成著名的“鴨子型別”(意思是,“如果它看起來像一隻鴨子……” )。就算相關物件沒有computeArea方法,我們也總還是能得到一條有意義的錯誤資訊。雖然鴨子型別很好,但是總有那麼些時候你就是想要確定某個特定物件是否是如你所想的一樣。
在更加經典的物件導向的語言中,有一種結合switch來使用多型的慣用語法。我們還沒有討論過CoffeeScript中的switch,它與JavaScript中的switch有數處差別:首先,它在每個分句之間都有隱式的打斷(break) 以防止意外的“落空”(fallthrough) ;其次,switch的執行結果會被用作它的返回值。(當該返回值被使用時,就不能使用breakreturn語句。如果你非要這樣試試看,那你會得到一個像Syntax-Error: cannot include a pure statement in an expression這樣的錯誤。用行話來說就是a=return x沒有意義,因此編譯器不會允許存在這種可能性。)
CoffeeScript還做了一些語法上的改變,這在某種程度上提醒了JavaScript程式設計師注意那些隱藏著的差異:使用when代替了caseelse代替了default。單個when後面可以跟幾個潛在的匹配(matches),匹配之間用逗號隔開。同樣,作為:的替代,也可以使用縮排 (或者then)把匹配分句與它們產生的結果隔開。
下面是如何把它們合併到一個工廠函式中去的示例:

requisitionStarship = (captain) ->
   switch captain
      when 'Kirk', 'Picard', 'Archer'
       new Enterprise()
     when 'Janeway'
       new Voyager()
     else
       throw new Error('Invalid starship captain')

關於模組和類我們就討論這些。你只要記住:CoffeeScript絕不會要求你必須使用類或者使用經典的物件導向的設計模式——畢竟,不使用它們,大多數JavaScript工程師也能幹得很出色——但是對於某些程式來說,類就顯得尤為適合。
說到這裡,還記得上一章程式中混亂的程式碼嗎?來看看我們能對它們做點什麼。

相關文章