Lodash 学习笔记(二):slice

217 阅读4分钟

为什么要有 _.slice

why not the 'baseslice' func use Array.slice(), loop faster than slice? 中有做说明:_.slice 会将数组当做密集数组处理,而 Array.slice 会把数组当做稀疏数组处理。

_.slice 的注释中也说明:

This method is used instead of Array#slice to ensure dense arrays are returned.

密集数组(dense arrays) VS 稀疏数组(sparse arrays)

什么是稀疏数组?JavaScript权威指南 给出的定义为:

稀疏数组就是包含从0开始的不连续索引的数组。通常,数组的length属性值代表数组中元素的个数。如果数组是稀疏的,length属性值大于元素的个数。

也就是说,稀疏数组中至少有一个位置不存在任何元素,包括 undefined

稀疏数组

var sparse = Array(5);
sparse[4] = 4;
console.log(sparse);
// 输出: [empty × 4, 4]
console.log(sparse[0]);
// 输出:undefined 
sparse.forEach((item, index) => console.log(item, index));
// 输出:4 4 

密集数组

var dense = Array(5);
dense[0] = undefined;
dense[1] = undefined;
dense[2] = undefined;
dense[3] = undefined;
dense[4] = 4;
console.log(dense);
// 输出: [undefined, undefined, undefined, undefined, 4]
console.log(dense[0]);
// 输出:undefined 
dense.forEach((item, index) => console.log(item, index));
// 输出:undefined 0
//       undefined 1
//       undefined 2
//       undefined 3
//       4 4

我们可以发现,稀疏数组和密集数组最大的区别是在迭代器中的表现。稀疏数组会跳过没有元素的项,而密集数组会遍历所有的元素项,即使该元素项是 undefined

_.sliceArray.slice 操作稀疏数组

_.slice 操作稀疏数组

var sparse = Array(5);
sparse[4] = 4;
var result = _.slice(sparse, 0 ,3);
console.log(result);
// 输出:[undefined, undefined, undefined]

Array.slice 操作稀疏数组

var sparse = Array(5);
sparse[4] = 4;
var result = sparse.slice(0 ,3);
console.log(result);
// 输出:[empty × 3]

很明显 _.slice 会保证返回的数组是密集数组,即使输入的数组是稀疏数组。而 Array.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 []
}

首先对参数 array 做了处理:若 array == null 时,length0,即 arraynullundefined 的时候,length 都会为0length0 时,返回一个空数组 []

start = start == null ? 0 : start
end = end === undefined ? length : end

然后对 startend 参数做了处理:当 start == null,即 startnullundefined 时,start0;当 endundefined 时,endarray 的长度。

这里 end 没有判断是否为 null ,不过当 endnull 时,在后续的判断与运算中会隐式转换为 0Number(null) === 0

if (start < 0) {
  start = -start > length ? 0 : (length + start)
}

start 为负数时:若 -start 大于数组长度,即超出了数组取值范围,则 start0;否则的话,取 length + start,即从后向前数所在的索引值。

end = end > length ? length : end
if (end < 0) {
  end += length
}

end 大于数组长度时,end 取数组长度;

end 小于 0 时,取 end + length,即从后向前数对应的索引。

length = start > end ? 0 : ((end - start) >>> 0)
start >>>= 0

start 大于 end 时,即开始位置在结束位置后面,length0;否则取 endstart 的差值。

>>> 为无符号右移运算,会把负数作为正数来处理。>>> 0 用来保证 length 大于等 于 0

>>>=+= 类似, start = start >>> 0,也保证 start 大于等于 0

let index = -1
const result = new Array(length)
while (++index < length) {
  result[index] = array[index + start]
}

长度确认后,以 result 为容器,从 start 开始,在 array 中取对应位置的值,并将值放在 result 指定的位置中。最终返回 result

补充说明

startendnull / undefined 时的输出

_.slice([11,22], undefined, undefined) //=> [11,22]
_.slice([11,22], null, undefined) //=> [11,22]
_.slice([11,22], undefined, null) //=> []
_.slice([11,22], null, null) //=> []

关于负数的二进制、无符号右移运算

1 的32位二进制 0000 0000 0000 0000 0000 0000 0000 0001

1 的反码 1111 1111 1111 1111 1111 1111 1111 1110

-1 的32位二进制 1111 1111 1111 1111 1111 1111 1111 1111

负数的二进制是正数二进制的反码 + 1

所以 -1 >>> 04294967295,也就是Math.pow(2, 32) - 1

参考: