整篇會分成以下幾個部分:
slice()
這個 method 的全寫應該是 Array.prototype.slice
,有興趣可以看 Day 2 的介紹,這邊會直接使用 slice()
作為替代。
最後會透過分析 ECMAScript 來驗證是否有吻合,如果覺得 ECMAScript 有點艱澀難懂,我們在 Day 4 、Day 5 有介紹其相關術語可以幫助閱讀。
slice()
的 ECMAScript 演算法略為繁雜,但挺值得一讀,大家有空可以往下看 ECMAScript 的介紹!
當你需要從原陣列複製一段陣列出來時,就像從原陣列截取出來一樣,例如以下:
[1, 2, 3, 4, 5] => [2, 3, 4]
你可以利用 start
跟 end
參數指定一段連續不中斷的區間,。
slice()
slice(start)
slice(start, end)
slice()
接受 2 個可選的參數。
第 1 個為 start
。
第 2 個為 end
。
start
(可選的)指定截取區間的起始點 (包含本身),預設為 0。
型別應該為 1 個數字:
array.length + start
)end
(可選的)指定截取區間的結束點 (不包含本身),預設為陣列的結尾 (陣列的長度)。
型別應該為 1 個數字:
array.length + start
)回傳一個從原陣列截取出來的新陣列,可以透過 start
跟 end
指定這個區間。
不會變動到原陣列。
slice()
會回傳一個新陣列,這個陣列的元素為原陣列某個區間的淺拷貝,我們可以透過 start
、 end
來指定這個區間。
slice()
會保留稀疏陣列 (sparse) 的 empty slot,例如範例的 Example 4。
const names = ['Alejo', 'Emma', 'Cate', 'Pedro', 'Russ']
console.log(names.slice())
// ['Alejo', 'Emma', 'Cate', 'Pedro', 'Russ']
console.log(names.slice(2))
// ['Cate', 'Pedro', 'Russ']
console.log(names.slice(1, 4))
// ['Emma', 'Cate', 'Pedro']
console.log(names.slice(-3))
// ['Cate', 'Pedro', 'Russ']
console.log(names.slice(1, -2))
// ['Emma', 'Cate']
注意是否有提供參數,及參數為正數、負數或小數。
const alexProfile = {
name: 'Alex',
age: 25,
gender: 'male'
}
const people = [alexProfile, 'Emma', 'Cate', 'Pedro']
const somePeople = people.slice(0, 2)
console.log(somePeople)
// [alexProfile, 'Emma']
console.log(alexProfile.gender)
// 'male'
console.log(people[0].gender)
// 'male'
console.log(somePeople[0].gender)
// 'male'
somePeople[0].gender = 'trans'
console.log(alexProfile.gender)
// 'trans'
console.log(people[0].gender)
// 'trans'
console.log(somePeople[0].gender)
// 'trans'
slice()
會回傳一個新陣列,元素為原陣列的淺拷貝,因此 alexProfile
、people[0]
、somePeople[0]
三個都指向同一個物件。
const unboundSlice = Array.prototype.slice
const slice = Function.prototype.call.bind(unboundSlice)
function list() {
return slice(arguments)
}
const list1 = list(1, 2, 3)
console.log(list1)
// [1, 2, 3]
這邊利用 bind
將 call
的 this
綁定為 slice()
這個 method,並回傳這個綁定後的 call
函式給 slice
變數,因此呼叫 slice
等同於呼叫 Array.prototype.slice.call()
。
所以傳入 list
函式的 arguments
會被帶進 slice
函式,也就表示這個 arguments
會成為slice
的 this
。
因此最後 arguments
這個類陣列物件會被轉成一個新陣列後回傳出來。
const array = [1, , 3, 4, , 6]
const slicedArray = array.slice(1, -1)
console.log(slicedArray)
// [empty, 3, 4, empty]
原陣列中的 empty slot 也會被前拷貝。
slice()
會回傳一個新陣列,這個新陣列的元素會是原陣列相對應元素的淺拷貝 (shallow copy),也就是說如果這個元素是一個基本型別 (primitive),那它就是單純複製一個值,但如果是物件型別 (object),那它複製的會是參照,這意味著新陣列的物件元素跟相對應的原陣列物件元素,它們會指向同一個物件,可參考範例的 Example 2。
start
跟 end
為負值時不代表要反向操作,單純只是讓起始索引跟結尾索引從陣列結尾開始計算。
當 start
大於陣列索引範圍,會回傳空陣列;如果 start
或 end
為負數且加上陣列長度後仍小於 0,則以 0 計;當 end
大於陣列長度則以陣列長度計 (array.length
)。
Array.prototype.slice(start,end)
slice()
的演算法並沒有要求呼叫它的一定要是一個陣列,可以從步驟 1 跟 Note 2 得知,為了方便解釋,這邊一律使用陣列來說明;我們先來驗證一下它是否被做成了通用的咩色:
演算法的前 2 個步驟都是用來做一些前置處理,包括轉型、確認長度。
步驟 3 蠻值得注意的,這個 ToIntegerOrInfinity
讓 start
是小數仍然可以正常使用,但因為它在轉換的過程使用了絕對值,導致轉成整數的結果可能會跟預期的不一樣,例如 -5.5 會被轉換成 -5 而非 -6。
步驟 4、5 、8、9 表明了當 start
或 end
為負數且加上陣列長度仍小於 0 時,會視為 0,驗證了前面的注意事項。
步驟 7 可以看到如果沒有提供 end
,則將其設為陣列長度。
步驟 10 ~ 12 挺有趣的,它先找到區間的結束點,再將區間的結束點減掉區間的起始點算出 count
,再利用原陣列創造出一個長度為 count
的新陣列。
步驟 14 便開始遍歷原陣列的指定區間,依照順序取出每個索引的值再新增至新陣列上,要注意的是每次新增前都會先確認原陣列是否有相對應的屬性。
步驟 15 可能會讓人非常困惑,因為它又重新對新陣列設置了一次長度 (length
),這是因為在 ES6 前,新陣列在初始化時並沒有給予長度;但 ES6 之後,陣列初始化使用的是 ArraySpeciesCreate()
這個抽象操作,新陣列在建立的同時便已指定好長度,所以不需要再重新設置長度;演算法保留這個步驟是為了跟之前的版本相容。
最後步驟 16 將設置好元素跟長度的新陣列回傳出去。
出現 ?
的地方代表有可能會丟出錯誤,所以整個演算法有 8 處有機會丟出錯誤,例如步驟 1 的 ToObject()
,當你傳入 Undefined 或 Null 即會丟出一個 TypeError 的錯誤,我們來驗證一下:
如果出現 !
,則代表這個 abstract operation 絕對不會丟出錯誤,例如步驟 14 的 ToString()
它會在參數是一個 Symbol 時丟出一個 TypeError,但我們確定丟進去的是一個 Number (F(k)
),因此不會有丟出錯誤的可能。
從 ECMAScript 的演算法來看,尚未找到與 JavaScript 實作的不同之處。
slice()
有時候會用來將類陣列轉換成真正的陣列,但在 ES6 之後可以用展開運算子 (spread operator) 來達到相同目的,我們也就不需要再借助 slice()
+ call
了。
slice()
也可以快速複製一個新陣列,但同樣地,使用展開運算子更快更方便,我們應該專注將 slice()
用在截取陣列上即可。
最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。