为什么要有 _.slice
在 why not the 'baseslice' func use Array.slice(), loop faster than slice? 中有做说明:_.slice 会将数组当做密集数组处理,而 Array.slice 会把数组当做稀疏数组处理。
在 _.slice 的注释中也说明:
This method is used instead of
Array#sliceto 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。
_.slice 和 Array.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 时,length 为 0,即 array 为 null 或 undefined 的时候,length 都会为0。length 为 0 时,返回一个空数组 []。
start = start == null ? 0 : start
end = end === undefined ? length : end
然后对 start 和 end 参数做了处理:当 start == null,即 start 为 null 或 undefined 时,start 取 0;当 end 为 undefined 时,end 取 array 的长度。
这里 end 没有判断是否为 null ,不过当 end 为 null 时,在后续的判断与运算中会隐式转换为 0,Number(null) === 0。
if (start < 0) {
start = -start > length ? 0 : (length + start)
}
start 为负数时:若 -start 大于数组长度,即超出了数组取值范围,则 start 取 0;否则的话,取 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 时,即开始位置在结束位置后面,length 取 0;否则取 end 和 start 的差值。
>>> 为无符号右移运算,会把负数作为正数来处理。>>> 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。
补充说明
start 和 end 为 null / 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 >>> 0 为 4294967295,也就是Math.pow(2, 32) - 1
参考: