Lodashのsliceメソッドで学ぶスパース配列とデンス配列の違い

はじめに

ネイティブのsliceメソッドには互換性の問題がないため、なぜlodashが独自のsliceメソッドを実装しているのか不思議に思うかもしれません。

この質問については、lodashの作者が「why not the 'baseslice' func use Array.slice(), loop faster than slice?」というissueで回答しています:lodashのsliceは配列をデンス配列として扱い、ネイティブのsliceは配列をスパース配列として扱うからです。

スパース配列とデンス配列

まず、スパース配列がどのように定義されているかを見てみましょう。JavaScriptの専門書では次のように説明されています:

スパース配列とは、0から始まる不連続なインデックスを持つ配列です。通常、配列のlengthプロパティは配列内の要素数を表します。配列がスパースな場合、lengthプロパティの値は要素数より大きくなります。

配列がスパースである場合、その配列には少なくとも1つ以上の位置に要素が存在しません(undefinedを含む)。

例えば:

const sparseArray = new Array(10)
const denseArray = new Array(10).fill(undefined)

ここで、sparseArraylengthは10ですが、sparseArray配列には要素がなく、スパース配列です。一方、denseArrayは各位置に要素があり、すべての要素がundefinedであってもデンス配列です。

では、スパース配列とデンス配列にはどのような違いがあるのでしょうか。lodashでは、主にイテレータにおける両者の振る舞いの違いが考慮されています。

スパース配列を反復処理する際には、存在しない要素はスキップされます。

sparseArray.forEach(function(item){
  console.log(item)
})
denseArray.forEach(function(item){
  console.log(item)
})

sparseArrayconsole.logを呼び出して何も出力しませんが、denseArrayは10個のundefinedを出力します。

ソースコードの概観

もちろん、スパース配列の扱いにおいてネイティブのsliceと異なる点以外は、他のルールは同じです。以下はlodashのsliceを実装したソースコードです。

function extractArrayElements(inputArray, startPosition, endPosition) {
  const arraySize = inputArray == null ? 0 : inputArray.length
  if (!arraySize) {
    return []
  }
  startPosition = startPosition == null ? 0 : startPosition
  endPosition = endPosition === undefined ? arraySize : endPosition

  if (startPosition < 0) {
    startPosition = -startPosition > arraySize ? 0 : (arraySize + startPosition)
  }
  endPosition = endPosition > arraySize ? arraySize : endPosition
  if (endPosition < 0) {
    endPosition += arraySize
  }
  arraySize = startPosition > endPosition ? 0 : ((endPosition - startPosition) >>> 0)
  startPosition >>>= 0

  let currentIndex = -1
  const extractedArray = new Array(arraySize)
  while (++currentIndex < arraySize) {
    extractedArray[currentIndex] = inputArray[currentIndex + startPosition]
  }
  return extractedArray
}

引数を渡さない場合

const arraySize = inputArray == null ? 0 : inputArray.length
if (!arraySize) {
  return []
}

引数を渡さない場合、arraySizeはデフォルトで0になり、そうでない場合は配列の長さを取得します。ここではinputArray == nullが使用されており、inputArray === nullではありません。これはundefinedの判断も含まれています。

そのため、lodashのsliceを引数なしで呼び出すと、空の配列が返されますが、ネイティブのsliceにはこのような呼び出し方法はありません。

startPositionパラメータの処理

startPositionパラメータは、切り取る開始位置を指定するために使用されます。

まず、MDNでのこのパラメータの説明を見てみましょう:

このパラメータが負の値の場合、元の配列の末尾から数えて何番目の要素から抽出を開始するかを示します。

省略された場合、インデックス0から開始します

startPosition = startPosition == null ? 0 : startPosition

したがって、この部分は省略された場合の処理で、省略時はデフォルト値0が設定されます。

if (startPosition < 0) {
  startPosition = -startPosition > arraySize ? 0 : (arraySize + startPosition)
}

この部分は、負の値の場合の処理です。

負の値を反転させた値が配列の長さより大きい場合、つまり配列の範囲を超えている場合は、0を設定します。これは開始位置から切り取ることを意味します。それ以外の場合はarraySize + startPositionを使用し、後方から数えます。

startPosition >>>= 0

最後に、>>>を使用してstartPositionパラメータが整数または0であることを保証します。

なぜなら、lodashのsliceは配列だけでなく、配列のようなオブジェクトも処理できるため、最初のパラメータinputArrayはオブジェクトになる可能性があり、lengthプロパティが数字であるとは限らないからです。

endPositionパラメータの処理

endPositionパラメータは、切り取る終了位置を指定するために使用されます。

同様に、MDNでの説明を見てみましょう:

このパラメータが負の値の場合、元の配列の末尾から数えて何番目の要素で抽出を終了するかを示します。

endPositionが省略された場合、sliceは元の配列の末尾まで抽出を続けます。

endPositionが配列の長さより大きい場合、sliceは元の配列の末尾まで抽出を続けます。

endPosition = endPosition === undefined ? arraySize : endPosition

この部分はendPositionが省略された場合の処理で、省略時はendPositionがデフォルトでarraySize、つまり配列の末尾まで切り取るように設定されます。

endPosition = endPosition > arraySize ? arraySize : endPosition

これはendPositionが配列の長さより大きい場合の処理で、配列の長さより大きい場合も配列の末尾まで切り取ります。

if (endPosition < 0) {
  endPosition += arraySize
}

この部分は負の値の場合の処理で、負の値の場合は配列の末尾から前方に数えます。

startPositionのようにendPositionが前方に数えた後が負数になるかどうかを制御する部分はありません。なぜなら、その後にもう1層の制御があるからです。

新しい配列の長さの取得

arraySize = startPosition > endPosition ? 0 : ((endPosition - startPosition) >>> 0)

新しい配列の長さの計算方法は非常に簡単で、endPosition - startPositionを使用するだけです。

先ほど述べたように、最終的なendPositionが負数になるかどうかを制御する部分はありません。ここではstartPositionendPositionの比較を使用しており、startPositionendPositionより大きい場合は新しい配列の長さは0、つまり空の配列が返されます。それ以外の場合はendPosition - startPositionを使用して計算します。

ここでも、配列の長さが正数または0であることを保証するために、符号なし右シフト演算子が使用されています。

切り取りと新しい配列の返却

let currentIndex = -1
const extractedArray = new Array(arraySize)
while (++currentIndex < arraySize) {
  extractedArray[currentIndex] = inputArray[currentIndex + startPosition]
}
return extractedArray

extractedArrayは新しい配列のコンテナです。

whileループを使用し、startPosition位置から始めて、元の配列の値を取得し、順番に新しい配列に格納します。

インデックスを使用して値を取得するため、スパース配列に遭遇した場合、対応するインデックスの位置に要素がない場合は、配列インデックスを使用して値を取得するとundefinedが返されますが、これはスパース配列のその位置の値がundefinedであることを意味するものではありません。

最後にextractedArrayを返します。

参考

  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中的稀疏数组与密集数组

タグ: javascript lodash 配列 スパース配列 デンス配列

5月15日 23:44 投稿