iT邦幫忙

2022 iThome 鐵人賽

DAY 19
0

Abstract

整篇會分成以下幾個部分:

  • 使用時機
  • 語法
  • 說明
  • 範例
  • 注意事項
  • ECMAScript
  • 結論

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]

你可以利用 startend 參數指定一段連續不中斷的區間,。


語法

slice()

slice(start)

slice(start, end)

參數

slice() 接受 2 個可選的參數。

第 1 個為 start

第 2 個為 end

start (可選的)

指定截取區間的起始點 (包含本身),預設為 0。

型別應該為 1 個數字:

  • 如果為正數,會從陣列開始的地方算起
  • 如果為負數,則會從陣列結尾的地方算起 (array.length + start)
  • 如果為小數,則會先轉換成整數,轉換方式請參考下面的注意事項
  • 如果沒有提供則為 0

end (可選的)

指定截取區間的結束點 (不包含本身),預設為陣列的結尾 (陣列的長度)。

型別應該為 1 個數字:

  • 如果為正數,會從陣列開始的地方算起
  • 如果為負數,則會從陣列結尾的地方算起 (array.length + start)
  • 如果為小數,則會先轉換成整數,轉換方式請參考下面的注意事項
  • 如果沒有提供則為陣列長度 (至陣列結尾)

Return Value

回傳一個從原陣列截取出來的新陣列,可以透過 startend 指定這個區間。

Mutability

不會變動到原陣列。


說明

slice() 會回傳一個新陣列,這個陣列的元素為原陣列某個區間的淺拷貝,我們可以透過 startend 來指定這個區間。

slice() 會保留稀疏陣列 (sparse) 的 empty slot,例如範例的 Example 4。


範例

Example 1 - 基礎用法

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']

注意是否有提供參數,及參數為正數、負數或小數。

Example 2 - 淺拷貝參照

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() 會回傳一個新陣列,元素為原陣列的淺拷貝,因此 alexProfilepeople[0]somePeople[0] 三個都指向同一個物件。

Example 3 - 利用 bind & call (取自 MDN)

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]

這邊利用 bindcallthis 綁定為 slice() 這個 method,並回傳這個綁定後的 call 函式給 slice 變數,因此呼叫 slice 等同於呼叫 Array.prototype.slice.call()

所以傳入 list 函式的 arguments 會被帶進 slice 函式,也就表示這個 arguments 會成為slicethis

因此最後 arguments 這個類陣列物件會被轉成一個新陣列後回傳出來。

Example 4 - 稀疏陣列的操作

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。

startend 為負值時不代表要反向操作,單純只是讓起始索引跟結尾索引從陣列結尾開始計算。

start 大於陣列索引範圍,會回傳空陣列;如果 startend 為負數且加上陣列長度後仍小於 0,則以 0 計;當 end 大於陣列長度則以陣列長度計 (array.length)。


ECMAScript

19.1

slice() 的演算法並沒有要求呼叫它的一定要是一個陣列,可以從步驟 1 跟 Note 2 得知,為了方便解釋,這邊一律使用陣列來說明;我們先來驗證一下它是否被做成了通用的咩色:

19.2

演算法的前 2 個步驟都是用來做一些前置處理,包括轉型、確認長度。

步驟 3 蠻值得注意的,這個 ToIntegerOrInfinity 讓 start 是小數仍然可以正常使用,但因為它在轉換的過程使用了絕對值,導致轉成整數的結果可能會跟預期的不一樣,例如 -5.5 會被轉換成 -5 而非 -6。

步驟 4、5 、8、9 表明了當 startend 為負數且加上陣列長度仍小於 0 時,會視為 0,驗證了前面的注意事項。

步驟 7 可以看到如果沒有提供 end ,則將其設為陣列長度。

步驟 10 ~ 12 挺有趣的,它先找到區間的結束點,再將區間的結束點減掉區間的起始點算出 count,再利用原陣列創造出一個長度為 count 的新陣列。

步驟 14 便開始遍歷原陣列的指定區間,依照順序取出每個索引的值再新增至新陣列上,要注意的是每次新增前都會先確認原陣列是否有相對應的屬性。

步驟 15 可能會讓人非常困惑,因為它又重新對新陣列設置了一次長度 (length),這是因為在 ES6 前,新陣列在初始化時並沒有給予長度;但 ES6 之後,陣列初始化使用的是 ArraySpeciesCreate() 這個抽象操作,新陣列在建立的同時便已指定好長度,所以不需要再重新設置長度;演算法保留這個步驟是為了跟之前的版本相容。

最後步驟 16 將設置好元素跟長度的新陣列回傳出去。

出現 ? 的地方代表有可能會丟出錯誤,所以整個演算法有 8 處有機會丟出錯誤,例如步驟 1 的 ToObject(),當你傳入 Undefined 或 Null 即會丟出一個 TypeError 的錯誤,我們來驗證一下:

19.3

如果出現 ! ,則代表這個 abstract operation 絕對不會丟出錯誤,例如步驟 14 的 ToString() 它會在參數是一個 Symbol 時丟出一個 TypeError,但我們確定丟進去的是一個 Number (F(k)),因此不會有丟出錯誤的可能。

從 ECMAScript 的演算法來看,尚未找到與 JavaScript 實作的不同之處。


結論

slice() 有時候會用來將類陣列轉換成真正的陣列,但在 ES6 之後可以用展開運算子 (spread operator) 來達到相同目的,我們也就不需要再借助 slice() + call 了。

slice() 也可以快速複製一個新陣列,但同樣地,使用展開運算子更快更方便,我們應該專注將 slice() 用在截取陣列上即可。

最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。


參考資源


上一篇
Day 18 咩色用得好 - Array.prototype.flat
下一篇
Day 20 咩色用得好 - Array.prototype.findIndex
系列文
咩色用得好,歸剛沒煩惱 - 從 ECMAScript 偷窺 JavaScript Array method30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言