[TEAP早期試讀]《深入淺出CoffeeScript》CoffeeScript中的OOP
圖靈社群按:
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
的執行結果會被用作它的返回值。(當該返回值被使用時,就不能使用break
或return
語句。如果你非要這樣試試看,那你會得到一個像Syntax-Error: cannot include a pure statement in an expression
這樣的錯誤。用行話來說就是a=return x
沒有意義,因此編譯器不會允許存在這種可能性。)
CoffeeScript還做了一些語法上的改變,這在某種程度上提醒了JavaScript程式設計師注意那些隱藏著的差異:使用when
代替了case
,else
代替了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工程師也能幹得很出色——但是對於某些程式來說,類就顯得尤為適合。
說到這裡,還記得上一章程式中混亂的程式碼嗎?來看看我們能對它們做點什麼。
相關文章
- CoffeeScript和Sass提高Web開發效率Web
- 在 Laravel 中使用 TypeScript 、CoffeeScript 、pug 提高開發效率LaravelTypeScript
- 淺讀-《深入淺出Nodejs》NodeJS
- 深入淺出node讀書筆記筆記
- 精讀《深入淺出Node.js》Node.js
- 深入淺出FE(十四)深入淺出websocketWeb
- 深入淺出解讀 Java 虛擬機器的差別測試技術Java虛擬機
- 反射的深入淺出反射
- 深入淺出解讀 Spring 原始碼:IOC/AOP 篇Spring原始碼
- 深入淺出Seata的AT模式模式
- 深入淺出java的MapJava
- 深入淺出mongooseGo
- HTTP深入淺出HTTP
- 深入淺出WebpackWeb
- 深入淺出HTTPHTTP
- mysqldump 深入淺出MySql
- 深入淺出——MVCMVC
- 深入淺出IO
- 深入淺出decorator
- ArrayList 深入淺出
- 深入淺出 RabbitMQMQ
- 深入淺出PromisePromise
- 深入淺出 ZooKeeper
- 深入淺出 Runtime(六):相關面試題面試題
- 深入淺出 Java 中列舉的實現原理Java
- 深入淺出VACUUM核心原理(中): index by passIndex
- 【原創】【深入淺出系列】之程式碼可讀性
- JAVA重試機制多種方式深入淺出Java
- 學術乾貨|深入淺出解讀 Java 虛擬機器的差別測試技術Java虛擬機
- 深入淺出解析JVM中的Safepoint | 得物技術JVM
- Flutter | 深入淺出KeyFlutter
- 深入淺出 Laravel EchoLaravel
- 深入淺出理解ReduxRedux
- 深入淺出 Laravel MacroableLaravelMac
- flutter ScopedModel深入淺出Flutter
- 《深入淺出webpack》有感Web
- 深入淺出Spring MVCSpringMVC
- 深入淺出Tomcat系列Tomcat
- [譯] 深入淺出 SVGSVG