reverse()
不像其他咩色需要帶入參數甚至是 callback,使用起來可以說是相對簡單,但它的演算法稍嫌複雜且有一些讓人迷惑的地方,因此這篇介紹 ECMAScript 的篇幅會較長,但有一些細節挺值得一看的!我們下面會利用 ECMAScript 的演算法實作一個簡單客製的 reverse()
函式 (範例的 Example 3)。
整篇會分成以下幾個部分:
reverse()
這個 method 的全寫應該是 Array.prototype.reverse()
,有興趣可以看 Day 2 的介紹,這邊會直接使用 reverse()
作為替代。
範例的 callback 都會使用箭頭函式做介紹,如果尚不熟悉的話可以參考 MDN 的介紹。
最後會透過分析 ECMAScript 來驗證是否有吻合,如果覺得 ECMAScript 有點艱澀難懂,我們在 Day 4 、Day 5 有介紹其相關術語可以幫助閱讀。
當你想要反轉一個陣列時,reverse()
會將原陣列反轉後回傳給你,例如以下:
[0, 1, 2, 3] -> [3, 2, 1, 0]
反轉後,原本陣列的第 1 個元素會變成最後一個,而最後 1 個元素會變成第 1 個,以此類推...。
reverse()
不需要提供任何參數給 reverse()
。
回傳順序被反轉後的原陣列,也就是原陣列的參照 (reference)。
注意回傳陣列是原陣列,而非一個反轉後的新陣列。
會改動到原陣列。
reverse()
會使用反轉的方式調換元素的順序,也就是說第 1 個元素會變成最後 1 個,而最後 1 個則變成第 1 個,以此類推...。
reverse()
會保留稀疏陣列中的 empty slot,例如以下:
reverse()
會變動原陣列並回傳原陣列的參照,所以如果有將回傳值指派給其他變數,這個變數所指向的陣列跟原陣列會是一樣的。
const names = ['Emma', 'Anita', 'Pedro', 'Damien']
const reversed = names.reverse()
console.log(reversed)
// ['Damien', 'Pedro', 'Anita', 'Emma']
console.log(names)
// ['Damien', 'Pedro', 'Anita', 'Emma']
注意回傳的是反轉後的原陣列參照。
const names = ['Emma', 'Anita', 'Pedro', 'Damien']
const reversed_1 = [...names].reverse()
console.log(reversed_1)
// ['Damien', 'Pedro', 'Anita', 'Emma']
const reversed_2 = Array.from(names).reverse()
console.log(reversed_2)
// ['Damien', 'Pedro', 'Anita', 'Emma']
const reversed_3 = names.map(element => element).reverse()
console.log(reversed_3)
// ['Damien', 'Pedro', 'Anita', 'Emma']
const reversed_4 = names.filter(() => true).reverse()
console.log(reversed_4)
// ['Damien', 'Pedro', 'Anita', 'Emma']
const reversed_5 = names.reduce((newArray, name) => {
newArray.push(name)
return newArray
}, []).reverse()
console.log(reversed_5)
// ['Damien', 'Pedro', 'Anita', 'Emma']
const reversed_6 = names.slice().reverse()
console.log(reversed_6)
// ['Damien', 'Pedro', 'Anita', 'Emma']
const reverse_7 = names.concat().reverse()
console.log(reverse_7)
// ['Damien', 'Pedro', 'Anita', 'Emma']
console.log(names)
// ['Emma', 'Anita', 'Pedro', 'Damien']
為了避免改動到原陣列,這邊使用了 7 種方法對原陣列做淺拷貝 (shallow copy) 後再執行反轉。
reverse()
const array = [0, 1, 2, 3, 4]
console.log(reverse(array))
// [4, 3, 2, 1, 0]
function reverse(array) {
const length = array.length
const middle = Math.floor(length / 2)
let lower = 0
while(lower !== middle) {
const upper = length - lower -1
const lowerValue = array[lower]
const upperValue = array[upper]
array[lower] = upperValue
array[upper] = lowerValue
lower++
}
return array
}
這邊利用了 ECMAScript reverse()
的演算法概念做了一個簡單客製的 reverse()
函式,但要注意的是這是一個很不嚴謹的函式,因為我們略過了很多檢查跟執行步驟,但這邊證明了我們可以用同樣的演算法達到相同的目的。
reverse()
被刻意做成通用的,擁有 length
屬性的類陣列物件都可以使用這個咩色;但要注意的是, 字串雖然也是類陣列的一種,但由於它是不可改變的(Immutable ),因此並沒有辦法適用這個咩色。
Array.prototype.reverse()
reverse()
的演算法並沒有要求呼叫它的一定要是一個陣列,可以從步驟 1 跟 Note 2 得知,為了方便解釋,這邊一律使用陣列來說明;我們來驗證一下它是否被做成了一個通用的咩色:
演算法的前 2 個步驟都是用來做一些前置處理,包括轉型、確認長度。
步驟 3 非常重要,這邊利用陣列的中點 (floor(len / 2)
) 作為迴圈的結束條件 - middle
。
而步驟 4 的 lower
便是回圈的計數,從 0 起始。
步驟 5 開始陣列反轉,迴圈會一直持續到陣列中點才停止,也就是 lower
等於 middle
;這邊可以看到 reverse()
每次都會將陣列頭尾元素取出並將其互換,接著將範圍往內縮減,再重複同樣的頭尾取值互換,直到範圍縮減至中點 (middle
) ,例如以下圖示:
可以看到步驟 5-d 跟 5-f 在每次取值前都會使用 HasProperty()
來檢查屬性是否存在,因此如果屬性不存在或為 empty slot, HasProperty()
都會回傳 false
。
reverse()
只會在屬性存在時才將值取出:
lower
跟 upper
都存在,則將值取出並交換lower
存在而 upper
不存在,則將 upper
的值設為 lower
,並將 lower
刪除lower
不存在而 upper
存在,則將 lower
的值設為 upper
,並將 upper
刪除lower
跟 upper
都不存在,則不會有任何動作這邊非常值得注意的是 - 為什麼從演算法來看, reverse()
明明最後會將不存在的屬性於交換後刪除,但實際操作下來,陣列中的 empty slot 卻仍然被保留下來?打開 MDN 也寫著 reverse()
會保留 empty slot!
我想這是因為 陣列 這個特殊物件有著一個非常非常重要的特性 -
就是陣列的 length
!
實際上,掌控陣列長度的並非是實際存在陣列中的元素,而是陣列的 length
屬性,陣列為了維持這個 length
,它會將不存在的元素補上一個 undefined,也就是我們常說的 undefined hole 或 empty slot,所以當你使用 delete
刪除陣列的某個元素時,它的確會被刪除,但 delete
並不會改變陣列的 length
,因此陣列會再馬上依據 length
的長度來補上相對應的 empty slot,我們先來驗證一下:
const array = [ , 1, 2, 3, ,]
console.log(array)
// [empty, 1, 2, 3, empty]
console.log(array.length)
// 5
delete array[2]
console.log(array)
// [empty, 1, empty, 3, empty]
console.log(array.length)
// 5
上面可以看到 delete
刪除的元素仍然會被自動補上 empty slot,因此這邊可以推斷 reverse()
不是直接保留 empty slot 的元素,它應該有先將其刪除,只是 reverse()
並不會更動到陣列的長度,因此陣列會馬上將其補上相對應的 empty slot,讓陣列維持它該有的長度! 因為這個是陣列物件才有的特性,因此如果使用在仿照陣列的一般物件,不存在的屬性就真的會被刪除了,我們來驗證一下:
const array = [ , 1, 2, , 4,]
console.log(array.reverse())
// [4, empty, 2, 1, empty]
const fakeArray = {
0: 0,
2: 2,
3: 3,
4: 4,
length: 6
}
console.log([].reverse.call(fakeArray))
// {1: 4, 2: 3, 3: 2, 5: 0, length: 6}
上面可以看到 array
原本交換後被刪除的元素被使用 empty slot 填充了,但 fakeArray
交換後不存在的屬性的確被刪除了,也就是屬性 0
跟 4
在交換後被刪除了,關於陣列的更多特性我們會在 Day 30 來做討論。
出現 ?
的地方代表有可能會丟出錯誤,所以整個演算法有 12 處有機會丟出錯誤,例如步驟 5-h-i 的 Set()
,當設值失敗便會丟出一個 TyperError 的錯誤;或者步驟 2 的 LengthOfArrayLike()
,當物件的 length
為一個 Symbal 或 BigInt,便會從內部的 ToLength()
-> ToIntegerOrInfinity()
-> ToNumber()
丟出一個 TyperError 的錯誤,我們來驗證一下:
如果出現 !
,則代表這個抽象操作 (abstract operation) 絕對不會丟出錯誤,例如步驟 5-b 的 ToString()
它會在參數是一個 Symbol 時丟出一個 TypeError,但我們確定丟進去的是一個 Number (F(upper)
),因此不會有丟出錯誤的可能。
從 ECMAScript 的演算法來看,尚未找到與 JavaScript 實作的不同之處。
reverse()
可以說是簡單好用又快速,但切記它會變動到原陣列,且回傳的值也是原陣列的參照,很容易不小心變動到原始資料卻不自知,如果站在 immutalbe 的角度,我們應該要儘量複製一個新陣列後再進行 reverse()
。
最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。