# 4數組
### 檢查變量的類型是否為數組
### 問題
你希望檢查一個變量是否為一個數組。
~~~
myArray = []
console.log typeof myArray // outputs 'object'
~~~
“typeof” 運算符為數組輸出了一個錯誤的結果。
### 解決方案
使用下面的代碼:
~~~
typeIsArray = Array.isArray || ( value ) -> return {}.toString.call( value ) is '[object Array]'
~~~
為了使用這個,像下面這樣調用 typeIsArray 就可以了。
~~~
myArray = []
typeIsArray myArray // outputs true
~~~
### 討論
上面方法取自 "the Miller Device"。另外一個方式是使用 Douglas Crockford 的片段。
~~~
typeIsArray = ( value ) ->
value and
typeof value is 'object' and
value instanceof Array and
typeof value.length is 'number' and
typeof value.splice is 'function' and
not ( value.propertyIsEnumerable 'length' )
~~~
### 將數組連接
### 問題
你希望將兩個數組連接到一起。
### 解決方案
在 JavaScript 中,有兩個標準方法可以用來連接數組。
第一種是使用 JavaScript 的數組方法 concat():
~~~
array1 = [1, 2, 3]
array2 = [4, 5, 6]
array3 = array1.concat array2
# => [1, 2, 3, 4, 5, 6]
~~~
需要指出的是 array1 沒有被運算修改。連接后形成的新數組的返回值是一個新的對象。
如果你希望在連接兩個數組后不產生新的對象,那么你可以使用下面的技術:
~~~
array1 = [1, 2, 3]
array2 = [4, 5, 6]
Array::push.apply array1, array2
array1
# => [1, 2, 3, 4, 5, 6]
~~~
在上面的例子中,Array.prototype.push.apply(a, b) 方法修改了 array1 而沒有產生一個新的數組對象。
在 CoffeeScript 中,我們可以簡化上面的方式,通過給數組創建一個新方法 merge():
~~~
Array::merge = (other) -> Array::push.apply @, other
?
array1 = [1, 2, 3]
array2 = [4, 5, 6]
array1.merge array2
array1
# => [1, 2, 3, 4, 5, 6]
~~~
另一種方法,我可以直接將一個 CoffeeScript splat(array2) 放入 push() 中,避免了使用數組原型。
~~~
array1 = [1, 2, 3]
array2 = [4, 5, 6]
array1.push array2...
array1
# => [1, 2, 3, 4, 5, 6]
~~~
一個更加符合語言習慣的方法是在一個數組語言中直接使用 splat 運算符(...)。這可以用來連接任意數量的數組。
~~~
array1 = [1, 2, 3]
array2 = [4, 5, 6]
array3 = [array1..., array2...]
array3
# => [1, 2, 3, 4, 5, 6]
~~~
### 討論
CoffeeScript 缺少了一種用來連接數組的特殊語法,但是 concat() 和 push() 是標準的 JavaScript 方法。
### 由數組創建一個對象詞典
### 問題
你有一組對象,例如:
~~~
cats = [
{
name: "Bubbles"
age: 1
},
{
name: "Sparkle"
favoriteFood: "tuna"
}
]
~~~
但是你想讓它像詞典一樣,可以通過關鍵字訪問它,就像使用 cats["Bubbles"]。
### 解決方案
你需要將你的數組轉換為一個對象。通過這樣使用 reduce:
~~~
# key = The key by which to index the dictionary
?
Array::toDict = (key) ->
@reduce ((dict, obj) -> dict[ obj[key] ] = obj if obj[key]?; return dict), {}
~~~
使用它時像下面這樣:
~~~
catsDict = cats.toDict('name')
catsDict["Bubbles"]
# => { age: 1, name: "Bubbles" }
~~~
### 討論
另一種方法是使用數組推導:
~~~
Array::toDict = (key) ->
dict = {}
dict[obj[key]] = obj for obj in this when obj[key]?
dict
~~~
如果你使用 Underscore.js,你可以創建一個 mixin:
~~~
_.mixin toDict: (arr, key) ->
throw new Error('_.toDict takes an Array') unless _.isArray arr
_.reduce arr, ((dict, obj) -> dict[ obj[key] ] = obj if obj[key]?; return dict), {}
catsDict = _.toDict(cats, 'name')
catsDict["Sparkle"]
# => { favoriteFood: "tuna", name: "Sparkle" }
~~~
### 由數組創建一個字符串
### 問題
你想由數組創建一個字符串。
### 解決方案
使用 JavaScript 的數組方法 toString():
~~~
["one", "two", "three"].toString()
# => 'one,two,three'
~~~
### 討論
toString() 是一個標準的 JavaScript 方法。不要忘記圓括號。
### 定義數組范圍
### 問題
你想定義一個數組的范圍。
### 解決方案
在 CoffeeScript 中,有兩種方式定義數組元素的范圍。
~~~
myArray = [1..10]
# => [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
~~~
~~~
myArray = [1...10]
# => [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
~~~
要想反轉元素的范圍,則可以寫成下面這樣。
~~~
myLargeArray = [10..1]
# => [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]
~~~
~~~
myLargeArray = [10...1]
# => [ 10, 9, 8, 7, 6, 5, 4, 3, 2 ]
~~~
### 討論
包含范圍以 “..” 運算符定義,包含最后一個值。 排除范圍以 “...” 運算符定義,并且通常忽略最后一個值。
### 篩選數組
### 問題
你想要根據布爾條件來篩選數組。
### 解決方案
使用 `Array.filter (ECMAScript 5): array = [1..10]`
~~~
array.filter (x) -> x > 5
# => [6,7,8,9,10]
~~~
在 EC5 之前的實現中,可以通過添加一個篩選函數擴展 Array 的原型,該函數接受一個回調并對自身進行過濾,將回調函數返回 true 的元素收集起來。
~~~
# 擴展 Array 的原型
?
Array::filter = (callback) ->
element for element in this when callback(element)
?
array = [1..10]
?
# 篩選偶數
?
filtered_array = array.filter (x) -> x % 2 == 0
# => [2,4,6,8,10]
?
?
# 過濾掉小于或等于5的元素
?
gt_five = (x) -> x > 5
filtered_array = array.filter gt_five
# => [6,7,8,9,10]
~~~
### 討論
這個方法與 Ruby 的 Array 的 #select 方法類似。
### 列表推導
### 問題
你有一個對象數組,想將它們映射到另一個數組,類似于 Python 的列表推導。
### 解決方案
使用列表推導,但不要忘記還有 [mapping-arrays](http://coffeescript-cookbook.github.io/chapters/arrays/mapping-arrays) 。
~~~
electric_mayhem = [ { name: "Doctor Teeth", instrument: "piano" },
{ name: "Janice", instrument: "lead guitar" },
{ name: "Sgt. Floyd Pepper", instrument: "bass" },
{ name: "Zoot", instrument: "sax" },
{ name: "Lips", instrument: "trumpet" },
{ name: "Animal", instrument: "drums" } ]
?
names = (muppet.name for muppet in electric_mayhem)
# => [ 'Doctor Teeth', 'Janice', 'Sgt. Floyd Pepper', 'Zoot', 'Lips', 'Animal' ]
~~~
### 討論
因為 CoffeeScript 直接支持列表推導,在你使用一個 Python 的語句時,他們會很好地起到作用。對于簡單的映射,列表推導具有更好的可讀性。但是對于復雜的轉換或鏈式映射,映射數組可能更合適。
### 映射數組
### 問題
你有一個對象數組,想把這些對象映射到另一個數組中,就像 Ruby 的映射一樣。
### 解決方案
使用 map() 和匿名函數,但不要忘了還有列表推導。
~~~
electric_mayhem = [ { name: "Doctor Teeth", instrument: "piano" },
{ name: "Janice", instrument: "lead guitar" },
{ name: "Sgt. Floyd Pepper", instrument: "bass" },
{ name: "Zoot", instrument: "sax" },
{ name: "Lips", instrument: "trumpet" },
{ name: "Animal", instrument: "drums" } ]
?
names = electric_mayhem.map (muppet) -> muppet.name
# => [ 'Doctor Teeth', 'Janice', 'Sgt. Floyd Pepper', 'Zoot', 'Lips', 'Animal' ]
~~~
### 討論
因為 CoffeeScript 支持匿名函數,所以在 CoffeeScript 中映射數組就像在 Ruby 中一樣簡單。映射在 CoffeeScript 中是處理復雜轉換和連綴映射的好方法。如果你的轉換如同上例中那么簡單,那可能將它當成[列表推導](http://coffeescript-cookbook.github.io/chapters/arrays/list-comprehensions) 看起來會清楚一些。
### 數組最大值
### 問題
你需要找出數組中包含的最大的值。
### 解決方案
你可以使用 JavaScript 實現,在列表推導基礎上使用 Math.max():
~~~
Math.max [12, 32, 11, 67, 1, 3]...
# => 67
~~~
另一種方法,在 ECMAScript 5 中,可以使用 Array 的 reduce 方法,它與舊的 JavaScript 實現兼容。
~~~
# ECMAScript 5
?
[12,32,11,67,1,3].reduce (a,b) -> Math.max a, b
# => 67
~~~
### 討論
Math.max 在這里比較兩個數值,返回其中較大的一個。省略號 (...) 將每個數組價值轉化為給函數的參數。你還可以使用它與其他帶可變數量的參數進行討論,如執行 console.log 。
### 歸納數組
### 問題
你有一個對象數組,想要把它們歸納為一個值,類似于 Ruby 中的 reduce() 和 reduceRight() 。
### 解決方案
可以使用一個匿名函數包含 Array 的 reduce() 和 reduceRight() 方法,保持代碼清晰易懂。這里歸納可能會像對數值和字符串應用 + 運算符那么簡單。
~~~
[1,2,3,4].reduce (x,y) -> x + y
# => 10
?
?
["words", "of", "bunch", "A"].reduceRight (x, y) -> x + " " + y
# => 'A bunch of words'
~~~
或者,也可能更復雜一些,例如把列表中的元素聚集到一個組合對象中。
~~~
people =
{ name: 'alec', age: 10 }
{ name: 'bert', age: 16 }
{ name: 'chad', age: 17 }
?
people.reduce (x, y) ->
x[y.name]= y.age
x
, {}
# => { alec: 10, bert: 16, chad: 17 }
~~~
#### 討論
Javascript 1.8 中引入了 reduce 和 reduceRight ,而 Coffeescript 為匿名函數提供了簡單自然的表達語法。二者配合使用,可以把集合的項合并為組合的結果。
### 刪除數組中的相同元素
### 問題
你想從數組中刪除相同元素。
### 解決方案
~~~
Array::unique = ->
output = {}
output[@[key]] = @[key] for key in [0...@length]
value for key, value of output
?
[1,1,2,2,2,3,4,5,6,6,6,"a","a","b","d","b","c"].unique()
# => [ 1, 2, 3, 4, 5, 6, 'a', 'b', 'd', 'c' ]
~~~
### 討論
在 JavaScript 中有很多的獨特方法來實現這一功能。這一次是基于“最快速的方法來查找數組的唯一元素”,出自[這里](http://www.shamasis.net/2009/09/fast-algorithm-to-find-unique-items-in-javascript-array/) 。
> 注意: 延長本機對象通常被認為是在 JavaScript 不好的做法,即便它在 Ruby 語言中相當普遍, (參考:[Maintainable JavaScript: Don’t modify objects you don’t own](http://www.nczonline.net/blog/2010/03/02/maintainable-javascript-dont-modify-objects-you-down-own/)
### 反轉數組
### 問題
你想要反轉數組元素。
### 解決方案
使用 JavaScript Array 的 reverse() 方法:
~~~
["one", "two", "three"].reverse()
# => ["three", "two", "one"]
~~~
#### 討論
reverse() 是標準的 JavaScript 方法,別忘了帶圓括號。
### 打亂數組中的元素
### 問題
你想打亂數組中的元素。
### 解決方案
[ Fisher-Yates shuffle ](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 是一種高效、公正的方式來讓數組中的元素隨機化。這是一個相當簡單的方法:在列表的結尾處開始,用一個隨機元素交換最后一個元素列表中的最后一個元素。繼續下一個并重復操作,直到你到達列表的起始端,最終列表中所有的元素都已打亂。這 [ Fisher-Yates shuffle Visualization ](http://bost.ocks.org/mike/shuffle/) 可以幫助你理解算法。
~~~
shuffle = (source) ->
# Arrays with < 2 elements do not shuffle well. Instead make it a noop.
return source unless source.length >= 2
# From the end of the list to the beginning, pick element `index`.
for index in [source.length-1..1]
# Choose random element `randomIndex` to the front of `index` to swap with.
randomIndex = Math.floor Math.random() * (index + 1)
# Swap `randomIndex` with `index`, using destructured assignment
[source[index], source[randomIndex]] = [source[randomIndex], source[index]]
source
?
shuffle([1..9])
# => [ 3, 1, 5, 6, 4, 8, 2, 9, 7 ]
~~~
### 討論
#### 一種錯誤的方式
有一個很常見但是錯誤的打亂數組的方式:通過隨機數。
~~~
shuffle = (a) -> a.sort -> 0.5 - Math.random()
~~~
如果你做了一個隨機的排序,你應該得到一個序列隨機的順序,對吧?甚至[微軟也用這種隨機排序算法](http://www.robweir.com/blog/2010/02/microsoft-random-browser-ballot.html) 。原來,[這種隨機排序算法產生有偏差的結果](http://blog.codinghorror.com/the-danger-of-naivete/) ,因為它存在一種洗牌的錯覺。隨機排序不會導致一個工整的洗牌,它會導致序列排序質量的參差不齊。
#### 速度和空間的優化
以上的解決方案處理速度是不一樣的。該列表,當轉換成 JavaScript 時,比它要復雜得多,變性分配比處理裸變量的速度要慢得多。以下代碼并不完善,并且需要更多的源代碼空間 … 但會編譯量更小,運行更快:
~~~
shuffle = (a) ->
i = a.length
while --i > 0
j = ~~(Math.random() * (i + 1)) # ~~ is a common optimization for Math.floor
t = a[j]
a[j] = a[i]
a[i] = t
a
~~~
#### 擴展 Javascript 來包含亂序數組
下面的代碼將亂序功能添加到數組原型中,這意味著你可以在任何希望的數組中運行它,并以更直接的方式來運行它。
~~~
Array::shuffle ?= ->
if @length > 1 then for i in [@length-1..1]
j = Math.floor Math.random() * (i + 1)
[@[i], @[j]] = [@[j], @[i]]
this
?
[1..9].shuffle()
# => [ 3, 1, 5, 6, 4, 8, 2, 9, 7 ]
~~~
> 注意: 雖然它像在 Ruby 語言中相當普遍,但是在 JavaScript 中擴展本地對象通常被認為是不太好的做法 ( 參考: [Maintainable JavaScript: Don’t modify objects you don’t own](http://www.nczonline.net/blog/2010/03/02/maintainable-javascript-dont-modify-objects-you-down-own/)
正如提到的,以上的代碼的添加是十分安全的。它僅僅需要添 Array :: shuffle 如果它不存在,就要添加賦值運算符 (? =) 。這樣,我們就不會重寫到別人的代碼,或是本地瀏覽器的方式。
> 同時,如果你認為你會使用很多的實用功能,可以考慮使用一個工具庫,像 [ Lo-dash](https://lodash.com/) 。他們有很多功能,像跨瀏覽器的簡潔高效的地圖。 [Underscore](http://underscorejs.org/) 也是一個不錯的選擇。
### 檢測每個元素
### 問題
你希望能夠在特定的情況下檢測出在數組中的每個元素。
### 解決方案
使用 Array.every (ECMAScript 5):
~~~
evens = (x for x in [0..10] by 2)
?
evens.every (x)-> x % 2 == 0
# => true
~~~
Array.every 被加入到 Mozilla 的 Javascript 1.6 ,ECMAScript 5 標準。如果你的瀏覽器支持,但仍無法實施 EC5 ,那么請檢查 [ _.all from underscore.js](http://documentcloud.github.io/underscore/) 。
對于一個真實例子,假設你有一個多選擇列表,如下:
~~~
<select multiple id="my-select-list">
<option>1</option>
<option>2</option>
<option>Red Car</option>
<option>Blue Car</option>
</select>
~~~
現在你要驗證用戶只選擇了數字。讓我們利用 array.every :
~~~
validateNumeric = (item)->
parseFloat(item) == parseInt(item) && !isNaN(item)
?
values = $("#my-select-list").val()
?
values.every validateNumeric
~~~
### 討論
這與 Ruby 中的 Array #all? 的方法很相似。
### 使用數組來交換變量
### 問題
你想通過數組來交換變量。
### 解決方案
使用 CoffeeScript 的解構賦值語法:
~~~
a = 1
b = 3
?
[a, b] = [b, a]
?
a
# => 3
?
?
b
# => 1
~~~
### 討論
解構賦值可以不依賴臨時變量實現變量值的交換。
這種語法特別適合在遍歷數組的時候只想迭代最短數組的情況:
~~~
ray1 = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
ray2 = [ 5, 9, 14, 20 ]
?
intersection = (a, b) ->
[a, b] = [b, a] if a.length > b.length
value for value in a when value in b
?
intersection ray1, ray2
# => [ 5, 9 ]
?
?
intersection ray2, ray1
# => [ 5, 9 ]
~~~
### 對象數組
### 問題
你想要得到一個與你的某些屬性匹配的數組對象。
你有一系列的對象,如:
~~~
cats = [
{
name: "Bubbles"
favoriteFood: "mice"
age: 1
},
{
name: "Sparkle"
favoriteFood: "tuna"
},
{
name: "flyingCat"
favoriteFood: "mice"
age: 1
}
]
~~~
你想用某些特征來濾出想要的對象。例如:貓的位置 ({ 年齡: 1 }) 或者貓的位置 ({ 年齡: 1 , 最愛的食物: "老鼠" })
### 解決方案
你可以像這樣來擴展數組:
~~~
Array::where = (query) ->
return [] if typeof query isnt "object"
hit = Object.keys(query).length
@filter (item) ->
match = 0
for key, val of query
match += 1 if item[key] is val
if match is hit then true else false
?
cats.where age:1
# => [ { name: 'Bubbles', favoriteFood: 'mice', age: 1 },{ name: 'flyingCat', favoriteFood: 'mice', age: 1 } ]
?
?
cats.where age:1, name: "Bubbles"
# => [ { name: 'Bubbles', favoriteFood: 'mice', age: 1 } ]
?
?
cats.where age:1, favoriteFood:"tuna"
# => []
~~~
### 討論
這是一個確定的匹配。我們能夠讓匹配函數更加靈活:
~~~
Array::where = (query, matcher = (a,b) -> a is b) ->
return [] if typeof query isnt "object"
hit = Object.keys(query).length
@filter (item) ->
match = 0
for key, val of query
match += 1 if matcher(item[key], val)
if match is hit then true else false
?
cats.where name:"bubbles"
# => []
?
# it's case sensitive
?
?
cats.where name:"bubbles", (a, b) -> "#{ a }".toLowerCase() is "#{ b }".toLowerCase()
# => [ { name: 'Bubbles', favoriteFood: 'mice', age: 1 } ]
?
# now it's case insensitive
~~~
處理收集的一種方式可以被叫做 “find” ,但是像 underscore 或者 lodash 這些庫把它叫做 “where” 。
### 類似 Python 的 zip 函數
### 問題
你想把多個數組連在一起,生成一個數組的數組。換句話說,你需要實現與 Python 中的 zip 函數類似的功能。 Python 的 zip 函數返回的是元組的數組,其中每個元組中包含著作為參數的數組中的第 i 個元素。
### 解決方案
使用下面的 CoffeeScript 代碼:
~~~
# Usage: zip(arr1, arr2, arr3, ...)
?
zip = () ->
lengthArray = (arr.length for arr in arguments)
length = Math.max.apply(Math, lengthArray)
argumentLength = arguments.length
results = []
for i in [0...length]
semiResult = []
for arr in arguments
semiResult.push arr[i]
results.push semiResult
return results
?
zip([0, 1, 2, 3], [0, -1, -2, -3])
# => [[0, 0], [1, -1], [2, -2], [3, -3]]
~~~