讀lodash源碼之從slice看稀疏數組與密集數組

卑鄙是卑鄙者的通行證,高尚是高尚者的墓誌銘。javascript

——北島《回答》html

看北島就是從這兩句詩開始的,高尚者已死,只剩卑鄙者在世間橫行。前端

本文爲讀 lodash 源碼的第一篇,後續文章會更新到這個倉庫中,歡迎 star:pocket-lodashjava

gitbook也會同步倉庫的更新,gitbook地址:pocket-lodashgit

引言

你可能會有點奇怪,原生的 slice 方法基本沒有兼容性的問題,爲何 lodash 還要實現一個 slice 方法呢?github

這個問題,lodash 的做者已經在 why not the 'baseslice' func use Array.slice(), loop faster than slice? 的 issue 中給出了答案:lodash 的 slice 會將數組當成密集數組對待,原生的 slice 會將數組當成稀疏數組對待。數組

密集數組VS稀疏數組

咱們先來看看犀牛書是怎樣定義稀疏數組的:微信

稀疏數組就是包含從0開始的不連續索引的數組。一般,數組的length屬性值表明數組中元素的個數。若是數組是稀疏的,length屬性值大於元素的個數。oop

若是數組是稀疏的,那麼這個數組中至少有一個以上的位置不存在元素(包括 undefined )。spa

例如:

var sparse = new Array(10)
var dense = new Array(10).fill(undefined)

其中 sparselength 爲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 是否爲負數的狀況。這裏用的是 startend 的比較,若是 startend 大,則新數組長度爲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 返回。

參考

  1. javascript權威指南(第6版), David Flanagan著,淘寶前端團隊譯,機械工業出版社

  2. why not the 'baseslice' func use Array.slice(), loop faster than slice?

  3. Array.prototype.slice()

  4. JavaScript: sparse arrays vs. dense arrays

  5. 【譯】JavaScript中的稀疏數組與密集數組

License

署名-非商業性使用-禁止演繹 4.0 國際 (CC BY-NC-ND 4.0)

最後,全部文章都會同步發送到微信公衆號上,歡迎關注,歡迎提意見:

做者:對角另外一面

相關文章
相關標籤/搜索