卑鄙是卑鄙者的通行證,高尚是高尚者的墓誌銘。 ——北島《回答》 看北島就是從這兩句詩開始的,高尚者已死,只剩卑鄙者在世間橫行。 本文為讀 lodash 源碼的第一篇,後續文章會更新到這個倉庫中,歡迎 star: "pocket lodash" gitbook也會同步倉庫的更新,gitbook地址: ...
卑鄙是卑鄙者的通行證,高尚是高尚者的墓誌銘。
——北島《回答》
看北島就是從這兩句詩開始的,高尚者已死,只剩卑鄙者在世間橫行。
本文為讀 lodash 源碼的第一篇,後續文章會更新到這個倉庫中,歡迎 star:pocket-lodash
gitbook也會同步倉庫的更新,gitbook地址:pocket-lodash
引言
你可能會有點奇怪,原生的 slice 方法基本沒有相容性的問題,為什麼 lodash 還要實現一個 slice 方法呢?
這個問題,lodash 的作者已經在 why not the 'baseslice' func use Array.slice(), loop faster than slice? 的 issue 中給出了答案:lodash 的 slice 會將數組當成密集數組對待,原生的 slice 會將數組當成稀疏數組對待。
密集數組VS稀疏數組
我們先來看看犀牛書是怎樣定義稀疏數組的:
稀疏數組就是包含從0開始的不連續索引的數組。通常,數組的length屬性值代表數組中元素的個數。如果數組是稀疏的,length屬性值大於元素的個數。
如果數組是稀疏的,那麼這個數組中至少有一個以上的位置不存在元素(包括 undefined
)。
例如:
var sparse = new Array(10)
var dense = new Array(10).fill(undefined)
其中 sparse
的 length
為10,但是 sparse
數組中沒有元素,是稀疏數組;而 dense
每個位置都是有元素的,雖然每個元素都為undefined
,為密集數組 。
那稀疏數組和密集數組有什麼區別呢?在 lodash 中最主要考慮的是兩者在迭代器中的表現。
稀疏數組在迭代的時候會跳過不存在的元素。
sparse.forEach(function(item){
console.log(item)
})
dense.forEach(function(item){
console.log(item)
})
sparse
根本不會調用 console.log
列印任何東西,但是 dense
會列印出10個 undefined
。
源碼總覽
當然,除了對待稀疏數組跟原生的 slice 不一致外,其他的規則還是一樣的,下麵是 lodash 實現 slice 的源碼。
function slice(array, start, end) {
let length = array == null ? 0 : array.length
if (!length) {
return []
}
start = start == null ? 0 : start
end = end === undefined ? length : end
if (start < 0) {
start = -start > length ? 0 : (length + start)
}
end = end > length ? length : end
if (end < 0) {
end += length
}
length = start > end ? 0 : ((end - start) >>> 0)
start >>>= 0
let index = -1
const result = new Array(length)
while (++index < length) {
result[index] = array[index + start]
}
return result
}
不傳參的情況
let length = array == null ? 0 : array.length
if (!length) {
return []
}
不傳參時,length
預設為0,否則獲取數組的長度。註意這裡用的是 array == null
,非 array === null
,包含了 undefined
的判斷。
所以在不傳參調用 lodash 的 slice 時,返回的是空數組,而原生的 slice 沒有這種調用方式。
處理start參數
start
參數用來指定截取的開始位置。
先來看下 MDN 對該參數的描述:
如果該參數為負數,則表示從原數組中的倒數第幾個元素開始提取。
如果省略,則從索引0開始
start = start == null ? 0 : start
因此這段是處理省略的情況,省略時,預設值為0。
if (start < 0) {
start = -start > length ? 0 : (length + start)
}
這段是處理負數的情況。
如果負數取反後比數組的長度還要大,即超出了數組的範圍,則取值為0,表示從開始的位置截取,否則用 length + start
,即向後倒數。
start >>>= 0
最後,用在 >>>
來確保 start
參數為整數或0。
因為 lodash 的 slice 除了可以處理數組外,也可以處理類數組,因此第一個參數 array
可能為一個對象, length
屬性不一定為數字。
處理end參數
end
參數用來指定截取的結束位置。
同樣來看下 MDN 對些的描述:
如果該參數為負數,則它表示在原數組中的倒數第幾個元素結束制取。
如果end被省略,則slice會一直提取到原數組的末尾。
如果end大於數組長度,slice也會一直提取到原數組末尾。
end = end === undefined ? length : end
這段是處理 end
被省略的情況,省略時,end
預設為為 length
,即截取到數組的末尾。
end = end > length ? length : end
這是處理 end
比數組長度大的情況,如果被數組長度大,也會截取到數組的末尾。
if (end < 0) {
end += length
}
這段是處理負值的情況,如果為負值,則從數組末尾開始向前倒數。
這裡沒有像 start
一樣控制 end
的向前倒數完後是否為負數,因為後面還有一層控制。
獲取新數組的長度
length = start > end ? 0 : ((end - start) >>> 0)
新數組的長度計算方式很簡單,就是用 edn - start
即可得出。
上面說到,沒有控制最終 end
是否為負數的情況。這裡用的是 start
和 end
的比較,如果 start
比 end
大,則新數組長度為0,即返回一個空數組。否則用 end - start
來計算。
這裡同樣用了無符號右移位運算符來確保 length
為正數或0。
截取並返回新數組
let index = -1
const result = new Array(length)
while (++index < length) {
result[index] = array[index + start]
}
return result
result
為新數組容器。
用 while
迴圈,從 start
位置開始,獲取原數組的值,依次存入新的數組中。
因為是通過索引取值,如果遇到稀疏數組,對應的索引值上沒有元素時,通過數組索引取值返回的是 undefined
, 但這並不是說稀疏數組中該位置的值為 undefined
。
最後將 result
返回。
參考
- javascript權威指南(第6版), David Flanagan著,淘寶前端團隊譯,機械工業出版社
- why not the 'baseslice' func use Array.slice(), loop faster than slice?
- Array.prototype.slice()
- JavaScript: sparse arrays vs. dense arrays
- [譯]JavaScript中的稀疏數組與密集數組
License
署名-非商業性使用-禁止演繹 4.0 國際 (CC BY-NC-ND 4.0)
作者:對角另一面