# 2類和對象
### 對象的鏈式調用
### 問題
你想調用一個對象上的多個方法,但不想每次都引用該對象。
### 解決方案
在每次鏈式調用后返回 this(即@)對象
~~~
class CoffeeCup
constructor: ->
@properties=
strength: 'medium'
cream: false
sugar: false
strength: (newStrength) ->
@properties.strength = newStrength
this
cream: (newCream) ->
@properties.cream = newCream
this
sugar: (newSugar) ->
@properties.sugar = newSugar
this
?
morningCup = new CoffeeCup()
?
morningCup.properties # => { strength: 'medium', cream: false, sugar: false }
?
eveningCup = new CoffeeCup().strength('dark').cream(true).sugar(true)
?
eveningCup.properties # => { strength: 'dark', cream: true, sugar: true }
~~~
### 討論
jQuery 庫使用類似的手段從每一個相似的方法中返回選擇符對象,并在后續方法中通過調整選擇的范圍修改該對象:
~~~
$('p').filter('.topic').first()
~~~
對我們自己對象而言,一點點元編程就可以自動設置這個過程并明確聲明返回 this 的意圖。
~~~
addChainedAttributeAccessor = (obj, propertyAttr, attr) ->
obj[attr] = (newValues...) ->
if newValues.length == 0
obj[propertyAttr][attr]
else
obj[propertyAttr][attr] = newValues[0]
obj
?
class TeaCup
constructor: ->
@properties=
size: 'medium'
type: 'black'
sugar: false
cream: false
addChainedAttributeAccessor(this, 'properties', attr) for attr of @properties
?
earlgrey = new TeaCup().size('small').type('Earl Grey').sugar('false')
?
earlgrey.properties # => { size: 'small', type: 'Earl Grey', sugar: false }
?
earlgrey.sugar true
?
earlgrey.sugar() # => true
~~~
### 類方法和實例方法
### 問題
你想創建類和實例的方法。
### 解決方案
#### 類方法
~~~
class Songs
@_titles: 0 # Although it's directly accessible, the leading _ defines it by convention as private property.
?
@get_count: ->
@_titles
?
constructor: (@artist, @title) ->
@constructor._titles++ # Refers to <Classname>._titles, in this case Songs.titles
?
Songs.get_count()
# => 0
?
?
song = new Songs("Rick Astley", "Never Gonna Give You Up")
Songs.get_count()
# => 1
?
?
song.get_count()
# => TypeError: Object <Songs> has no method 'get_count'
~~~
#### 實例方法
~~~
class Songs
_titles: 0 # Although it's directly accessible, the leading _ defines it by convention as private property.
?
get_count: ->
@_titles
?
constructor: (@artist, @title) ->
@_titles++
?
song = new Songs("Rick Astley", "Never Gonna Give You Up")
song.get_count()
# => 1
?
?
Songs.get_count()
# => TypeError: Object function Songs(artist, title) ... has no method 'get_count'
~~~
### 討論
Coffeescript 會在對象本身中保存類方法(也叫靜態方法),而不是在對象原型中(以及單一的對象實例),在保存了記錄的同時也將類級的變量保存在中心位置。
### 類變量和實例變量
### 問題
你想創建類變量和實例變量(屬性)。
### 解決方案
#### 類變量
~~~
class Zoo
@MAX_ANIMALS: 50
MAX_ZOOKEEPERS: 3
?
helpfulInfo: =>
"Zoos may contain a maximum of #{@constructor.MAX_ANIMALS} animals and #{@MAX_ZOOKEEPERS} zoo keepers."
?
Zoo.MAX_ANIMALS
# => 50
?
?
Zoo.MAX_ZOOKEEPERS
# => undefined (it is a prototype member)
?
?
Zoo::MAX_ZOOKEEPERS
# => 3
?
?
zoo = new Zoo
zoo.MAX_ZOOKEEPERS
# => 3
?
zoo.helpfulInfo()
# => "Zoos may contain a maximum of 50 animals and 3 zoo keepers."
?
?
zoo.MAX_ZOOKEEPERS = "smelly"
zoo.MAX_ANIMALS = "seventeen"
zoo.helpfulInfo()
# => "Zoos may contain a maximum of 50 animals and smelly zoo keepers."
~~~
#### 實例變量
你必須在一個類的方法中才能定義實例變量(例如屬性),在 constructor 結構中初始化你的默認值。
~~~
class Zoo
constructor: ->
@animals = [] # Here the instance variable is defined
?
addAnimal: (name) ->
@animals.push name
?
?
zoo = new Zoo()
zoo.addAnimal 'elephant'
?
otherZoo = new Zoo()
otherZoo.addAnimal 'lion'
?
zoo.animals
# => ['elephant']
?
?
otherZoo.animals
# => ['lion']
~~~
### 警告!
不要試圖在 constructor 外部添加變量(即使在 [elsewhere](http://arcturo.github.io/library/coffeescript/03_classes.html#content) 中提到了,由于潛在的 JavaScript 的原型概念,這不會像預期那樣運行正確)。
~~~
class BadZoo
animals: [] # Translates to BadZoo.prototype.animals = []; and is thus shared between instances
?
addAnimal: (name) ->
@animals.push name # Works due to the prototype concept of Javascript
?
?
zoo = new BadZoo()
zoo.addAnimal 'elephant'
?
otherZoo = new BadZoo()
otherZoo.addAnimal 'lion'
?
zoo.animals
# => ['elephant','lion'] # Oops...
?
?
otherZoo.animals
# => ['elephant','lion'] # Oops...
?
?
BadZoo::animals
# => ['elephant','lion'] # The value is stored in the prototype
~~~
### 討論
Coffeescript 會將類變量的值保存在類中而不是它定義的原型中。這在定義類中的變量時是十分有用的,因為這不會被實體屬性變量重寫。
### 克隆對象(深度復制)
### 問題
你想復制一個對象,包含其所有子對象。
### 解決方案
~~~
clone = (obj) ->
if not obj? or typeof obj isnt 'object'
return obj
?
if obj instanceof Date
return new Date(obj.getTime())
?
if obj instanceof RegExp
flags = ''
flags += 'g' if obj.global?
flags += 'i' if obj.ignoreCase?
flags += 'm' if obj.multiline?
flags += 'y' if obj.sticky?
return new RegExp(obj.source, flags)
?
newInstance = new obj.constructor()
?
for key of obj
newInstance[key] = clone obj[key]
?
return newInstance
?
x =
foo: 'bar'
bar: 'foo'
?
y = clone(x)
?
y.foo = 'test'
?
console.log x.foo isnt y.foo, x.foo, y.foo
# => true, bar, test
~~~
### 討論
通過賦值來復制對象與通過克隆函數來復制對象的區別在于如何處理引用。賦值只會復制對象的引用,而克隆函數則會:
- 創建一個全新的對象
- 這個新對象會復制原對象的所有屬性,
- 并且對原對象的所有子對象,也會遞歸調用克隆函數,復制每個子對象的所有屬性。
下面是一個通過賦值來復制對象的例子:
~~~
x =
foo: 'bar'
bar: 'foo'
?
y = x
?
y.foo = 'test'
?
console.log x.foo isnt y.foo, x.foo, y.foo
# => false, test, test
~~~
顯然,復制之后修改 y 也就修改了 x。
### 類的混合
### 問題
你有一些通用方法,你想把他們包含到很多不同的類中。
### 解決方案
使用 mixOf 庫函數,它會生成一個混合父類。
~~~
mixOf = (base, mixins...) ->
class Mixed extends base
for mixin in mixins by -1 #earlier mixins override later ones
for name, method of mixin::
Mixed::[name] = method
Mixed
?
...
?
class DeepThought
answer: ->
42
?
class PhilosopherMixin
pontificate: ->
console.log "hmm..."
@wise = yes
?
class DeeperThought extends mixOf DeepThought, PhilosopherMixin
answer: ->
@pontificate()
super()
?
earth = new DeeperThought
earth.answer()
# hmm...
?
# => 42
~~~
### 討論
這適用于輕量級的混合。因此你可以從基類和基類的祖先中繼承方法,也可以從混合類的基類和祖先中繼承,但是不能從混合類的祖先中繼承。與此同時,在聲明了一個混合類后,此后的對這個混合類進行的改變是不會反應出來的。
### 創建一個不存在的對象字面值
### 問題
你想初始化一個對象字面值,但如果這個對象已經存在,你不想重寫它。
### 解決方案
使用存在判斷運算符(existential operator)。
~~~
?
window.MY_NAMESPACE ?= {}
~~~
### 討論
這行代碼與下面的 JavaScript 代碼等價:
~~~
?
window.MY_NAMESPACE = window.MY_NAMESPACE || {};
~~~
這是 JavaScript 中一個常用的技巧,即使用對象字面值來定義命名空間。這樣先判斷是否存在同名的命名空間然后再創建,可以避免重寫已經存在的命名空間。
### CoffeeScrip 的 type 函數
### 問題
你想在不使用 typeof 的情況下知道一個函數的類型。(要了解為什么 typeof 不靠譜,請參見 [http://javascript.crockford.com/remedial.html](http://javascript.crockford.com/remedial.html)。)
### 解決方案
使用下面這個type函數
~~~
type = (obj) ->
if obj == undefined or obj == null
return String obj
classToType = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regexp',
'[object Object]': 'object'
}
return classToType[Object.prototype.toString.call(obj)]
~~~
### 討論
這個函數模仿了 jQuery 的 [$.type函數](http://api.jquery.com/jQuery.type/)。
需要注意的是,在某些情況下,只要使用鴨子類型檢測及存在運算符就可以不必檢測對象的類型了。例如,下面這行代碼不會發生異常,它會在 myArray 的確是數組(或者一個帶有 push 方法的類數組對象)的情況下向其中推入一個元素,否則什么也不做。
~~~
myArray?.push? myValue
~~~